Bladeren bron

mol-state: basic cellState support

David Sehnal 6 jaren geleden
bovenliggende
commit
d665ebf99f

+ 1 - 1
src/mol-model/shape/shape.ts

@@ -25,7 +25,7 @@ export namespace Shape {
         let currentGroupCount = -1
 
         return {
-            id: UUID.create(),
+            id: UUID.create22(),
             name,
             mesh,
             get groupCount() {

+ 3 - 3
src/mol-model/structure/model/formats/mmcif.ts

@@ -169,7 +169,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities
     if (previous && atomic.sameAsPrevious) {
         return {
             ...previous,
-            id: UUID.create(),
+            id: UUID.create22(),
             modelNum: atom_site.pdbx_PDB_model_num.value(0),
             atomicConformation: atomic.conformation,
             _dynamicPropertyData: Object.create(null)
@@ -182,7 +182,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities
         : format.data._name;
 
     return {
-        id: UUID.create(),
+        id: UUID.create22(),
         label,
         sourceData: format,
         modelNum: atom_site.pdbx_PDB_model_num.value(0),
@@ -208,7 +208,7 @@ function createModelIHM(format: mmCIF_Format, data: IHMData, formatData: FormatD
     const coarse = getIHMCoarse(data, formatData);
 
     return {
-        id: UUID.create(),
+        id: UUID.create22(),
         label: data.model_name,
         sourceData: format,
         modelNum: data.model_id,

+ 1 - 1
src/mol-model/structure/model/formats/mmcif/atomic.ts

@@ -62,7 +62,7 @@ function createHierarchyData(atom_site: AtomSite, offsets: { residues: ArrayLike
 
 function getConformation(atom_site: AtomSite): AtomicConformation {
     return {
-        id: UUID.create(),
+        id: UUID.create22(),
         atomId: atom_site.id,
         occupancy: atom_site.occupancy,
         B_iso_or_equiv: atom_site.B_iso_or_equiv,

+ 1 - 1
src/mol-model/structure/model/formats/mmcif/ihm.ts

@@ -49,7 +49,7 @@ export function getIHMCoarse(data: IHMData, formatData: FormatData): { hierarchy
             gaussians: { ...gaussianData, ...gaussianKeys, ...gaussianRanges },
         },
         conformation: {
-            id: UUID.create(),
+            id: UUID.create22(),
             spheres: sphereConformation,
             gaussians: gaussianConformation
         }

+ 1 - 1
src/mol-model/structure/model/properties/custom/descriptor.ts

@@ -31,7 +31,7 @@ function ModelPropertyDescriptor<Ctx, Desc extends ModelPropertyDescriptor<Ctx>>
 namespace ModelPropertyDescriptor {
     export function getUUID(prop: ModelPropertyDescriptor): UUID {
         if (!(prop as any).__key) {
-            (prop as any).__key = UUID.create();
+            (prop as any).__key = UUID.create22();
         }
         return (prop as any).__key;
     }

+ 3 - 3
src/mol-model/structure/model/properties/custom/indexed.ts

@@ -83,7 +83,7 @@ function arrayToMap<Idx extends IndexedCustomProperty.Index, T>(array: ArrayLike
 }
 
 class SegmentedMappedIndexedCustomProperty<Idx extends IndexedCustomProperty.Index, T = any> implements IndexedCustomProperty<Idx, T> {
-    readonly id: UUID = UUID.create();
+    readonly id: UUID = UUID.create22();
     readonly kind: Unit.Kind;
     has(idx: Idx): boolean { return this.map.has(idx); }
     get(idx: Idx) { return this.map.get(idx); }
@@ -129,7 +129,7 @@ class SegmentedMappedIndexedCustomProperty<Idx extends IndexedCustomProperty.Ind
 }
 
 class ElementMappedCustomProperty<T = any> implements IndexedCustomProperty<ElementIndex, T> {
-    readonly id: UUID = UUID.create();
+    readonly id: UUID = UUID.create22();
     readonly kind: Unit.Kind;
     readonly level = 'atom';
     has(idx: ElementIndex): boolean { return this.map.has(idx); }
@@ -173,7 +173,7 @@ class ElementMappedCustomProperty<T = any> implements IndexedCustomProperty<Elem
 }
 
 class EntityMappedCustomProperty<T = any> implements IndexedCustomProperty<EntityIndex, T> {
-    readonly id: UUID = UUID.create();
+    readonly id: UUID = UUID.create22();
     readonly kind: Unit.Kind;
     readonly level = 'entity';
     has(idx: EntityIndex): boolean { return this.map.has(idx); }

+ 12 - 1
src/mol-plugin/behavior/built-in/state.ts

@@ -7,6 +7,14 @@
 import { PluginCommands } from '../../command';
 import { PluginContext } from '../../context';
 
+export function registerAll(ctx: PluginContext) {
+    SetCurrentObject(ctx);
+    Update(ctx);
+    ApplyAction(ctx);
+    RemoveObject(ctx);
+    ToggleExpanded(ctx);
+}
+
 export function SetCurrentObject(ctx: PluginContext) {
     PluginCommands.State.SetCurrentObject.subscribe(ctx, ({ state, ref }) => state.setCurrent(ref));
 }
@@ -22,11 +30,14 @@ export function ApplyAction(ctx: PluginContext) {
 export function RemoveObject(ctx: PluginContext) {
     PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref }) => {
         const tree = state.tree.build().delete(ref).getTree();
-        console.log('tree', tree);
         return ctx.runTask(state.update(tree));
     });
 }
 
+export function ToggleExpanded(ctx: PluginContext) {
+    PluginCommands.State.ToggleExpanded.subscribe(ctx, ({ state, ref }) => state.updateCellState(ref, ({ isCollapsed }) => ({ isCollapsed: !isCollapsed })));
+}
+
 // export const SetCurrentObject = PluginBehavior.create({
 //     name: 'set-current-data-object-behavior',
 //     ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.SetCurrentObject, ({ state, ref }, ctx) => state.setCurrent(ref)),

+ 40 - 26
src/mol-plugin/command/command.ts

@@ -7,22 +7,22 @@
 import { PluginContext } from '../context';
 import { LinkedList } from 'mol-data/generic';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
+import { UUID } from 'mol-util';
 
 export { PluginCommand }
 
 interface PluginCommand<T = unknown> {
-    readonly id: PluginCommand.Id,
+    readonly id: UUID,
     dispatch(ctx: PluginContext, params: T): Promise<void>,
     subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription,
-    params?: { toJSON(params: T): any, fromJSON(json: any): T }
+    params: { isImmediate: boolean }
 }
 
 /** namespace.id must a globally unique identifier */
-function PluginCommand<T>(namespace: string, id: string, params?: PluginCommand<T>['params']): PluginCommand<T> {
-    return new Impl(`${namespace}.${id}` as PluginCommand.Id, params);
+function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginCommand<T> {
+    return new Impl({ isImmediate: false, ...params });
 }
 
-const cmdRepo = new Map<string, PluginCommand<any>>();
 class Impl<T> implements PluginCommand<T> {
     dispatch(ctx: PluginContext, params: T): Promise<void> {
         return ctx.commands.dispatch(this, params)
@@ -30,9 +30,8 @@ class Impl<T> implements PluginCommand<T> {
     subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription {
         return ctx.commands.subscribe(this, action);
     }
-    constructor(public id: PluginCommand.Id, public params: PluginCommand<T>['params']) {
-        if (cmdRepo.has(id)) throw new Error(`Command id '${id}' already in use.`);
-        cmdRepo.set(id, this);
+    id = UUID.create22();
+    constructor(public params: PluginCommand<T>['params']) {
     }
 }
 
@@ -44,7 +43,7 @@ namespace PluginCommand {
     }
 
     export type Action<T> = (params: T) => void | Promise<void>
-    type Instance = { id: string, params: any, resolve: () => void, reject: (e: any) => void }
+    type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void }
 
     export class Manager {
         private subs = new Map<string, Action<any>[]>();
@@ -85,22 +84,27 @@ namespace PluginCommand {
 
 
         /** Resolves after all actions have completed */
-        dispatch<T>(cmd: PluginCommand<T> | Id, params: T) {
+        dispatch<T>(cmd: PluginCommand<T>, params: T) {
             return new Promise<void>((resolve, reject) => {
                 if (this.disposing) {
                     reject('disposed');
                     return;
                 }
 
-                const id = typeof cmd === 'string' ? cmd : (cmd as PluginCommand<T>).id;
-                const actions = this.subs.get(id);
+                const actions = this.subs.get(cmd.id);
                 if (!actions) {
                     resolve();
                     return;
                 }
 
-                this.queue.addLast({ id, params, resolve, reject });
-                this.next();
+                const instance: Instance = { cmd, params, resolve, reject };
+
+                if (cmd.params.isImmediate) {
+                    this.resolve(instance);
+                } else {
+                    this.queue.addLast({ cmd, params, resolve, reject });
+                    this.next();
+                }
             });
         }
 
@@ -111,29 +115,39 @@ namespace PluginCommand {
             }
         }
 
-        private executing = false;
-        private async next() {
-            if (this.queue.count === 0 || this.executing) return;
-            const cmd = this.queue.removeFirst()!;
-
-            const actions = this.subs.get(cmd.id);
+        private async resolve(instance: Instance) {
+            const actions = this.subs.get(instance.cmd.id);
             if (!actions) {
+                try {
+                    instance.resolve();
+                } finally {
+                    if (!instance.cmd.params.isImmediate && !this.disposing) this.next();
+                }
                 return;
             }
 
             try {
-                this.executing = true;
+                if (!instance.cmd.params.isImmediate) this.executing = true;
                 // TODO: should actions be called "asynchronously" ("setImmediate") instead?
                 for (const a of actions) {
-                    await a(cmd.params);
+                    await a(instance.params);
                 }
-                cmd.resolve();
+                instance.resolve();
             } catch (e) {
-                cmd.reject(e);
+                instance.reject(e);
             } finally {
-                this.executing = false;
-                if (!this.disposing) this.next();
+                if (!instance.cmd.params.isImmediate) {
+                    this.executing = false;
+                    if (!this.disposing) this.next();
+                }
             }
         }
+
+        private executing = false;
+        private async next() {
+            if (this.queue.count === 0 || this.executing) return;
+            const instance = this.queue.removeFirst()!;
+            this.resolve(instance);
+        }
     }
 }

+ 6 - 4
src/mol-plugin/command/state.ts

@@ -8,10 +8,12 @@ import { PluginCommand } from './command';
 import { Transform, State } from 'mol-state';
 import { StateAction } from 'mol-state/action';
 
-export const SetCurrentObject = PluginCommand<{ state: State, ref: Transform.Ref }>('ms-data', 'set-current-object');
-export const ApplyAction = PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>('ms-data', 'apply-action');
-export const Update = PluginCommand<{ state: State, tree: State.Tree | State.Builder }>('ms-data', 'update');
+export const SetCurrentObject = PluginCommand<{ state: State, ref: Transform.Ref }>();
+export const ApplyAction = PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>();
+export const Update = PluginCommand<{ state: State, tree: State.Tree | State.Builder }>();
 
 // export const UpdateObject = PluginCommand<{ ref: Transform.Ref, params: any }>('ms-data', 'update-object');
 
-export const RemoveObject = PluginCommand<{ state: State, ref: Transform.Ref }>('ms-data', 'remove-object');
+export const RemoveObject = PluginCommand<{ state: State, ref: Transform.Ref }>();
+
+export const ToggleExpanded = PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true });

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

@@ -82,17 +82,14 @@ export class PluginContext {
     }
 
     private initBuiltInBehavior() {
-        BuiltInPluginBehaviors.State.ApplyAction(this);
-        BuiltInPluginBehaviors.State.RemoveObject(this);
-        BuiltInPluginBehaviors.State.SetCurrentObject(this);
-        BuiltInPluginBehaviors.State.Update(this);
+        BuiltInPluginBehaviors.State.registerAll(this);
     }
 
     async _test_initBehaviors() {
         const tree = this.state.behavior.tree.build()
             .toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas, { ref: PluginBehaviors.Representation.AddRepresentationToCanvas.id })
-            .and().toRoot().apply(PluginBehaviors.Representation.HighlightLoci, { ref: PluginBehaviors.Representation.HighlightLoci.id })
-            .and().toRoot().apply(PluginBehaviors.Representation.SelectLoci, { ref: PluginBehaviors.Representation.SelectLoci.id })
+            .toRoot().apply(PluginBehaviors.Representation.HighlightLoci, { ref: PluginBehaviors.Representation.HighlightLoci.id })
+            .toRoot().apply(PluginBehaviors.Representation.SelectLoci, { ref: PluginBehaviors.Representation.SelectLoci.id })
             .getTree();
 
         await this.runTask(this.state.behavior.update(tree));

+ 16 - 2
src/mol-plugin/ui/state-tree.tsx

@@ -9,6 +9,7 @@ import { PluginStateObject } from 'mol-plugin/state/objects';
 import { State } from 'mol-state'
 import { PluginCommands } from 'mol-plugin/command';
 import { PluginComponent } from './base';
+import { merge } from 'rxjs';
 
 export class StateTree extends PluginComponent<{ state: State }, { }> {
     componentDidMount() {
@@ -26,6 +27,12 @@ export class StateTree extends PluginComponent<{ state: State }, { }> {
 }
 
 export class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { }> {
+    componentDidMount() {
+        this.subscribe(merge(this.context.events.state.data.object.cellState, this.context.events.state.behavior.object.cellState), o => {
+            if (o.ref === this.props.nodeRef && o.state === this.props.state) this.forceUpdate();
+        });
+    }
+
     render() {
         const n = this.props.state.tree.nodes.get(this.props.nodeRef)!;
         const cell = this.props.state.cells.get(this.props.nodeRef)!;
@@ -50,10 +57,17 @@ export class StateTreeNode extends PluginComponent<{ nodeRef: string, state: Sta
             }}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>;
         }
 
+        const expander = <>
+            [<a href='#' onClick={e => {
+                e.preventDefault();
+                PluginCommands.State.ToggleExpanded.dispatch(this.context, { state: this.props.state, ref: this.props.nodeRef });
+            }}>{cell.transform.cellState.isCollapsed ? '+' : '-'}</a>]
+        </>;
+
         const children = this.props.state.tree.children.get(this.props.nodeRef);
         return <div>
-            {remove} {label}
-            {children.size === 0
+            {remove}{children.size === 0 ? void 0 : expander} {label}
+            {cell.transform.cellState.isCollapsed || children.size === 0
                 ? void 0
                 : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999' }}>{children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)}</div>
             }

+ 1 - 1
src/mol-state/action.ts

@@ -55,7 +55,7 @@ namespace StateAction {
     export function create<A extends StateObject, T, P>(definition: Definition<A, T, P>): StateAction<A, T, P> {
         const action: StateAction<A, T, P> = {
             create(params) { return { action, params }; },
-            id: UUID.create(),
+            id: UUID.create22(),
             definition
         };
         return action;

+ 6 - 10
src/mol-state/object.ts

@@ -29,7 +29,7 @@ namespace StateObject {
         return class implements StateObject<Data, T> {
             static type = type;
             static is(obj?: StateObject): obj is StateObject<Data, T> { return !!obj && type === obj.type; }
-            id = UUID.create();
+            id = UUID.create22();
             type = type;
             label: string;
             description?: string;
@@ -49,6 +49,7 @@ interface StateObjectCell {
 
     version: string
     status: StateObjectCell.Status,
+    visibility: StateObjectCell.Visibility,
 
     errorText?: string,
     obj?: StateObject
@@ -58,16 +59,11 @@ namespace StateObjectCell {
     export type Status = 'ok' | 'error' | 'pending' | 'processing'
 
     export interface State {
-        isObjectHidden: boolean,
-        isTransformHidden: boolean,
-        isBinding: boolean,
+        isHidden: boolean,
         isCollapsed: boolean
     }
 
-    export const DefaultState: State = {
-        isObjectHidden: false,
-        isTransformHidden: false,
-        isBinding: false,
-        isCollapsed: false
-    };
+    export const DefaultState: State = { isHidden: false, isCollapsed: false };
+
+    export type Visibility = 'visible' | 'hidden' | 'partial'
 }

+ 25 - 10
src/mol-state/state.ts

@@ -15,11 +15,12 @@ import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { StateTreeBuilder } from './tree/builder';
 import { StateAction } from './action';
 import { StateActionManager } from './action/manager';
+import { TransientTree } from './tree/transient';
 
 export { State }
 
 class State {
-    private _tree: StateTree = StateTree.createEmpty();
+    private _tree: TransientTree = StateTree.createEmpty().asTransient();
 
     protected errorFree = true;
     private transformCache = new Map<Transform.Ref, unknown>();
@@ -46,7 +47,7 @@ class State {
 
     readonly actions = new StateActionManager();
 
-    get tree() { return this._tree; }
+    get tree(): StateTree { return this._tree; }
     get current() { return this.behaviors.currentObject.value.ref; }
 
     build() { return this._tree.build(); }
@@ -66,8 +67,14 @@ class State {
         this.behaviors.currentObject.next({ state: this, ref });
     }
 
-    updateCellState(ref: Transform.Ref, state?: Partial<StateObjectCell.State>) {
-        // TODO
+    updateCellState(ref: Transform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
+        const cell = this.cells.get(ref)!;
+        const state = typeof stateOrProvider === 'function'
+            ? stateOrProvider(cell.transform.cellState)
+            : stateOrProvider;
+
+        cell.transform = this._tree.setCellState(ref, state);
+        this.events.object.cellState.next({ state: this, ref, cell });
     }
 
     dispose() {
@@ -96,7 +103,7 @@ class State {
     }
 
     update(tree: StateTree | StateTreeBuilder): Task<void> {
-        const _tree = StateTreeBuilder.is(tree) ? tree.getTree() : tree;
+        const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient();
         return Task.create('Update Tree', async taskCtx => {
             let updated = false;
             try {
@@ -137,7 +144,9 @@ class State {
             sourceRef: void 0,
             obj: rootObject,
             status: 'ok',
-            version: root.version
+            visibility: 'visible',
+            version: root.version,
+            errorText: void 0
         });
 
         this.globalContext = params && params.globalContext;
@@ -284,13 +293,19 @@ function initCellStatus(ctx: UpdateContext, roots: Ref[]) {
 }
 
 function initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) {
-    if (ctx.cells.has(transform.ref)) return;
+    if (ctx.cells.has(transform.ref)) {
+        if (transform.cellState && transform.cellState.isHidden) {
+            ctx.cells.get(transform.ref)!.visibility = 'hidden';
+        }
+        return;
+    }
 
     const obj: StateObjectCell = {
         transform,
         sourceRef: void 0,
         status: 'pending',
-        version: UUID.create(),
+        visibility: transform.cellState && transform.cellState.isHidden ? 'hidden' : 'visible',
+        version: UUID.create22(),
         errorText: void 0
     };
     ctx.cells.set(transform.ref, obj);
@@ -323,7 +338,7 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref {
         if (deletes.has(s.value)) continue;
 
         const t = tree.nodes.get(s.value);
-        if (t.cellState && t.cellState.isTransformHidden) continue;
+        if (t.props && t.props.isGhost) continue;
         if (s.value === ref) {
             seenRef = true;
             if (!deletes.has(ref)) prevCandidate = ref;
@@ -383,7 +398,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
             ctx.parent.events.object.created.next({ state: ctx.parent, ref: root, obj: update.obj! });
             if (!ctx.hadError) {
                 const transform = ctx.tree.nodes.get(root);
-                if (!transform.cellState || !transform.cellState.isTransformHidden) ctx.newCurrent = root;
+                if (!transform.props || !transform.props.isGhost) ctx.newCurrent = root;
             }
         } else if (update.action === 'updated') {
             ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, action: 'in-place', obj: update.obj });

+ 27 - 13
src/mol-state/transform.ts

@@ -12,10 +12,10 @@ export interface Transform<A extends StateObject = StateObject, B extends StateO
     readonly parent: Transform.Ref,
     readonly transformer: Transformer<A, B, P>,
     readonly params: P,
+    readonly props: Transform.Props,
     readonly ref: Transform.Ref,
     readonly version: string,
-    readonly cellState?: Partial<StateObjectCell.State>,
-    readonly tag?: string
+    readonly cellState: StateObjectCell.State
 }
 
 export namespace Transform {
@@ -23,23 +23,37 @@ export namespace Transform {
 
     export const RootRef = '-=root=-' as Ref;
 
-    export interface Options { ref?: Ref, tag?: string, cellState?: Partial<StateObjectCell.State> }
+    export interface Props {
+        tag?: string
+        isGhost?: boolean,
+        isBinding?: boolean
+    }
+
+    export interface Options {
+        ref?: string,
+        props?: Props,
+        cellState?: Partial<StateObjectCell.State>
+    }
 
     export function create<A extends StateObject, B extends StateObject, P>(parent: Ref, transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> {
-        const ref = options && options.ref ? options.ref : UUID.create() as string as Ref;
+        const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref;
         return {
             parent,
             transformer,
             params: params || {} as any,
+            props: (options && options.props) || { },
             ref,
-            version: UUID.create(),
-            cellState: options && options.cellState,
-            tag: options && options.tag
+            version: UUID.create22(),
+            cellState: { ...StateObjectCell.DefaultState, ...(options && options.cellState) }
         }
     }
 
-    export function updateParams<T>(t: Transform, params: any): Transform {
-        return { ...t, params, version: UUID.create() };
+    export function withParams<T>(t: Transform, params: any): Transform {
+        return { ...t, params, version: UUID.create22() };
+    }
+
+    export function withCellState<T>(t: Transform, state: Partial<StateObjectCell.State>): Transform {
+        return { ...t, cellState: { ...t.cellState, ...state } };
     }
 
     export function createRoot(): Transform {
@@ -50,10 +64,10 @@ export namespace Transform {
         parent: string,
         transformer: string,
         params: any,
+        props: Props,
         ref: string,
         version: string,
-        cellState?: Partial<StateObjectCell.State>,
-        tag?: string
+        cellState: StateObjectCell.State,
     }
 
     function _id(x: any) { return x; }
@@ -65,10 +79,10 @@ export namespace Transform {
             parent: t.parent,
             transformer: t.transformer.id,
             params: pToJson(t.params),
+            props: t.props,
             ref: t.ref,
             version: t.version,
             cellState: t.cellState,
-            tag: t.tag
         };
     }
 
@@ -81,10 +95,10 @@ export namespace Transform {
             parent: t.parent as Ref,
             transformer,
             params: pFromJson(t.params),
+            props: t.props,
             ref: t.ref as Ref,
             version: t.version,
             cellState: t.cellState,
-            tag: t.tag
         };
     }
 }

+ 7 - 13
src/mol-state/tree/builder.ts

@@ -9,7 +9,6 @@ import { TransientTree } from './transient';
 import { StateObject } from '../object';
 import { Transform } from '../transform';
 import { Transformer } from '../transformer';
-import { shallowEqual } from 'mol-util';
 
 export { StateTreeBuilder }
 
@@ -63,30 +62,25 @@ namespace StateTreeBuilder {
         update<T extends Transformer<A, any, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root
         update(params: any): Root
         update<T extends Transformer<A, any, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) {
-            const old = this.state.tree.nodes.get(this.ref)!;
             let params: any;
             if (provider) {
+                const old = this.state.tree.nodes.get(this.ref)!;
                 params = provider(old.params as any);
             } else {
                 params = paramsOrTransformer;
             }
 
-            if (old.transformer.definition.params && old.transformer.definition.params.areEqual) {
-                if (old.transformer.definition.params.areEqual(old.params, params)) return this.root;
-            } else {
-                if (shallowEqual(old.params, params)) {
-                    return this.root;
-                }
+            if (this.state.tree.setParams(this.ref, params)) {
+                this.editInfo.count++;
+                this.editInfo.lastUpdate = this.ref;
             }
 
-            this.editInfo.count++;
-            this.editInfo.lastUpdate = this.ref;
-
-            this.state.tree.set(Transform.updateParams(old, params));
             return this.root;
         }
 
-        and() { return this.root; }
+        to<A extends StateObject>(ref: Transform.Ref) { return this.root.to<A>(ref); }
+        toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
+        delete(ref: Transform.Ref) { return this.root.delete(ref); }
 
         getTree(): StateTree { return this.state.tree.asImmutable(); }
 

+ 10 - 4
src/mol-state/tree/immutable.ts

@@ -34,9 +34,15 @@ namespace StateTree {
         readonly map: OrderedSet<Ref>['map']
     }
 
+    interface _Map<T> {
+        readonly size: number,
+        has(ref: Ref): boolean,
+        get(ref: Ref): T
+    }
+
     export type Node = Transform
-    export type Nodes = ImmutableMap<Ref, Transform>
-    export type Children = ImmutableMap<Ref, ChildSet>
+    export type Nodes = _Map<Transform>
+    export type Children = _Map<ChildSet>
 
     class Impl implements StateTree {
         get root() { return this.nodes.get(Transform.RootRef)! }
@@ -67,7 +73,7 @@ namespace StateTree {
 
     type VisitorCtx = { tree: StateTree, state: any, f: (node: Node, tree: StateTree, state: any) => boolean | undefined | void };
 
-    function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.nodes.get(c!)); }
+    function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.nodes.get(c!)!); }
     function _doPostOrder(ctx: VisitorCtx, root: Node) {
         const children = ctx.tree.children.get(root.ref);
         if (children && children.size) {
@@ -85,7 +91,7 @@ namespace StateTree {
         return ctx.state;
     }
 
-    function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.nodes.get(c!)); }
+    function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.nodes.get(c!)!); }
     function _doPreOrder(ctx: VisitorCtx, root: Node) {
         const ret = ctx.f(root, ctx.tree, ctx.state);
         if (typeof ret === 'boolean' && !ret) return;

+ 70 - 20
src/mol-state/tree/transient.ts

@@ -4,25 +4,29 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { OrderedSet } from 'immutable';
+import { Map as ImmutableMap, OrderedSet } from 'immutable';
 import { Transform } from '../transform';
 import { StateTree } from './immutable';
 import { StateTreeBuilder } from './builder';
+import { StateObjectCell } from 'mol-state/object';
+import { shallowEqual } from 'mol-util/object';
+import { UUID } from 'mol-util';
 
 export { TransientTree }
 
 class TransientTree implements StateTree {
-    nodes = this.tree.nodes;
-    children = this.tree.children;
+    nodes = this.tree.nodes as ImmutableMap<Transform.Ref, Transform>;
+    children = this.tree.children as ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>;
 
     private changedNodes = false;
     private changedChildren = false;
-    private _mutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> = void 0 as any;
+    private _childMutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> | undefined = void 0;
+    private _transformMutations: Map<Transform.Ref, Transform> | undefined = void 0;
 
-    private get mutations() {
-        if (this._mutations) return this._mutations;
-        this._mutations = new Map();
-        return this._mutations;
+    private get childMutations() {
+        if (this._childMutations) return this._childMutations;
+        this._childMutations = new Map();
+        return this._childMutations;
     }
 
     get root() { return this.nodes.get(Transform.RootRef)! }
@@ -41,13 +45,13 @@ class TransientTree implements StateTree {
             this.children = this.children.asMutable();
         }
 
-        if (this.mutations.has(parent)) {
-            this.mutations.get(parent)!.add(child);
+        if (this.childMutations.has(parent)) {
+            this.childMutations.get(parent)!.add(child);
         } else {
             const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
             set.add(child);
             this.children.set(parent, set);
-            this.mutations.set(parent, set);
+            this.childMutations.set(parent, set);
         }
     }
 
@@ -57,13 +61,13 @@ class TransientTree implements StateTree {
             this.children = this.children.asMutable();
         }
 
-        if (this.mutations.has(parent)) {
-            this.mutations.get(parent)!.remove(child);
+        if (this.childMutations.has(parent)) {
+            this.childMutations.get(parent)!.remove(child);
         } else {
             const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
             set.remove(child);
             this.children.set(parent, set);
-            this.mutations.set(parent, set);
+            this.childMutations.set(parent, set);
         }
     }
 
@@ -76,7 +80,7 @@ class TransientTree implements StateTree {
             this.children = this.children.asMutable();
         }
         this.children.set(parent, set);
-        this.mutations.set(parent, set);
+        this.childMutations.set(parent, set);
     }
 
     add(transform: Transform) {
@@ -111,7 +115,48 @@ class TransientTree implements StateTree {
         return this;
     }
 
-    set(transform: Transform) {
+    /** Calls Transform.definition.params.areEqual if available, otherwise uses shallowEqual to check if the params changed */
+    setParams(ref: Transform.Ref, params: unknown) {
+        ensurePresent(this.nodes, ref);
+
+        const transform = this.nodes.get(ref)!;
+        const def = transform.transformer.definition;
+        if (def.params && def.params.areEqual) {
+            if (def.params.areEqual(transform.params, params)) return false;
+        } else {
+            if (shallowEqual(transform.params, params)) {
+                return false;
+            }
+        }
+
+        if (this._transformMutations && this._transformMutations.has(transform.ref)) {
+            const mutated = this._transformMutations.get(transform.ref)!;
+            (mutated.params as any) = params;
+            (mutated.version as UUID) = UUID.create22();
+        } else {
+            this.set(Transform.withParams(transform, params));
+        }
+
+        return true;
+    }
+
+    setCellState(ref: Transform.Ref, state: Partial<StateObjectCell.State>) {
+        ensurePresent(this.nodes, ref);
+
+        if (this._transformMutations && this._transformMutations.has(ref)) {
+            const transform = this._transformMutations.get(ref)!;
+            const old = transform.cellState;
+            (transform.cellState as StateObjectCell.State) = { ...old, ...state };
+            return transform;
+        } else {
+            const transform = this.nodes.get(ref);
+            const newT = Transform.withCellState(transform, state);
+            this.set(newT);
+            return newT;
+        }
+    }
+
+    private set(transform: Transform) {
         ensurePresent(this.nodes, transform.ref);
 
         if (!this.changedNodes) {
@@ -119,6 +164,11 @@ class TransientTree implements StateTree {
             this.nodes = this.nodes.asMutable();
         }
 
+        if (!this._transformMutations) {
+            this._transformMutations = new Map();
+        }
+        this._transformMutations.set(transform.ref, transform);
+
         this.nodes.set(transform.ref, transform);
         return this;
     }
@@ -145,15 +195,15 @@ class TransientTree implements StateTree {
         for (const n of st) {
             this.nodes.delete(n.ref);
             this.children.delete(n.ref);
-            if (this._mutations) this._mutations.delete(n.ref);
+            if (this._childMutations) this._childMutations.delete(n.ref);
         }
 
         return st;
     }
 
     asImmutable() {
-        if (!this.changedNodes && !this.changedChildren && !this._mutations) return this.tree;
-        if (this._mutations) this._mutations.forEach(fixChildMutations, this.children);
+        if (!this.changedNodes && !this.changedChildren && !this._childMutations) return this.tree;
+        if (this._childMutations) this._childMutations.forEach(fixChildMutations, this.children);
         return StateTree.create(
             this.changedNodes ? this.nodes.asImmutable() : this.nodes,
             this.changedChildren ? this.children.asImmutable() : this.children);
@@ -164,7 +214,7 @@ class TransientTree implements StateTree {
     }
 }
 
-function fixChildMutations(this: StateTree.Children, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); }
+function fixChildMutations(this: ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); }
 
 function alreadyPresent(ref: Transform.Ref) {
     throw new Error(`Transform '${ref}' is already present in the tree.`);

+ 12 - 7
src/mol-util/uuid.ts

@@ -10,19 +10,24 @@ type UUID = string & { '@type': 'uuid' }
 
 namespace UUID {
     const chars: string[] = [];
-    export function create(): UUID {
+    /** Creates 22 characted "base64" UUID */
+    export function create22(): UUID {
         let d = (+new Date()) + now();
         for (let i = 0; i < 16; i++) {
             chars[i] = String.fromCharCode((d + Math.random()*0xff)%0xff | 0);
             d = Math.floor(d/0xff);
         }
         return btoa(chars.join('')).replace(/\+/g, '-').replace(/\//g, '_').substr(0, 22) as UUID;
-        // const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
-        //     const r = (d + Math.random()*16)%16 | 0;
-        //     d = Math.floor(d/16);
-        //     return (c==='x' ? r : (r&0x3|0x8)).toString(16);
-        // });
-        // return uuid as any;
+    }
+
+    export function createv4(): UUID {
+        let d = (+new Date()) + now();
+        const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+            const r = (d + Math.random()*16)%16 | 0;
+            d = Math.floor(d/16);
+            return (c==='x' ? r : (r&0x3|0x8)).toString(16);
+        });
+        return uuid as any;
     }
 }
 

+ 1 - 1
src/servers/model/properties/providers/pdbe.ts

@@ -83,7 +83,7 @@ function getParam<T>(params: any, ...path: string[]): T | undefined {
 
 
 function apiQueryProvider(urlPrefix: string, cache: any) {
-    const cacheKey = UUID.create();
+    const cacheKey = UUID.create22();
     return async (model: Model) => {
         try {
             if (cache[cacheKey]) return cache[cacheKey];

+ 1 - 1
src/servers/model/server/jobs.ts

@@ -43,7 +43,7 @@ export function createJob<Name extends QueryName>(definition: JobDefinition<Name
     const normalizedParams = normalizeQueryParams(queryDefinition, definition.queryParams);
     const sourceId = definition.sourceId || '_local_';
     return {
-        id: UUID.create(),
+        id: UUID.create22(),
         datetime_utc: `${new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')}`,
         key: `${sourceId}/${definition.entryId}`,
         sourceId,

+ 1 - 1
src/servers/volume/server/query/execute.ts

@@ -26,7 +26,7 @@ export default async function execute(params: Data.QueryParams, outputProvider:
     const start = getTime();
     State.pendingQueries++;
 
-    const guid = UUID.create() as any as string;
+    const guid = UUID.create22() as any as string;
     params.detail = Math.min(Math.max(0, params.detail | 0), ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1);
     ConsoleLogger.logId(guid, 'Info', `id=${params.sourceId},encoding=${params.asBinary ? 'binary' : 'text'},detail=${params.detail},${queryBoxToString(params.box)}`);