From 5cf0b1c08c5a79b7efc9ea39201852d95c9e8773 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Tue, 26 Mar 2019 12:36:29 +0100
Subject: [PATCH] wip mol-state refactoring

---
 src/mol-plugin/state.ts          |  2 +-
 src/mol-state/manager.ts         |  7 ---
 src/mol-state/object.ts          | 21 +--------
 src/mol-state/state.ts           | 71 +++++++++++-----------------
 src/mol-state/state/builder.ts   | 22 ++++-----
 src/mol-state/state/selection.ts | 10 ++--
 src/mol-state/transform.ts       | 80 +++++++++++++++++++++++++-------
 src/mol-state/tree/immutable.ts  | 30 +++++-------
 src/mol-state/tree/transient.ts  | 50 ++++----------------
 9 files changed, 132 insertions(+), 161 deletions(-)
 delete mode 100644 src/mol-state/manager.ts

diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts
index 0e2a82007..25f6c1035 100644
--- a/src/mol-plugin/state.ts
+++ b/src/mol-plugin/state.ts
@@ -99,7 +99,7 @@ class PluginState {
     constructor(private plugin: import('./context').PluginContext) {
         this.snapshots = new PluginStateSnapshotManager(plugin);
         this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
-        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } });
+        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootState: { isLocked: true } });
 
         this.dataState.behaviors.currentObject.subscribe(o => {
             if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o);
diff --git a/src/mol-state/manager.ts b/src/mol-state/manager.ts
deleted file mode 100644
index 0042b15a9..000000000
--- a/src/mol-state/manager.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-// TODO manage snapshots etc
\ No newline at end of file
diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts
index b74efcf5e..c2f86b646 100644
--- a/src/mol-state/object.ts
+++ b/src/mol-state/object.ts
@@ -19,7 +19,7 @@ interface StateObject<D = any, T extends StateObject.Type = StateObject.Type<any
     readonly label: string,
     readonly description?: string,
     // assigned by reconciler to be StateTransform.props.tag
-    readonly tag?: string
+    readonly tags?: string[]
 }
 
 namespace StateObject {
@@ -62,6 +62,7 @@ interface StateObjectCell<T extends StateObject = StateObject, F extends StateTr
     sourceRef: StateTransform.Ref | undefined,
 
     status: StateObjectCell.Status,
+    state: StateTransform.State,
 
     params: {
         definition: ParamDefinition.Params,
@@ -79,24 +80,6 @@ namespace StateObjectCell {
 
     export type Obj<C extends StateObjectCell> = C extends StateObjectCell<infer T> ? T : never
     export type Transform<C extends StateObjectCell> = C extends StateObjectCell<any, infer T> ? T : never
-
-    export interface State {
-        isHidden: boolean,
-        isCollapsed: boolean
-    }
-
-    export const DefaultState: State = { isHidden: false, isCollapsed: false };
-
-    export function areStatesEqual(a: State, b: State) {
-        return a.isHidden !== b.isHidden || a.isCollapsed !== b.isCollapsed;
-    }
-
-    export function isStateChange(a: State, b?: Partial<State>) {
-        if (!b) return false;
-        if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true;
-        if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true;
-        return false;
-    }
 }
 
 // TODO: improve the API?
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index 66563660a..a05d24847 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -33,7 +33,7 @@ class State {
     readonly globalContext: unknown = void 0;
     readonly events = {
         cell: {
-            stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
+            stateUpdated: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
         },
@@ -55,7 +55,6 @@ class State {
 
     get tree(): StateTree { return this._tree; }
     get transforms() { return (this._tree as StateTree).transforms; }
-    get cellStates() { return (this._tree as StateTree).cellStates; }
     get current() { return this.behaviors.currentObject.value.ref; }
 
     build() { return new StateBuilder.Root(this.tree, this); }
@@ -64,6 +63,7 @@ class State {
     private spine = new StateTreeSpine.Impl(this.cells);
 
     getSnapshot(): State.Snapshot {
+        this.cells.forEach(c => this._tree.updateState(c.transform.ref, c.state));
         return { tree: StateTree.toJSON(this._tree) };
     }
 
@@ -76,13 +76,15 @@ class State {
         this.behaviors.currentObject.next({ state: this, ref });
     }
 
-    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
-        const update = typeof stateOrProvider === 'function'
-            ? stateOrProvider(this.tree.cellStates.get(ref))
-            : stateOrProvider;
+    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateTransform.State) => Partial<StateTransform.State>) | Partial<StateTransform.State>) {
+        const cell = this.cells.get(ref);
+        if (!cell) return;
 
-        if (this._tree.updateCellState(ref, update)) {
-            this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
+        const update = typeof stateOrProvider === 'function' ? stateOrProvider(cell.state) : stateOrProvider;
+
+        if (StateTransform.assignState(cell.state, update)) {
+            // this._tree.updateCellState(ref, update)) {
+            this.events.cell.stateUpdated.next({ state: this, ref, cell });
         }
     }
 
@@ -165,10 +167,6 @@ class State {
 
             if (updated) this.events.changed.next();
             this.events.isUpdating.next(false);
-
-            for (const ref of ctx.stateChanges) {
-                this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
-            }
         }
     }
 
@@ -189,7 +187,6 @@ class State {
             spine: this.spine,
 
             results: [],
-            stateChanges: [],
 
             options: { ...StateUpdateDefaultOptions, ...options },
 
@@ -203,8 +200,8 @@ class State {
         return ctx;
     }
 
-    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
-        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootProps)).asTransient();
+    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
+        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootState)).asTransient();
         const tree = this._tree;
         const root = tree.root;
 
