Browse Source

mol-state: accept partial params for transforms; refactored measurements

David Sehnal 5 years ago
parent
commit
83a8474731

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

@@ -59,7 +59,7 @@ export class StructureHierarchyManager {
 
         plugin.behaviors.state.isAnimating.subscribe(isAnimating => {
             if (!isAnimating && !plugin.behaviors.state.isUpdating.value) this.sync();
-        })
+        });
     }
 }
 

+ 245 - 0
src/mol-plugin-state/manager/structure/measurement.ts

@@ -0,0 +1,245 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureElement } from '../../../mol-model/structure';
+import { PluginContext } from '../../../mol-plugin/context';
+import { StateSelection, StateTransform, StateTransformer, StateObject, StateObjectCell } from '../../../mol-state';
+import { StateTransforms } from '../../transforms';
+import { PluginCommands } from '../../../mol-plugin/commands';
+import { arraySetAdd } from '../../../mol-util/array';
+import { PluginStateObject } from '../../objects';
+import { PluginComponent } from '../../../mol-plugin/component';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { MeasurementRepresentationCommonTextParams } from '../../../mol-repr/shape/loci/common';
+
+export { StructureMeasurementManager }
+
+export const MeasurementGroupTag = 'measurement-group';
+
+export type StructureMeasurementCell = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Structure.Selections, PluginStateObject.Shape.Representation3D, any>>>
+
+export const StructureMeasurementParams = {
+    distanceUnitLabel: PD.Text('\u212B', { isEssential: true }),
+    ...MeasurementRepresentationCommonTextParams
+}
+const DefaultStructureMeasurementOptions = PD.getDefaultValues(StructureMeasurementParams)
+export type StructureMeasurementOptions = PD.ValuesFor<typeof StructureMeasurementParams>
+
+export interface StructureMeasurementManagerState {
+    labels: StructureMeasurementCell[],
+    distances: StructureMeasurementCell[],
+    angles: StructureMeasurementCell[],
+    dihedrals: StructureMeasurementCell[],
+    // TODO: orientations
+    options: StructureMeasurementOptions
+}
+
+class StructureMeasurementManager extends PluginComponent<StructureMeasurementManagerState>  {
+    readonly behaviors = {
+        state: this.ev.behavior(this.state)
+    };
+
+    private stateUpdated() {
+        this.behaviors.state.next(this.state);
+    }
+
+    private getGroup() {
+        const state = this.plugin.state.dataState;
+        const groupRef = StateSelection.findTagInSubtree(state.tree, StateTransform.RootRef, MeasurementGroupTag);
+        const builder = this.plugin.state.dataState.build();
+
+        if (groupRef) return builder.to(groupRef);
+        return builder.toRoot().group(StateTransforms.Misc.CreateGroup, { label: `Measurements` }, { tags: MeasurementGroupTag });
+    }
+
+    async setOptions(options: StructureMeasurementOptions) {
+        this.updateState({ options });
+
+        if (this.state.distances.length === 0) {
+            this.stateUpdated();
+            return;
+        }
+
+        const update = this.plugin.state.dataState.build();
+        for (const cell of this.state.distances) {
+            update.to(cell).update((old: any) => { 
+                old.unitLabel = options.distanceUnitLabel;
+                old.textColor = options.textColor;
+                old.textSize = options.textSize;
+            });
+        }
+        for (const cell of this.state.labels) {
+            update.to(cell).update((old: any) => { old.textColor = options.textColor; old.textSize = options.textSize; });
+        }
+        for (const cell of this.state.angles) {
+            update.to(cell).update((old: any) => { old.textColor = options.textColor; old.textSize = options.textSize; });
+        }
+        for (const cell of this.state.dihedrals) {
+            update.to(cell).update((old: any) => { old.textColor = options.textColor; old.textSize = options.textSize; });
+        }
+        await PluginCommands.State.Update(this.plugin, { state: this.plugin.state.dataState, tree: update, options: { doNotLogTiming: true } });
+    }
+
+    async addDistance(a: StructureElement.Loci, b: StructureElement.Loci) {
+        const cellA = this.plugin.helpers.substructureParent.get(a.structure);
+        const cellB = this.plugin.helpers.substructureParent.get(b.structure);
+
+        if (!cellA || !cellB) return;
+
+        const dependsOn = [cellA.transform.ref];
+        arraySetAdd(dependsOn, cellB.transform.ref);
+
+        const update = this.getGroup();
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections: [
+                    { key: 'a', groupId: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
+                    { key: 'b', groupId: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) }
+                ],
+                isTransitive: true,
+                label: 'Distance'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsDistance3D, { unitLabel: this.state.options.distanceUnitLabel })
+
+        const state = this.plugin.state.dataState;
+        await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+    }
+
+    async addAngle(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci) {
+        const cellA = this.plugin.helpers.substructureParent.get(a.structure);
+        const cellB = this.plugin.helpers.substructureParent.get(b.structure);
+        const cellC = this.plugin.helpers.substructureParent.get(c.structure);
+
+        if (!cellA || !cellB || !cellC) return;
+
+        const dependsOn = [cellA.transform.ref];
+        arraySetAdd(dependsOn, cellB.transform.ref);
+        arraySetAdd(dependsOn, cellC.transform.ref);
+
+        const update = this.getGroup();
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections: [
+                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
+                    { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) },
+                    { key: 'c', ref: cellC.transform.ref, expression: StructureElement.Loci.toExpression(c) }
+                ],
+                isTransitive: true,
+                label: 'Angle'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsAngle3D)
+
+        const state = this.plugin.state.dataState;
+        await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+    }
+
+    async addDihedral(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci, d: StructureElement.Loci) {
+        const cellA = this.plugin.helpers.substructureParent.get(a.structure);
+        const cellB = this.plugin.helpers.substructureParent.get(b.structure);
+        const cellC = this.plugin.helpers.substructureParent.get(c.structure);
+        const cellD = this.plugin.helpers.substructureParent.get(d.structure);
+
+        if (!cellA || !cellB || !cellC || !cellD) return;
+
+        const dependsOn = [cellA.transform.ref];
+        arraySetAdd(dependsOn, cellB.transform.ref);
+        arraySetAdd(dependsOn, cellC.transform.ref);
+        arraySetAdd(dependsOn, cellD.transform.ref);
+
+        const update = this.getGroup();
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections: [
+                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
+                    { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) },
+                    { key: 'c', ref: cellC.transform.ref, expression: StructureElement.Loci.toExpression(c) },
+                    { key: 'd', ref: cellD.transform.ref, expression: StructureElement.Loci.toExpression(d) }
+                ],
+                isTransitive: true,
+                label: 'Dihedral'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsDihedral3D)
+
+        const state = this.plugin.state.dataState;
+        await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+    }
+
+    async addLabel(a: StructureElement.Loci) {
+        const cellA = this.plugin.helpers.substructureParent.get(a.structure);
+
+        if (!cellA) return;
+
+        const dependsOn = [cellA.transform.ref];
+
+        const update = this.getGroup();
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections: [
+                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
+                ],
+                isTransitive: true,
+                label: 'Label'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsLabel3D)
+
+        const state = this.plugin.state.dataState;
+        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);
+
+        if (!cellA) return;
+
+        const dependsOn = [cellA.transform.ref];
+
+        const update = this.getGroup();
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections: [
+                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
+                ],
+                isTransitive: true,
+                label: 'Orientation'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsOrientation3D)
+
+        const state = this.plugin.state.dataState;
+        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.dataState;
+        const groupRef = StateSelection.findTagInSubtree(state.tree, StateTransform.RootRef, MeasurementGroupTag);
+        const ret = groupRef ? state.select(StateSelection.Generators.ofTransformer(transformer, groupRef)) : this._empty;
+        if (ret.length === 0) return this._empty;
+        return ret;
+    }
+
+    private sync() {
+        const updated = this.updateState({
+            labels: this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D),
+            distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D),
+            angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D),
+            dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D)
+        });
+        if (updated) this.stateUpdated();
+    }
+
+    constructor(private plugin: PluginContext) {
+        super({ labels: [], distances: [], angles: [], dihedrals: [], options: DefaultStructureMeasurementOptions });
+
+        plugin.state.dataState.events.changed.subscribe(e => {
+            if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return;
+            this.sync();
+        });
+
+        plugin.behaviors.state.isAnimating.subscribe(isAnimating => {
+            if (!isAnimating && !plugin.behaviors.state.isUpdating.value) this.sync();
+        });
+    }
+}