@@ -213,6 +210,7 @@ class State {
             sourceRef: void 0,
             obj: rootObject,
             status: 'ok',
+            state: { ...root.state },
             errorText: void 0,
             params: {
                 definition: {},
@@ -245,7 +243,7 @@ namespace State {
         doNotUpdateCurrent: boolean
     }
 
-    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
+    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
         return new State(rootObject, params);
     }
 }
@@ -271,7 +269,6 @@ interface UpdateContext {
     spine: StateTreeSpine.Impl,
 
     results: UpdateNodeResult[],
-    stateChanges: StateTransform.Ref[],
 
     // suppress timing messages
     options: State.UpdateOptions,
@@ -319,12 +316,6 @@ async function update(ctx: UpdateContext) {
         roots = findUpdateRoots(ctx.cells, ctx.tree);
     }
 
-    let newCellStates: StateTree.CellStates;
-    if (!ctx.editInfo) {
-        newCellStates = ctx.tree.cellStatesSnapshot();
-        syncOldStates(ctx);
-    }
-
     // Init empty cells where not present
     // this is done in "pre order", meaning that "parents" will be created 1st.
     const addedCells = initCells(ctx, roots);
@@ -353,7 +344,7 @@ async function update(ctx: UpdateContext) {
 
     // Sync cell states
     if (!ctx.editInfo) {
-        syncNewStates(ctx, newCellStates!);
+        syncNewStates(ctx);
     }
 
     let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent;
@@ -363,7 +354,7 @@ async function update(ctx: UpdateContext) {
             ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! });
             if (!ctx.newCurrent) {
                 const transform = ctx.tree.transforms.get(update.ref);
-                if (!(transform.props && transform.props.isGhost) && update.obj !== StateObject.Null) newCurrent = update.ref;
+                if (!transform.state.isGhost && update.obj !== StateObject.Null) newCurrent = update.ref;
             }
         } else if (update.action === 'updated') {
             ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj });
@@ -415,25 +406,14 @@ function findDeletes(ctx: UpdateContext): Ref[] {
     return deleteCtx.deletes;
 }
 