+ 3 - 1
src/mol-plugin-ui/controls/common.tsx

@@ -268,7 +268,9 @@ export function IconButton(props: {
     extraContent?: JSX.Element
 }) {
     let className = `msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}${props.customClass ? ' ' + props.customClass : ''}`;
-    if (typeof props.toggleState !== 'undefined') className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}`
+    if (typeof props.toggleState !== 'undefined') {
+        className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}`
+    }
     return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']} style={props.style}>
         <Icon name={props.icon} />
         {props.extraContent}

+ 9 - 2
src/mol-plugin-ui/controls/parameters.tsx

@@ -30,13 +30,20 @@ export type ParameterControlsCategoryFilter = string | null | (string | null)[]
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
-    onChange: ParamsOnChange<PD.Values<P>>,
+    onChange?: ParamsOnChange<PD.ValuesFor<P>>,
+    onChangeObject?: (values: PD.ValuesFor<P>, prev: PD.ValuesFor<P>) => void,
     isDisabled?: boolean,
     onEnter?: () => void
 }
 
 export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>> {
-    onChange: ParamOnChange = (params) => this.props.onChange(params, this.props.values);
+    onChange: ParamOnChange = (params) => {
+        this.props.onChange?.(params, this.props.values);
+        if (this.props.onChangeObject) {
+            const values = { ...this.props.values, [params.name]: params.value };
+            this.props.onChangeObject(values, this.props.values);
+        }
+    }
 
     renderGroup(group: ParamInfo[]) {
         if (group.length === 0) return null;

+ 2 - 2
src/mol-plugin-ui/skin/base/components/controls-base.scss

@@ -68,11 +68,11 @@
 }
 
 .msp-btn-link-toggle-off, .msp-btn-link-toggle-off:active, .msp-btn-link-toggle-off:focus {
-    color: $msp-btn-link-toggle-off-font-color;
+    color: $msp-btn-link-toggle-off-font-color !important;
 }
 
 .msp-btn-link-toggle-off:hover,  .msp-btn-link-toggle-on:hover {
-    color: $hover-font-color;
+    color: $hover-font-color !important;
 }
 
 @mixin msp-btn($name, $font, $bg) {

+ 1 - 1
src/mol-plugin-ui/structure/components.tsx

@@ -27,7 +27,7 @@ export class StructureComponentControls extends CollapsableControls<{}, Structur
     }
 
     get currentModels() {
-        return this.plugin.managers.structureHierarchy.behaviors.currentModels;
+        return this.plugin.managers.structure.hierarchy.behaviors.currentModels;
     }
 
     componentDidMount() {

+ 112 - 121
src/mol-plugin-ui/structure/measurements.tsx

@@ -5,184 +5,175 @@
  */
 
 import * as React from 'react';
-import { CollapsableControls, CollapsableState } from '../base';
+import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
 import { lociLabel, dihedralLabel, angleLabel, distanceLabel } from '../../mol-theme/label';
 import { Loci } from '../../mol-model/loci';
 import { FiniteArray } from '../../mol-util/type-helpers';
-import { StateObjectCell, StateTransform, StateTransformer } from '../../mol-state';
+import { State } from '../../mol-state';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
-import { ShapeRepresentation } from '../../mol-repr/shape/representation';
-import { IconButton } from '../controls/common';
+import { IconButton, ExpandGroup } from '../controls/common';
 import { PluginCommands } from '../../mol-plugin/commands';
+import { StructureMeasurementCell, StructureMeasurementOptions, StructureMeasurementParams } from '../../mol-plugin-state/manager/structure/measurement';
+import { ParameterControls } from '../controls/parameters';
 
 // TODO details, options (e.g. change text for labels)
 // TODO better updates on state changes
 
-type MeasurementTransform = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Structure.Selections, PluginStateObject.Shape.Representation3D, any>>>
-
+const MeasurementFocusOptions = {
+    minRadius: 8,
+    extraRadius: 4,
+    durationMs: 250,
+    unitLabel: '\u212B',
+}
 
 interface StructureMeasurementsControlsState extends CollapsableState {
-    minRadius: number,
-    extraRadius: number,
-    durationMs: number,
     unitLabel: string,
 
     isDisabled: boolean,
 }
 
-export class StructureMeasurementsControls<P, S extends StructureMeasurementsControlsState> extends CollapsableControls<P, S> {
+export class StructureMeasurementsControls extends CollapsableControls<{}, StructureMeasurementsControlsState> {
     componentDidMount() {
-        this.subscribe(this.plugin.events.state.object.updated, ({ }) => {
-            // TODO
-            this.forceUpdate()
-        })
-
-        this.subscribe(this.plugin.events.state.object.created, ({ }) => {
-            // TODO
-            this.forceUpdate()
-        })
-
-        this.subscribe(this.plugin.events.state.object.removed, ({ }) => {
-            // TODO
-            this.forceUpdate()
-        })
+        this.subscribe(this.plugin.managers.structure.measurement.behaviors.state, () => {
+            this.forceUpdate();
+        });
 
         this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
             this.setState({ isDisabled: v })
-        })
-    }
-
-    focus(selections: PluginStateObject.Molecule.Structure.Selections) {
-        return () => {
-            const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections.data))
-            if (sphere) {
-                const { extraRadius, minRadius, durationMs } = this.state
-                const radius = Math.max(sphere.radius + extraRadius, minRadius);
-                this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
-            }
-        }
+        });
     }
 
     defaultState() {
         return {
             isCollapsed: false,
             header: 'Measurements',
-
-            minRadius: 8,
-            extraRadius: 4,
-            durationMs: 250,
             unitLabel: '\u212B',
-
             isDisabled: false
-        } as S
+        } as StructureMeasurementsControlsState
     }
 