-function syncOldStatesVisitor(n: StateTransform, tree: StateTree, oldState: StateTree.CellStates) {
-    if (oldState.has(n.ref)) {
-        (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
-    }
-}
-function syncOldStates(ctx: UpdateContext) {
-    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncOldStatesVisitor);
+function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: UpdateContext) {
+    const cell = ctx.cells.get(n.ref);
+    if (!cell || !StateTransform.assignState(cell.state, n.state)) return;
+    ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref: n.ref, cell });
 }
 
-function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: { newState: StateTree.CellStates, changes: StateTransform.Ref[] }) {
-    if (ctx.newState.has(n.ref)) {
-        const changed = (tree as TransientTree).updateCellState(n.ref, ctx.newState.get(n.ref));
-        if (changed) {
-            ctx.changes.push(n.ref);
-        }
-    }
-}
-function syncNewStates(ctx: UpdateContext, newState: StateTree.CellStates) {
-    StateTree.doPreOrder(ctx.tree, ctx.tree.root, { newState, changes: ctx.stateChanges }, syncNewStatesVisitor);
+function syncNewStates(ctx: UpdateContext) {
+    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx, syncNewStatesVisitor);
 }
 
 function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
@@ -441,7 +421,7 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta
     const changed = cell.status !== status;
     cell.status = status;
     cell.errorText = errorText;
-    if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) });
+    if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cell });
 }
 
 function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) {
@@ -465,6 +445,7 @@ function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: Ini
         transform,
         sourceRef: void 0,
         status: 'pending',
+        state: { ...transform.state },
         errorText: void 0,
         params: void 0,
         cache: void 0
@@ -505,7 +486,7 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Ma
         }
 
         const t = tree.transforms.get(s.value);