-    getLabel(selections: PluginStateObject.Molecule.Structure.Selections) {
-        switch (selections.data.length) {
-            case 1: return lociLabel(selections.data[0].loci, { condensed: true })
-            case 2: return distanceLabel(toLociBundle(selections.data), { condensed: true })
-            case 3: return angleLabel(toLociBundle(selections.data), { condensed: true })
-            case 4: return dihedralLabel(toLociBundle(selections.data), { condensed: true })
+    renderGroup(cells: ReadonlyArray<StructureMeasurementCell>, header: string) {
+        const group: JSX.Element[] = [];
+        for (const cell of cells) {
+            if (cell.obj) group.push(<MeasurementEntry key={cell.obj.id} cell={cell} />)
         }
-        return ''
+        return group.length ? <ExpandGroup header={header} initiallyExpanded={true}>{group}</ExpandGroup> : null;
     }
 
-    highlight(repr: ShapeRepresentation<any, any, any>, selections: PluginStateObject.Molecule.Structure.Selections) {
-        return () => {
-            this.plugin.interactivity.lociHighlights.clearHighlights();
-            for (const d of selections.data) {
-                this.plugin.interactivity.lociHighlights.highlight({ loci: d.loci }, false);
-            }
-            this.plugin.interactivity.lociHighlights.highlight({ loci: repr.getLoci() }, false);
-        }
+    renderControls() {
+        const measurements = this.plugin.managers.structure.measurement.state;
+
+        return <>
+            {this.renderGroup(measurements.labels, 'Labels')}
+            {this.renderGroup(measurements.distances, 'Distances')}
+            {this.renderGroup(measurements.angles, 'Angles')}
+            {this.renderGroup(measurements.dihedrals, 'Dihedrals')}
+            <MeasurementsOptions />
+        </>
     }
+}
 
-    clearHighlight() {
-        return () => {
-            this.plugin.interactivity.lociHighlights.clearHighlights();
-        }
-    }
+export class MeasurementsOptions extends PurePluginUIComponent<{}, { isDisabled: boolean }> {
+    state = { isDisabled: false }
 
-    delete(cell: MeasurementTransform) {
-        return () => {
-            PluginCommands.State.RemoveObject(this.plugin, { state: cell.parent, ref: cell.transform.parent, removeParentGhosts: true });
-        }
-    }
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.measurement.behaviors.state, () => {
+            this.forceUpdate();
+        });
 
-    toggleVisibility(cell: MeasurementTransform) {
-        return (e: React.MouseEvent<HTMLElement>) => {
-            e.preventDefault();
-            PluginCommands.State.ToggleVisibility(this.plugin, { state: cell.parent, ref: cell.transform.parent });
-            e.currentTarget.blur();
-        }
+        this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
+            this.setState({ isDisabled: v })
+        });
     }
 
-    getRow(cell: MeasurementTransform) {
-        const { obj } = cell
-        if (!obj) return null
+    changed = (options: StructureMeasurementOptions) => {
+        this.plugin.managers.structure.measurement.setOptions(options);
+    }
 
-        const selections = obj.data.source as PluginStateObject.Molecule.Structure.Selections
+    render() {
+        const measurements = this.plugin.managers.structure.measurement.state;
 
-        return <div className='msp-btn-row-group' key={obj.id} onMouseEnter={this.highlight(obj.data.repr, selections)} onMouseLeave={this.clearHighlight()}>
-            <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={this.focus(selections)}>
-                <span dangerouslySetInnerHTML={{ __html: this.getLabel(selections) }} />
-            </button>
-            <IconButton isSmall={true} customClass='msp-form-control' onClick={this.delete(cell)} icon='remove' style={{ width: '52px' }} title='Delete' />
-            <IconButton isSmall={true} customClass='msp-form-control' onClick={this.toggleVisibility(cell)} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={cell.state.isHidden} />
-        </div>
+        return <ExpandGroup header='Options'>
+            <ParameterControls params={StructureMeasurementParams} values={measurements.options} onChangeObject={this.changed} isDisabled={this.state.isDisabled} />
+        </ExpandGroup>;
     }
+}
 
-    getData() {
+class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasurementCell }> {
+    componentDidMount() {
+        this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
+            if (State.ObjectEvent.isCell(e, this.props.cell)) {
+                this.forceUpdate();
+            }
+        });
+    }
 
+    get selections() {
+        return this.props.cell.obj?.data.source as PluginStateObject.Molecule.Structure.Selections | undefined;
     }
 
-    renderControls() {
-        const labels: JSX.Element[] = [];
-        const distances: JSX.Element[] = [];
-        const angles: JSX.Element[] = [];
-        const dihedrals: JSX.Element[] = [];
+    delete = () => {
+        PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent, removeParentGhosts: true });
+    };
 
-        const measurements = this.plugin.helpers.measurement.getMeasurements();
+    toggleVisibility = (e: React.MouseEvent<HTMLElement>) => {
+        e.preventDefault();
+        PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent });
+        e.currentTarget.blur();
+    }
 
-        for (const d of measurements.labels) {
-            const row = this.getRow(d)
-            if (row) labels.push(row)
-        }
+    highlight = () => {
+        const selections = this.selections;
+        if (!selections) return;
 
-        for (const d of measurements.distances) {
-            const row = this.getRow(d)
-            if (row) distances.push(row)
+        this.plugin.interactivity.lociHighlights.clearHighlights();
+        for (const d of selections.data) {
+            this.plugin.interactivity.lociHighlights.highlight({ loci: d.loci }, false);
         }
+        this.plugin.interactivity.lociHighlights.highlight({ loci: this.props.cell.obj?.data.repr.getLoci()! }, false);
+    }
 
-        for (const d of measurements.angles) {
-            const row = this.getRow(d)
-            if (row) angles.push(row)
+    clearHighlight = () => {
+        this.plugin.interactivity.lociHighlights.clearHighlights();
+    }
+
+    focus = () => {
+        const selections = this.selections;
+        if (!selections) return;
+
+        const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections.data))
+        if (sphere) {
+            const { extraRadius, minRadius, durationMs } = MeasurementFocusOptions;
+            const radius = Math.max(sphere.radius + extraRadius, minRadius);
+            this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
         }
+    }
 
-        for (const d of measurements.dihedrals) {
-            const row = this.getRow(d)
-            if (row) dihedrals.push(row)
+    get label() {
+        const selections = this.selections;
+        switch (selections?.data.length) {
+            case 1: return lociLabel(selections.data[0].loci, { condensed: true })
+            case 2: return distanceLabel(toLociBundle(selections.data), { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel })
+            case 3: return angleLabel(toLociBundle(selections.data), { condensed: true })
+            case 4: return dihedralLabel(toLociBundle(selections.data), { condensed: true })
+            default: return ''
         }
+    }
+
+    render() {
+        const { cell } = this.props;
+        const { obj } = cell;
+        if (!obj) return null;
 
-        return <div>
-            {labels.length > 0 && <div>
-                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Labels</span></div>
-                <div className='msp-control-offset'>{labels}</div>
-            </div>}
-            {distances.length > 0 && <div>
-                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Distances</span></div>
-                <div className='msp-control-offset'>{distances}</div>
-            </div>}
-            {angles.length > 0 && <div>
-                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Angles</span></div>
-                <div className='msp-control-offset'>{angles}</div>
-            </div>}
-            {dihedrals.length > 0 && <div>
-                <div className='msp-control-group-header' style={{ marginTop: '1px' }}><span>Dihedrals</span></div>
-                <div className='msp-control-offset'>{dihedrals}</div>
-            </div>}
+        return <div className='msp-btn-row-group' key={obj.id} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}>
+            <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={this.focus}>
+                <span dangerouslySetInnerHTML={{ __html: this.label }} />
+            </button>
+            <IconButton isSmall={true} customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ width: '52px' }} title='Delete' />
+            <IconButton isSmall={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
         </div>
     }