-        if (t.props && t.props.isGhost) continue;
+        if (t.state.isGhost) continue;
         if (s.value === ref) {
             seenRef = true;
             if (!deletes.has(ref)) prevCandidate = ref;
@@ -671,7 +652,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
 
 function updateTag(obj: StateObject | undefined, transform: StateTransform) {
     if (!obj || obj === StateObject.Null) return;
-    (obj.tag as string | undefined) = transform.props.tag;
+    (obj.tags as string[] | undefined) = transform.tags;
 }
 
 function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
diff --git a/src/mol-state/state/builder.ts b/src/mol-state/state/builder.ts
index 6e9e48a10..1b2cf5492 100644
--- a/src/mol-state/state/builder.ts
+++ b/src/mol-state/state/builder.ts
@@ -36,7 +36,7 @@ namespace StateBuilder {
         | { kind: 'add', transform: StateTransform }
         | { kind: 'update', ref: string, params: any }
         | { kind: 'delete', ref: string }
-        | { kind: 'insert', ref: string, transform: StateTransform, initialCellState?: Partial<StateObjectCell.State> }
+        | { kind: 'insert', ref: string, transform: StateTransform }
 
     function buildTree(state: BuildState) {
         if (!state.state || state.state.tree === state.editInfo.sourceTree) {
@@ -52,7 +52,7 @@ namespace StateBuilder {
                 case 'delete': tree.remove(a.ref); break;
                 case 'insert': {
                     const children = tree.children.get(a.ref).toArray();
-                    tree.add(a.transform, a.initialCellState);
+                    tree.add(a.transform);
                     for (const c of children) {
                         tree.changeParent(c, a.transform.ref);
                     }
@@ -89,7 +89,7 @@ namespace StateBuilder {
             this.state.actions.push({ kind: 'delete', ref });
             return this;
         }
-        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+        getTree(): StateTree { return buildTree(this.state); }
         constructor(tree: StateTree, state?: State) { this.state = { state, tree: tree.asTransient(), actions: [], editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
     }
 
@@ -102,9 +102,9 @@ 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>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
             const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
+            this.state.tree.add(t);
             this.editInfo.count++;
             this.editInfo.lastUpdate = t.ref;
 
@@ -116,20 +116,20 @@ namespace StateBuilder {
         /**
          * A helper to greate a group-like state object and keep the current type.
          */
-        group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<A> {
-            return this.apply(tr, params, options, initialCellState) as To<A>;
+        group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<A> {
+            return this.apply(tr, params, options) as To<A>;
         }
 
         /**
          * 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>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
             // cache the children
             const children = this.state.tree.children.get(this.ref).toArray();
 
             // add the new node
             const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
+            this.state.tree.add(t);
 
             // move the original children to the new node
             for (const c of children) {
@@ -139,7 +139,7 @@ namespace StateBuilder {
             this.editInfo.count++;
             this.editInfo.lastUpdate = t.ref;
 
-            this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t, initialCellState });
+            this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t });
 
             return new To(this.state, t.ref, this.root);
         }
@@ -193,7 +193,7 @@ namespace StateBuilder {
         toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
         delete(ref: StateTransform.Ref) { return this.root.delete(ref); }
 
-        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+        getTree(): StateTree { return buildTree(this.state); }
 
         constructor(private state: BuildState, ref: StateTransform.Ref, private root: Root) {
             this.ref = ref;
diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts
index 12a319073..dd8a638d5 100644
--- a/src/mol-state/state/selection.ts
+++ b/src/mol-state/state/selection.ts
@@ -268,8 +268,12 @@ namespace StateSelection {
     }
 
     function _findUniqueTagsInSubtree(n: StateTransform, _: any, s: { refs: { [name: string]: StateTransform.Ref }, tags: Set<string> }) {
-        if (n.props.tag && s.tags.has(n.props.tag)) {
-            s.refs[n.props.tag] = n.ref;
+        if (n.tags) {
+            for (const t of n.tags) {
+                if (!s.tags.has(t)) continue;
+                s.refs[t] = n.ref;
+                break;
+            }
         }
         return true;
     }
@@ -279,7 +283,7 @@ namespace StateSelection {
     }
 
     function _findTagInSubtree(n: StateTransform, _: any, s: { ref: string | undefined, tag: string }) {
-        if (n.props.tag === s.tag) {
+        if (n.tags && n.tags.indexOf(s.tag) >= 0) {
             s.ref = n.ref;
             return false;
         }
diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts
index 9703a2f53..80ccc317e 100644
--- a/src/mol-state/transform.ts
+++ b/src/mol-state/transform.ts
@@ -12,7 +12,8 @@ export { Transform as StateTransform }
 interface Transform<T extends StateTransformer = StateTransformer> {
     readonly parent: Transform.Ref,
     readonly transformer: T,
-    readonly props: Transform.Props,
+    readonly state: Transform.State,
+    readonly tags?: string[],
     readonly ref: Transform.Ref,
     readonly params?: StateTransformer.Params<T>,
     readonly version: string
@@ -24,24 +25,60 @@ namespace Transform {
 
     export const RootRef = '-=root=-' as Ref;
 
-    export interface Props {
-        tag?: string
+    export interface State {
+        // is the cell shown in the UI
         isGhost?: boolean,
-        // determine if the corresponding cell can be deleted by the user.
-        isLocked?: boolean
+        // can the corresponding be deleted by the user.
+        isLocked?: boolean,
+        // is the representation associated with the cell hidden
+        isHidden?: boolean,
+        // is the tree node collapsed?
+        isCollapsed?: boolean
+    }
+
+    export function areStatesEqual(a: State, b: State) {
+        return !!a.isHidden !== !!b.isHidden || !!a.isCollapsed !== !!b.isCollapsed
+            || !!a.isGhost !== !!b.isGhost || !!a.isLocked !== !!b.isLocked;
+    }
+
+    export function isStateChange(a: State, b?: Partial<State>) {
+        if (!b) return false;
+        if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true;
+        if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true;
+        if (typeof b.isGhost !== 'undefined' && a.isGhost !== b.isGhost) return true;
+        if (typeof b.isLocked !== 'undefined' && a.isLocked !== b.isLocked) return true;
+        return false;
+    }
+
+    export function assignState(a: State, b?: Partial<State>): boolean {
+        if (!b) return false;
+        let changed = false;
+        for (const k of Object.keys(b)) {
+            const s = (b as any)[k], t = (a as any)[k];
+            if (!!s === !!t) continue;
+            changed = true;
+            (a as any)[k] = s;
+        }
+        return changed;
     }
 
     export interface Options {
         ref?: string,
-        props?: Props
+        tags?: string | string[],
+        state?: State
     }
 
     export function create<T extends StateTransformer>(parent: Ref, transformer: T, params?: StateTransformer.Params<T>, options?: Options): Transform<T> {
         const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref;
+        let tags: string[] | undefined = void 0;
+        if (options && options.tags) {
+            tags = typeof options.tags === 'string' ? [options.tags] : options.tags;
+        }
         return {
             parent,
             transformer,
-            props: (options && options.props) || { },
+            state: (options && options.state) || { },
+            tags,
             ref,
             params,
             version: UUID.create22()
@@ -52,23 +89,25 @@ namespace Transform {
         return { ...t, params, version: UUID.create22() };
     }
 
-    export function withParent(t: Transform, parent: Ref): Transform {
-        return { ...t, parent, version: UUID.create22() };
+    export function withState(t: Transform, state?: Partial<State>): Transform {
+        if (!state) return t;
+        return { ...t, state: { ...t.state, ...state } };
     }
 
-    export function withNewVersion(t: Transform): Transform {
-        return { ...t, version: UUID.create22() };
+    export function withParent(t: Transform, parent: Ref): Transform {
+        return { ...t, parent, version: UUID.create22() };
     }
 
-    export function createRoot(props?: Props): Transform {
-        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, props });
+    export function createRoot(state?: State): Transform {
+        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, state });
     }
 
     export interface Serialized {
         parent: string,
         transformer: string,
         params: any,
-        props: Props,
+        state?: State,
+        tags?: string[],
         ref: string,
         version: string
     }
@@ -78,11 +117,19 @@ namespace Transform {
         const pToJson = t.transformer.definition.customSerialization
             ? t.transformer.definition.customSerialization.toJSON
             : _id;
+        let state: any = void 0;
+        for (const k of Object.keys(t.state)) {
+            const s = (t.state as any)[k];
+            if (!s) continue;
+            if (!state) state = { };
+            state[k] = true;
+        }
         return {
             parent: t.parent,
             transformer: t.transformer.id,
             params: t.params ? pToJson(t.params) : void 0,
-            props: t.props,
+            state,
+            tags: t.tags,
             ref: t.ref,
             version: t.version
         };
@@ -97,7 +144,8 @@ namespace Transform {
             parent: t.parent as Ref,
             transformer,
             params: t.params ? pFromJson(t.params) : void 0,
-            props: t.props,
+            state: t.state || { },
+            tags: t.tags,
             ref: t.ref as Ref,
             version: t.version
         };
diff --git a/src/mol-state/tree/immutable.ts b/src/mol-state/tree/immutable.ts
index ab3d8f603..fba1811e7 100644
--- a/src/mol-state/tree/immutable.ts
+++ b/src/mol-state/tree/immutable.ts
@@ -7,7 +7,6 @@
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
 import { StateTransform } from '../transform';
 import { TransientTree } from './transient';
-import { StateObjectCell } from 'mol-state/object';
 
 export { StateTree }
 
@@ -19,7 +18,6 @@ interface StateTree {
     readonly root: StateTransform,
     readonly transforms: StateTree.Transforms,
     readonly children: StateTree.Children,
-    readonly cellStates: StateTree.CellStates,
 
     asTransient(): TransientTree
 }
@@ -43,7 +41,6 @@ namespace StateTree {
 
     export interface Transforms extends _Map<StateTransform> {}
     export interface Children extends _Map<ChildSet> { }
-    export interface CellStates extends _Map<StateObjectCell.State> { }
 
     class Impl implements StateTree {
         get root() { return this.transforms.get(StateTransform.RootRef)! }
@@ -52,7 +49,7 @@ namespace StateTree {
             return new TransientTree(this);
         }
 
-        constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) {
+        constructor(public transforms: StateTree.Transforms, public children: Children) {
         }
     }
 
@@ -61,11 +58,11 @@ namespace StateTree {
      */
     export function createEmpty(customRoot?: StateTransform): StateTree {
         const root = customRoot || StateTransform.createRoot();
-        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap([[root.ref, StateObjectCell.DefaultState]]));
+        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]));
     }
 
-    export function create(nodes: Transforms, children: Children, cellStates: CellStates): StateTree {
-        return new Impl(nodes, children, cellStates);
+    export function create(nodes: Transforms, children: Children): StateTree {
+        return new Impl(nodes, children);
     }
 
     type VisitorCtx = { tree: StateTree, state: any, f: (node: StateTransform, tree: StateTree, state: any) => boolean | undefined | void };
@@ -116,19 +113,19 @@ namespace StateTree {
         return doPostOrder<StateTransform[]>(tree, root, [], _subtree);
     }
 
-    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: [StateTransform.Serialized, StateObjectCell.State][]) {
+    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: StateTransform.Serialized[]) {
         // const children: Ref[] = [];
         // tree.children.get(node.ref).forEach(_visitChildToJson as any, children);
-        ctx.push([StateTransform.toJSON(node), tree.cellStates.get(node.ref)]);
+        ctx.push(StateTransform.toJSON(node));
     }
 
     export interface Serialized {
         /** Transforms serialized in pre-order */
-        transforms: [StateTransform.Serialized, StateObjectCell.State][]
+        transforms: StateTransform.Serialized[]
     }
 
     export function toJSON(tree: StateTree): Serialized {
-        const transforms: [StateTransform.Serialized, StateObjectCell.State][] = [];
+        const transforms: StateTransform.Serialized[] = [];
         doPreOrder(tree, tree.root, transforms, _visitNodeToJson);
         return { transforms };
     }
@@ -136,12 +133,10 @@ namespace StateTree {
     export function fromJSON(data: Serialized): StateTree {
         const nodes = ImmutableMap<Ref, StateTransform>().asMutable();
         const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable();
-        const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable();
 
         for (const t of data.transforms) {
-            const transform = StateTransform.fromJSON(t[0]);
+            const transform = StateTransform.fromJSON(t);
             nodes.set(transform.ref, transform);
-            cellStates.set(transform.ref, t[1]);
 
             if (!children.has(transform.ref)) {
                 children.set(transform.ref, OrderedSet<Ref>().asMutable());
@@ -151,19 +146,18 @@ namespace StateTree {
         }
 
         for (const t of data.transforms) {
-            const ref = t[0].ref;
+            const ref = t.ref;
             children.set(ref, children.get(ref).asImmutable());
         }
 
-        return create(nodes.asImmutable(), children.asImmutable(), cellStates.asImmutable());
+        return create(nodes.asImmutable(), children.asImmutable());
     }
 
     export function dump(tree: StateTree) {
         console.log({
             tr: (tree.transforms as ImmutableMap<any, any>).keySeq().toArray(),
             tr1: (tree.transforms as ImmutableMap<any, any>).valueSeq().toArray().map(t => t.ref),
-            ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray(),
-            cs: (tree.cellStates as ImmutableMap<any, any>).keySeq().toArray()
+            ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray()
         });
     }
 }
\ No newline at end of file
diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts
index a48e8e663..068a5264d 100644
--- a/src/mol-state/tree/transient.ts
+++ b/src/mol-state/tree/transient.ts
@@ -7,7 +7,6 @@
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
 import { StateTransform } from '../transform';
 import { StateTree } from './immutable';
-import { StateObjectCell } from 'mol-state/object';
 import { shallowEqual } from 'mol-util/object';
 
 export { TransientTree }
@@ -15,11 +14,9 @@ export { TransientTree }
 class TransientTree implements StateTree {
     transforms = this.tree.transforms as ImmutableMap<StateTransform.Ref, StateTransform>;
     children = this.tree.children as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>;
-    cellStates = this.tree.cellStates as ImmutableMap<StateTransform.Ref, StateObjectCell.State>;
 
     private changedNodes = false;
     private changedChildren = false;
-    private changedStates = false;
 
     private _childMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0;
 
@@ -29,12 +26,6 @@ class TransientTree implements StateTree {
         return this._childMutations;
     }
 
-    private changeStates() {
-        if (this.changedStates) return;
-        this.changedStates = true;
-        this.cellStates = this.cellStates.asMutable();
-    }
-
     private changeNodes() {
         if (this.changedNodes) return;
         this.changedNodes = true;
@@ -49,10 +40,6 @@ class TransientTree implements StateTree {
 
     get root() { return this.transforms.get(StateTransform.RootRef)! }
 
-    cellStatesSnapshot() {
-        return this.cellStates.asImmutable();
-    }
-
     asTransient() {
         return this.asImmutable().asTransient();
     }
@@ -104,15 +91,7 @@ class TransientTree implements StateTree {
         this.transforms.set(ref, StateTransform.withParent(old, newParent));
     }
 
-    updateVersion(ref: StateTransform.Ref) {
-        ensurePresent(this.transforms, ref);
-
-        const t = this.transforms.get(ref);
-        this.changeNodes();
-        this.transforms.set(ref, StateTransform.withNewVersion(t));
-    }
-
-    add(transform: StateTransform, initialState?: Partial<StateObjectCell.State>) {
+    add(transform: StateTransform) {
         const ref = transform.ref;
 
         if (this.transforms.has(transform.ref)) {
@@ -138,15 +117,6 @@ class TransientTree implements StateTree {
         this.changeNodes();
         this.transforms.set(ref, transform);
 
-        if (!this.cellStates.has(ref)) {
-            this.changeStates();
-            if (StateObjectCell.isStateChange(StateObjectCell.DefaultState, initialState)) {
-                this.cellStates.set(ref, { ...StateObjectCell.DefaultState, ...initialState });
-            } else {
-                this.cellStates.set(ref, StateObjectCell.DefaultState);
-            }
-        }
-
         return this;
     }
 
@@ -169,14 +139,15 @@ class TransientTree implements StateTree {
         return true;
     }
 
-    updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) {
+    updateState(ref: StateTransform.Ref, state?: Partial<StateTransform.State>) {
         ensurePresent(this.transforms, ref);
 
-        const old = this.cellStates.get(ref);
-        if (!StateObjectCell.isStateChange(old, state)) return false;
+        const old = this.transforms.get(ref);
+        if (!StateTransform.isStateChange(old.state, state)) return false;
 
-        this.changeStates();
-        this.cellStates.set(ref, { ...old, ...state });
+        this.changeNodes();
+        // TODO: cache these changes?
+        this.transforms.set(ref, StateTransform.withState(old, state));
 
         return true;
     }
@@ -197,12 +168,10 @@ class TransientTree implements StateTree {
 
         this.changeNodes();
         this.changeChildren();
-        this.changeStates();
 
         for (const n of st) {
             this.transforms.delete(n.ref);
             this.children.delete(n.ref);
-            this.cellStates.delete(n.ref);
             if (this._childMutations) this._childMutations.delete(n.ref);
         }
 
@@ -210,12 +179,11 @@ class TransientTree implements StateTree {
     }
 
     asImmutable() {
-        if (!this.changedNodes && !this.changedChildren && !this.changedStates && !this._childMutations) return this.tree;
+        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.transforms.asImmutable() : this.transforms,
-            this.changedChildren ? this.children.asImmutable() : this.children,
-            this.changedStates ? this.cellStates.asImmutable() : this.cellStates);
+            this.changedChildren ? this.children.asImmutable() : this.children);
     }
 
     constructor(private tree: StateTree) {
-- 
GitLab