+
 }
 
 function toLociBundle(data: FiniteArray<{ loci: Loci }, any>): { loci: FiniteArray<Loci, any> } {

+ 5 - 5
src/mol-plugin-ui/structure/selection.tsx

@@ -83,27 +83,27 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
 
     measureDistance = () => {
         const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
-        this.plugin.helpers.measurement.addDistance(loci[0].loci, loci[1].loci);
+        this.plugin.managers.structure.measurement.addDistance(loci[0].loci, loci[1].loci);
     }
 
     measureAngle = () => {
         const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
-        this.plugin.helpers.measurement.addAngle(loci[0].loci, loci[1].loci, loci[2].loci);
+        this.plugin.managers.structure.measurement.addAngle(loci[0].loci, loci[1].loci, loci[2].loci);
     }
 
     measureDihedral = () => {
         const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
-        this.plugin.helpers.measurement.addDihedral(loci[0].loci, loci[1].loci, loci[2].loci, loci[3].loci);
+        this.plugin.managers.structure.measurement.addDihedral(loci[0].loci, loci[1].loci, loci[2].loci, loci[3].loci);
     }
 
     addLabel = () => {
         const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
-        this.plugin.helpers.measurement.addLabel(loci[0].loci);
+        this.plugin.managers.structure.measurement.addLabel(loci[0].loci);
     }
 
     addOrientation = () => {
         const loci = this.plugin.helpers.structureSelectionManager.latestLoci;
-        this.plugin.helpers.measurement.addOrientation(loci[0].loci);
+        this.plugin.managers.structure.measurement.addOrientation(loci[0].loci);
     }
 
     setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {

+ 5 - 3
src/mol-plugin/context.ts

@@ -41,7 +41,7 @@ import { StructureRepresentationHelper } from './util/structure-representation-h
 import { StructureSelectionHelper } from './util/structure-selection-helper';
 import { StructureOverpaintHelper } from './util/structure-overpaint-helper';
 import { PluginToastManager } from './util/toast';
-import { StructureMeasurementManager } from './util/structure-measurement';
+import { StructureMeasurementManager } from '../mol-plugin-state/manager/structure/measurement';
 import { ViewportScreenshotHelper } from './util/viewport-screenshot';
 import { RepresentationBuilder } from '../mol-plugin-state/builder/representation';
 import { CustomProperty } from '../mol-model-props/common/custom-property';
@@ -133,7 +133,10 @@ export class PluginContext {
     };
 
     readonly managers = {
-        structureHierarchy: new StructureHierarchyManager(this)
+        structure: {
+            hierarchy: new StructureHierarchyManager(this),
+            measurement: new StructureMeasurementManager(this)
+        }
     };
 
     readonly customModelProperties = new CustomProperty.Registry<Model>();
@@ -141,7 +144,6 @@ export class PluginContext {
     readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
 
     readonly helpers = {
-        measurement: new StructureMeasurementManager(this),
         structureSelectionManager: new StructureElementSelectionManager(this),
         structureSelection: new StructureSelectionHelper(this),
         structureRepresentation: new StructureRepresentationHelper(this),

+ 0 - 191
src/mol-plugin/util/structure-measurement.ts

@@ -1,191 +0,0 @@
-/**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { StructureElement } from '../../mol-model/structure';
-import { PluginContext } from '../context';
-import { StateSelection, StateTransform, StateTransformer, StateObject } from '../../mol-state';
-import { StateTransforms } from '../../mol-plugin-state/transforms';
-import { PluginCommands } from '../commands';
-import { arraySetAdd } from '../../mol-util/array';
-import { PluginStateObject } from '../../mol-plugin-state/objects';
-
-export { StructureMeasurementManager }
-
-export const MeasurementGroupTag = 'measurement-group';
-
-class StructureMeasurementManager {
-    private getGroup() {
-        const state = this.context.state.dataState;
-        const groupRef = StateSelection.findTagInSubtree(state.tree, StateTransform.RootRef, MeasurementGroupTag);
-        const builder = this.context.state.dataState.build();
-
-        if (groupRef) return builder.to(groupRef);
-        return builder.toRoot().group(StateTransforms.Misc.CreateGroup, { label: `Measurements` }, { tags: MeasurementGroupTag });
-    }
-
-    private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) {
-        const state = this.context.state.dataState;
-        const groupRef = StateSelection.findTagInSubtree(state.tree, StateTransform.RootRef, MeasurementGroupTag);
-        return groupRef ? state.select(StateSelection.Generators.ofTransformer(transformer, groupRef)) : []
-    }
-
-    getLabels() {
-        return this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D)
-    }
-
-    getDistances() {
-        return this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D)
-    }
-
-    getAngles() {
-        return this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D)
-    }
-
-    getDihedrals() {
-        return this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D)
-    }
-
-    getMeasurements() {
-        return {
-            labels: this.getLabels(),
-            distances: this.getDistances(),
-            angles: this.getAngles(),
-            dihedrals: this.getDihedrals(),
-        }
-    }
-
-    async addDistance(a: StructureElement.Loci, b: StructureElement.Loci) {
-        const cellA = this.context.helpers.substructureParent.get(a.structure);
-        const cellB = this.context.helpers.substructureParent.get(b.structure);
-
-        if (!cellA || !cellB) return;
-
-        const dependsOn = [cellA.transform.ref];
-        arraySetAdd(dependsOn, cellB.transform.ref);
-
-        const update = this.getGroup();
-        update
-            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
-                selections: [
-                    { key: 'a', groupId: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
-                    { key: 'b', groupId: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) }
-                ],
-                isTransitive: true,
-                label: 'Distance'
-            }, { dependsOn })
-            .apply(StateTransforms.Representation.StructureSelectionsDistance3D)
-
-        const state = this.context.state.dataState;
-        await PluginCommands.State.Update(this.context, { state, tree: update, options: { doNotLogTiming: true } });
-    }
-
-    async addAngle(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci) {
-        const cellA = this.context.helpers.substructureParent.get(a.structure);
-        const cellB = this.context.helpers.substructureParent.get(b.structure);
-        const cellC = this.context.helpers.substructureParent.get(c.structure);
-
-        if (!cellA || !cellB || !cellC) return;
-
-        const dependsOn = [cellA.transform.ref];
-        arraySetAdd(dependsOn, cellB.transform.ref);
-        arraySetAdd(dependsOn, cellC.transform.ref);
-
-        const update = this.getGroup();
-        update
-            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
-                selections: [
-                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
-                    { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) },
-                    { key: 'c', ref: cellC.transform.ref, expression: StructureElement.Loci.toExpression(c) }
-                ],
-                isTransitive: true,
-                label: 'Angle'
-            }, { dependsOn })
-            .apply(StateTransforms.Representation.StructureSelectionsAngle3D)
-
-        const state = this.context.state.dataState;
-        await PluginCommands.State.Update(this.context, { state, tree: update, options: { doNotLogTiming: true } });
-    }
-
-    async addDihedral(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci, d: StructureElement.Loci) {
-        const cellA = this.context.helpers.substructureParent.get(a.structure);
-        const cellB = this.context.helpers.substructureParent.get(b.structure);
-        const cellC = this.context.helpers.substructureParent.get(c.structure);
-        const cellD = this.context.helpers.substructureParent.get(d.structure);
-
-        if (!cellA || !cellB || !cellC || !cellD) return;
-
-        const dependsOn = [cellA.transform.ref];
-        arraySetAdd(dependsOn, cellB.transform.ref);
-        arraySetAdd(dependsOn, cellC.transform.ref);
-        arraySetAdd(dependsOn, cellD.transform.ref);
-
-        const update = this.getGroup();
-        update
-            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
-                selections: [
-                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
-                    { key: 'b', ref: cellB.transform.ref, expression: StructureElement.Loci.toExpression(b) },
-                    { key: 'c', ref: cellC.transform.ref, expression: StructureElement.Loci.toExpression(c) },
-                    { key: 'd', ref: cellD.transform.ref, expression: StructureElement.Loci.toExpression(d) }
-                ],
-                isTransitive: true,
-                label: 'Dihedral'
-            }, { dependsOn })
-            .apply(StateTransforms.Representation.StructureSelectionsDihedral3D)
-
-        const state = this.context.state.dataState;
-        await PluginCommands.State.Update(this.context, { state, tree: update, options: { doNotLogTiming: true } });
-    }
-
-    async addLabel(a: StructureElement.Loci) {
-        const cellA = this.context.helpers.substructureParent.get(a.structure);
-
-        if (!cellA) return;
-
-        const dependsOn = [cellA.transform.ref];
-
-        const update = this.getGroup();
-        update
-            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
-                selections: [
-                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
-                ],
-                isTransitive: true,
-                label: 'Label'
-            }, { dependsOn })
-            .apply(StateTransforms.Representation.StructureSelectionsLabel3D)
-
-        const state = this.context.state.dataState;
-        await PluginCommands.State.Update(this.context, { state, tree: update, options: { doNotLogTiming: true } });
-    }
-
-    async addOrientation(a: StructureElement.Loci) {
-        const cellA = this.context.helpers.substructureParent.get(a.structure);
-
-        if (!cellA) return;
-
-        const dependsOn = [cellA.transform.ref];
-
-        const update = this.getGroup();
-        update
-            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
-                selections: [
-                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
-                ],
-                isTransitive: true,
-                label: 'Orientation'
-            }, { dependsOn })
-            .apply(StateTransforms.Representation.StructureSelectionsOrientation3D)
-
-        const state = this.context.state.dataState;
-        await PluginCommands.State.Update(this.context, { state, tree: update, options: { doNotLogTiming: true } });
-    }
-
-    constructor(private context: PluginContext) {
-
-    }
-}

+ 14 - 0
src/mol-repr/shape/loci/common.ts

@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 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 { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { ColorNames } from '../../../mol-util/color/names';
+
+export const MeasurementRepresentationCommonTextParams = {
+    textColor: PD.Color(ColorNames.black, { isEssential: true }),
+    textSize: PD.Numeric(0.4, { min: 0.1, max: 5, step: 0.1 }, { isEssential: true }),
+}

+ 2 - 2
src/mol-repr/shape/loci/dihedral.ts

@@ -23,6 +23,7 @@ import { Circle } from '../../../mol-geo/primitive/circle';
 import { transformPrimitive } from '../../../mol-geo/primitive/primitive';
 import { MarkerActions, MarkerAction } from '../../../mol-util/marker-action';
 import { dihedralLabel } from '../../../mol-theme/label';
+import { MeasurementRepresentationCommonTextParams } from './common';
 
 export interface DihedralData {
     quads: Loci.Bundle<4>[]
@@ -67,8 +68,7 @@ type SectorParams = typeof SectorParams
 const TextParams = {
     ...Text.Params,
     borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 }),
-    textColor: PD.Color(ColorNames.black),
-    textSize: PD.Numeric(0.4, { min: 0.1, max: 5, step: 0.1 }),
+    ...MeasurementRepresentationCommonTextParams
 }
 type TextParams = typeof TextParams
 

+ 4 - 4
src/mol-repr/shape/loci/distance.ts

@@ -18,20 +18,21 @@ import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { MarkerActions, MarkerAction } from '../../../mol-util/marker-action';
 import { distanceLabel } from '../../../mol-theme/label';
+import { MeasurementRepresentationCommonTextParams } from './common';
 
 export interface DistanceData {
     pairs: Loci.Bundle<2>[]
 }
 
 const SharedParams = {
-    unitLabel: PD.Text('\u212B')
+    unitLabel: PD.Text('\u212B', { isEssential: true })
 }
 
 const LineParams = {
     ...Lines.Params,
     ...SharedParams,
     lineSizeAttenuation: PD.Boolean(true),
-    linesColor: PD.Color(ColorNames.lightgreen),
+    linesColor: PD.Color(ColorNames.lightgreen, { isEssential: true }),
     linesSize: PD.Numeric(0.075, { min: 0.01, max: 5, step: 0.01 }),
     dashLength: PD.Numeric(0.2, { min: 0.01, max: 0.2, step: 0.01 }),
 }
@@ -41,8 +42,7 @@ const TextParams = {
     ...Text.Params,
     ...SharedParams,
     borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 }),
-    textColor: PD.Color(ColorNames.black),
-    textSize: PD.Numeric(0.4, { min: 0.1, max: 5, step: 0.1 }),
+    ...MeasurementRepresentationCommonTextParams
 }
 type TextParams = typeof TextParams
 

+ 2 - 3
src/mol-repr/shape/loci/label.ts

@@ -8,13 +8,13 @@ import { Loci } from '../../../mol-model/loci';
 import { RuntimeContext } from '../../../mol-task';
 import { Text } from '../../../mol-geo/geometry/text/text';
 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 { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { lociLabel } from '../../../mol-theme/label';
+import { MeasurementRepresentationCommonTextParams } from './common';
 
 export interface LabelData {
     infos: { loci: Loci, label?: string }[]
@@ -23,8 +23,7 @@ export interface LabelData {
 const TextParams = {
     ...Text.Params,
     borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 }),
-    textColor: PD.Color(ColorNames.black),
-    textSize: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
+    ...MeasurementRepresentationCommonTextParams,
     offsetZ: PD.Numeric(2, { min: 0, max: 10, step: 0.1 }),
 }
 type TextParams = typeof TextParams

+ 16 - 4
src/mol-state/state.ts

@@ -23,6 +23,7 @@ import { AsyncQueue } from '../mol-util/async-queue';
 import { isProductionMode } from '../mol-util/debug'
 import { arraySetAdd, arraySetRemove } from '../mol-util/array';
 import { UniqueArray } from '../mol-data/generic';
+import { assignIfUndefined } from '../mol-util/object';
 
 export { State }
 
@@ -134,6 +135,7 @@ class State {
             const snapshot = this._tree.asImmutable();
             let restored = false;
             try {
+                this.events.isUpdating.next(true);
                 this.inTransaction = true;
                 await edits();
 
@@ -151,6 +153,7 @@ class State {
             } finally {
                 this.inTransaction = false;
                 this.events.changed.next({ state: this, inTransaction: false });
+                this.events.isUpdating.next(false);
             }
         });
     }
@@ -170,6 +173,7 @@ class State {
             const removed = await this.updateQueue.enqueue(params);
             if (!removed) return;
 
+            if (!this.inTransaction) this.events.isUpdating.next(true);
             try {
                 const ret = options && (options.revertIfAborted || options.revertOnError)
                     ? await this._revertibleTreeUpdate(taskCtx, params, options)
@@ -177,6 +181,7 @@ class State {
                 return ret.cell;
             } finally {
                 this.updateQueue.handled(params);
+                if (!this.inTransaction) this.events.isUpdating.next(false);
             }
         }, () => {
             this.updateQueue.remove(params);
@@ -194,7 +199,6 @@ class State {
     }
 
     private async _updateTree(taskCtx: RuntimeContext, params: UpdateParams) {
-        this.events.isUpdating.next(true);
         let updated = false;
         const ctx = this.updateTreeAndCreateCtx(params.tree, taskCtx, params.options);
         try {
@@ -208,7 +212,6 @@ class State {
             this.spine.current = undefined;
 
             if (updated) this.events.changed.next({ state: this, inTransaction: this.inTransaction });
-            this.events.isUpdating.next(false);
         }
     }
 
@@ -279,6 +282,12 @@ namespace State {
         ref: Ref
     }
 
+    export namespace ObjectEvent {
+        export function isCell(e: ObjectEvent, cell: StateObjectCell) {
+            return e.ref === cell.transform.ref && e.state === cell.parent
+        }
+    }
+
     export interface Snapshot {
         readonly tree: StateTree.Serialized
     }
@@ -697,8 +706,11 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
 function resolveParams(ctx: UpdateContext, transform: StateTransform, src: StateObject) {
     const prms = transform.transformer.definition.params;
     const definition = prms ? prms(src, ctx.parent.globalContext) : {};
-    const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition);
-    return { definition, values };
+    const defaultValues = ParamDefinition.getDefaultValues(definition);
+    (transform.params as any) = transform.params 
+        ? assignIfUndefined(transform.params, defaultValues)
+        : defaultValues;
+    return { definition, values: transform.params };
 }
 
 async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNodeResult> {

+ 6 - 28
src/mol-state/state/builder.ts

@@ -114,7 +114,7 @@ namespace StateBuilder {
          * Apply the transformed to the parent node
          * If no params are specified (params <- undefined), default params are lazily resolved.
          */
-        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {const t = tr.apply(this.ref, params, options);
+        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: Partial<StateTransformer.Params<T>>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {const t = tr.apply(this.ref, params, options);
             this.state.tree.add(t);
             this.editInfo.count++;
             this.editInfo.lastUpdate = t.ref;
@@ -127,7 +127,7 @@ namespace StateBuilder {
          * If the ref is present, the transform is applied.
          * Otherwise a transform with the specifed ref is created.
          */
-        applyOrUpdate<T extends StateTransformer<A, any, any>>(ref: StateTransform.Ref, tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {
+        applyOrUpdate<T extends StateTransformer<A, any, any>>(ref: StateTransform.Ref, tr: T, params?: Partial<StateTransformer.Params<T>>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {
             if (this.state.tree.transforms.has(ref)) {
                 const to = this.to<StateTransformer.To<T>, T>(ref);
                 if (params) to.update(params);
@@ -141,7 +141,7 @@ namespace StateBuilder {
          * Apply the transformed to the parent node
          * If no params are specified (params <- undefined), default params are lazily resolved.
          */
-        applyOrUpdateTagged<T extends StateTransformer<A, any, any>>(tags: string | string[], tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {
+        applyOrUpdateTagged<T extends StateTransformer<A, any, any>>(tags: string | string[], tr: T, params?: Partial<StateTransformer.Params<T>>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {
             const children = this.state.tree.children.get(this.ref).values();
             while (true) {
                 const child = children.next();
@@ -174,7 +174,7 @@ namespace StateBuilder {
         /**
          * Inserts a new transform that does not change the object type and move the original children to it.
          */
-        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {
+        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: Partial<StateTransformer.Params<T>>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {
             // cache the children
             const children = this.state.tree.children.get(this.ref).toArray();
 
@@ -195,28 +195,6 @@ namespace StateBuilder {
             return new To(this.state, t.ref, this.root);
         }
 
-        // /**
-        //  * Updates a transform in an instantiated tree, passing the transform's source into the providers
-        //  *
-        //  * This only works if the transform source is NOT updated by the builder. Use at own discression.
-        //  */
-        // updateInState<T extends StateTransformer<any, A, any>>(transformer: T, state: State, provider: (old: StateTransformer.Params<T>, a: StateTransformer.From<T>) => StateTransformer.Params<T>): Root {
-        //     const old = this.state.tree.transforms.get(this.ref)!;
-        //     const cell = state.cells.get(this.ref);
-        //     if (!cell || !cell.sourceRef) throw new Error('Source cell is not present in the tree.');
-        //     const parent = state.cells.get(cell.sourceRef);
-        //     if (!parent || !parent.obj) throw new Error('Parent cell is not present or computed.');
-
-        //     const params = provider(old.params as any, parent.obj as any);
-
-        //     if (this.state.tree.setParams(this.ref, params)) {
-        //         this.editInfo.count++;
-        //         this.editInfo.lastUpdate = this.ref;
-        //     }
-
-        //     return this.root;
-        // }
-
         private updateTagged(params: any, tags: string | string[] | undefined) {
             if (this.state.tree.setParams(this.ref, params) || this.state.tree.setTags(this.ref, tags)) {
                 this.editInfo.count++;
@@ -225,8 +203,8 @@ namespace StateBuilder {
             }
         }
 
-        update<T extends StateTransformer<any, A, any>>(transformer: T, params: (old: StateTransformer.Params<T>) => StateTransformer.Params<T> | void): Root
-        update(params: StateTransformer.Params<T> | ((old: StateTransformer.Params<T>) => StateTransformer.Params<T> | void)): Root
+        update<T extends StateTransformer<any, A, any>>(transformer: T, params: (old: StateTransformer.Params<T>) => Partial<StateTransformer.Params<T>> | void): Root
+        update(params: Partial<StateTransformer.Params<T>> | ((old: StateTransformer.Params<T>) => Partial<StateTransformer.Params<T>> | void)): Root
         update<T extends StateTransformer<any, A, any>>(paramsOrTransformer: T | any, provider?: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>) {
             let params: any;
             if (provider) {

+ 12 - 0
src/mol-util/object.ts

@@ -7,6 +7,18 @@
 
 const hasOwnProperty = Object.prototype.hasOwnProperty;
 
+/** Assign to the object if a given property in update is undefined */
+export function assignIfUndefined<T>(to: Partial<T>, full: T): T {
+    for (const k of Object.keys(full)) {
+        if (!hasOwnProperty.call(full, k)) continue;
+
+        if (typeof (to as any)[k] === 'undefined') {
+            (to as any)[k] = (full as any)[k];
+        }
+    }
+    return to as T;
+}
+
 /** Create new object if any property in "update" changes in "source". */
 export function shallowMerge2<T>(source: T, update: Partial<T>): T {
     // Adapted from LiteMol (https://github.com/dsehnal/LiteMol)