From 047365921289d59da940eaee9dca34cbdb6954ad Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Fri, 9 Nov 2018 16:10:23 +0100
Subject: [PATCH] mol-state: simplify tree

---
 src/mol-plugin/context.ts                     |   2 +-
 src/mol-plugin/ui/controls.tsx                |   5 +-
 src/mol-plugin/ui/state-tree.tsx              |  10 +-
 src/mol-state/context.ts                      |   4 +-
 src/mol-state/immutable-tree.ts               | 278 ------------------
 src/mol-state/selection.ts                    |  10 +-
 src/mol-state/state.ts                        |  77 +++--
 src/mol-state/tree.ts                         |  32 +-
 .../{immutable-tree.1.ts => tree/base.ts}     |  32 +-
 src/perf-tests/state.ts                       |   8 +-
 10 files changed, 80 insertions(+), 378 deletions(-)
 delete mode 100644 src/mol-state/immutable-tree.ts
 rename src/mol-state/{immutable-tree.1.ts => tree/base.ts} (90%)

diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index 12f4a76b6..ced63f9a5 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -161,7 +161,7 @@ export class PluginContext {
 
     _test_nextModel() {
         const models = StateSelection.select('models', this.state.data)[0].obj as SO.Molecule.Models;
-        const idx = (this.state.data.tree.getValue('structure')!.params as Transformer.Params<typeof StateTransforms.Model.CreateStructureFromModel>).modelIndex;
+        const idx = (this.state.data.tree.nodes.get('structure')!.params as Transformer.Params<typeof StateTransforms.Model.CreateStructureFromModel>).modelIndex;
         const newTree = StateTree.updateParams(this.state.data.tree, 'structure', { modelIndex: (idx + 1) % models.data.length });
         return this.state.updateData(newTree);
         // this.viewer.requestDraw(true);
diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx
index ccf64d214..e9a92645e 100644
--- a/src/mol-plugin/ui/controls.tsx
+++ b/src/mol-plugin/ui/controls.tsx
@@ -92,8 +92,7 @@ export class _test_CreateTransform extends React.Component<{ plugin: PluginConte
 
 export class _test_UpdateTransform extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref }, { params: any }> {
     private getTransform() {
-        const t = this.props.plugin.state.data.tree.nodes.get(this.props.nodeRef)!;
-        return t ? t.value : t;
+        return this.props.plugin.state.data.tree.nodes.get(this.props.nodeRef)!;
     }
 
     private getParamDef() {
@@ -122,7 +121,7 @@ export class _test_UpdateTransform extends React.Component<{ plugin: PluginConte
 
     render() {
         const transform = this.getTransform();
-        if (!transform || transform.ref === this.props.plugin.state.data.tree.rootRef) {
+        if (!transform || transform.ref === Transform.RootRef) {
             return <div />;
         }
 
diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx
index 66967f956..83daeb922 100644
--- a/src/mol-plugin/ui/state-tree.tsx
+++ b/src/mol-plugin/ui/state-tree.tsx
@@ -17,7 +17,7 @@ export class StateTree extends React.Component<{ plugin: PluginContext, state: S
     }
     render() {
         // const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!;
-        const n = this.props.state.tree.rootRef;
+        const n = this.props.state.tree.root.ref;
         return <div>
             <StateTreeNode plugin={this.props.plugin} state={this.props.state} nodeRef={n} key={n} />
             { /* n.children.map(c => <StateTreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />) */}
@@ -37,7 +37,7 @@ export class StateTreeNode extends React.Component<{ plugin: PluginContext, node
 
         let label: any;
         if (cell.status !== 'ok' || !cell.obj) {
-            const name = (n.value.transformer.definition.display && n.value.transformer.definition.display.name) || n.value.transformer.definition.name;
+            const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name;
             label = <><b>{cell.status}</b> <a href='#' onClick={e => {
                 e.preventDefault();
                 PluginCommands.Data.SetCurrentObject.dispatch(this.props.plugin, { ref: this.props.nodeRef });
@@ -52,11 +52,13 @@ export class StateTreeNode extends React.Component<{ plugin: PluginContext, node
             }}>{props.label}</a> {props.description ? <small>{props.description}</small> : void 0}</>;
         }
 
+        const children = this.props.state.tree.children.get(this.props.nodeRef);
+
         return <div>
             {remove}{label}
-            {n.children.size === 0
+            {children.size === 0
                 ? void 0
-                : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999' }}>{n.children.map(c => <StateTreeNode plugin={this.props.plugin} state={this.props.state} nodeRef={c!} key={c} />)}</div>
+                : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999' }}>{children.map(c => <StateTreeNode plugin={this.props.plugin} state={this.props.state} nodeRef={c!} key={c} />)}</div>
             }
         </div>;
     }
diff --git a/src/mol-state/context.ts b/src/mol-state/context.ts
index be61abb32..67641d7be 100644
--- a/src/mol-state/context.ts
+++ b/src/mol-state/context.ts
@@ -40,9 +40,9 @@ class StateContext {
         this.ev.dispose();
     }
 
-    constructor(params: { globalContext: unknown, defaultCellState: unknown, rootRef: Transform.Ref }) {
+    constructor(params: { globalContext: unknown, defaultCellState: unknown }) {
         this.globalContext = params.globalContext;
         this.defaultCellState = params.defaultCellState;
-        this.behaviors.currentObject.next({ ref: params.rootRef });
+        this.behaviors.currentObject.next({ ref: Transform.RootRef });
     }
 }
\ No newline at end of file
diff --git a/src/mol-state/immutable-tree.ts b/src/mol-state/immutable-tree.ts
deleted file mode 100644
index 21c9a1689..000000000
--- a/src/mol-state/immutable-tree.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { Map as ImmutableMap, OrderedSet } from 'immutable';
-
-/**
- * An immutable tree where each node requires a unique reference.
- * Represented as an immutable map.
- */
-export interface ImmutableTree<T> {
-    readonly rootRef: ImmutableTree.Ref,
-    readonly version: number,
-    readonly nodes: ImmutableTree.Nodes<T>,
-    getRef(e: T): ImmutableTree.Ref,
-    getValue(ref: ImmutableTree.Ref): T | undefined
-}
-
-export namespace ImmutableTree {
-    export type Ref = string
-    export interface MutableNode<T> { ref: ImmutableTree.Ref, value: T, version: number, parent: ImmutableTree.Ref, children: OrderedSet<ImmutableTree.Ref> }
-    export interface Node<T> extends Readonly<MutableNode<T>> { }
-    export interface Nodes<T> extends ImmutableMap<ImmutableTree.Ref, Node<T>> { }
-
-    class Impl<T> implements ImmutableTree<T> {
-        readonly rootRef: ImmutableTree.Ref;
-        readonly version: number;
-        readonly nodes: ImmutableTree.Nodes<T>;
-        readonly getRef: (e: T) => ImmutableTree.Ref;
-
-        getValue(ref: Ref) {
-            const n = this.nodes.get(ref);
-            return n ? n.value : void 0;
-        }
-
-        constructor(rootRef: ImmutableTree.Ref, nodes: ImmutableTree.Nodes<T>, getRef: (e: T) => ImmutableTree.Ref, version: number) {
-            this.rootRef = rootRef;
-            this.nodes = nodes;
-            this.getRef = getRef;
-            this.version = version;
-        }
-    }
-
-    /**
-     * Create an instance of an immutable tree.
-     */
-    export function create<T>(root: T, getRef: (t: T) => ImmutableTree.Ref): ImmutableTree<T> {
-        const ref = getRef(root);
-        const r: Node<T> = { ref, value: root, version: 0, parent: ref, children: OrderedSet() };
-        return new Impl(ref, ImmutableMap([[ref, r]]), getRef, 0);
-    }
-
-    export function asTransient<T>(tree: ImmutableTree<T>) {
-        return new Transient(tree);
-    }
-
-    type N = Node<any>
-    type Ns = Nodes<any>
-
-    type VisitorCtx = { nodes: Ns, state: any, f: (node: N, nodes: Ns, state: any) => boolean | undefined | void };
-
-    function _postOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPostOrder(this, this.nodes.get(c!)!); }
-    function _doPostOrder(ctx: VisitorCtx, root: N) {
-        if (root.children.size) {
-            root.children.forEach(_postOrderFunc, ctx);
-        }
-        ctx.f(root, ctx.nodes, ctx.state);
-    }
-
-    /**
-     * Visit all nodes in a subtree in "post order", meaning leafs get visited first.
-     */
-    export function doPostOrder<T, S>(tree: ImmutableTree<T>, root: Node<T>, state: S, f: (node: Node<T>, nodes: Nodes<T>, state: S) => boolean | undefined | void): S {
-        const ctx: VisitorCtx = { nodes: tree.nodes, state, f };
-        _doPostOrder(ctx, root);
-        return ctx.state;
-    }
-
-    function _preOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPreOrder(this, this.nodes.get(c!)!); }
-    function _doPreOrder(ctx: VisitorCtx, root: N) {
-        const ret = ctx.f(root, ctx.nodes, ctx.state);
-        if (typeof ret === 'boolean' && !ret) return;
-        if (root.children.size) {
-            root.children.forEach(_preOrderFunc, ctx);
-        }
-    }
-
-    /**
-     * Visit all nodes in a subtree in "pre order", meaning leafs get visited last.
-     * If the visitor function returns false, the visiting for that branch is interrupted.
-     */
-    export function doPreOrder<T, S>(tree: ImmutableTree<T>, root: Node<T>, state: S, f: (node: Node<T>, nodes: Nodes<T>, state: S) => boolean | undefined | void): S {
-        const ctx: VisitorCtx = { nodes: tree.nodes, state, f };
-        _doPreOrder(ctx, root);
-        return ctx.state;
-    }
-
-    function _subtree(n: N, nodes: Ns, subtree: N[]) { subtree.push(n); }
-    /**
-     * Get all nodes in a subtree, leafs come first.
-     */
-    export function subtreePostOrder<T>(tree: ImmutableTree<T>, root: Node<T>) {
-        return doPostOrder<T, Node<T>[]>(tree, root, [], _subtree);
-    }
-
-
-    function _visitChildToJson(this: Ref[], ref: Ref) { this.push(ref); }
-    interface ToJsonCtx { nodes: [any, any, any[]][], refs: string[], valueToJSON: (v: any) => any }
-    function _visitNodeToJson(this: ToJsonCtx, node: Node<any>) {
-        const children: Ref[] = [];
-        node.children.forEach(_visitChildToJson as any, children);
-        this.nodes.push([this.valueToJSON(node.value), node.parent, children]);
-        this.refs.push(node.ref);
-    }
-
-    export interface Serialized {
-        root: number, // root index
-        nodes: [any /** value */, number /** parent index */, number[] /** children indices */][]
-    }
-
-    export function toJSON<T>(tree: ImmutableTree<T>, valueToJSON: (v: T) => any): Serialized {
-        const ctx: ToJsonCtx = { nodes: [], refs: [], valueToJSON };
-
-        tree.nodes.forEach(_visitNodeToJson as any, ctx);
-
-        const map = new Map<string, number>();
-        let i = 0;
-        for (const n of ctx.refs) map.set(n, i++);
-
-        for (const n of ctx.nodes) {
-            n[1] = map.get(n[1]);
-            const children = n[2];
-            for (i = 0; i < children.length; i++) {
-                children[i] = map.get(children[i]);
-            }
-        }
-        return {
-            root: map.get(tree.rootRef)!,
-            nodes: ctx.nodes
-        };
-    }
-
-    export function fromJSON<T>(data: Serialized, getRef: (v: T) => Ref, valueFromJSON: (v: any) => T): ImmutableTree<T> {
-        const nodes = ImmutableMap<ImmutableTree.Ref, Node<T>>().asMutable();
-
-        const values = data.nodes.map(n => valueFromJSON(n[0]));
-        let i = 0;
-        for (const value of values) {
-            const node = data.nodes[i++];
-            const ref = getRef(value);
-            nodes.set(ref, {
-                ref,
-                value,
-                version: 0,
-                parent: getRef(values[node[1]]),
-                children: OrderedSet(node[2].map(c => getRef(values[c])))
-            });
-        }
-        return new Impl(getRef(values[data.root]), nodes.asImmutable(), getRef, 0);
-    }
-
-    function checkSetRef(oldRef: ImmutableTree.Ref, newRef: ImmutableTree.Ref) {
-        if (oldRef !== newRef) {
-            throw new Error(`Cannot setValue of node '${oldRef}' because the new value has a different ref '${newRef}'.`);
-        }
-    }
-
-    function ensureNotPresent(nodes: Ns, ref: ImmutableTree.Ref) {
-        if (nodes.has(ref)) {
-            throw new Error(`Cannot add node '${ref}' because a different node with this ref already present in the tree.`);
-        }
-    }
-
-    function ensurePresent(nodes: Ns, ref: ImmutableTree.Ref) {
-        if (!nodes.has(ref)) {
-            throw new Error(`Node '${ref}' is not present in the tree.`);
-        }
-    }
-
-    function mutateNode(nodes: Ns, mutations: Map<ImmutableTree.Ref, N>, ref: ImmutableTree.Ref): N {
-        ensurePresent(nodes, ref);
-        if (mutations.has(ref)) {
-            return mutations.get(ref)!;
-        }
-        const node = nodes.get(ref)!;
-        const newNode: N = { ref: node.ref, value: node.value, version: node.version + 1, parent: node.parent, children: node.children.asMutable() };
-        mutations.set(ref, newNode);
-        nodes.set(ref, newNode);
-        return newNode;
-    }
-
-    export class Transient<T> implements ImmutableTree<T> {
-        nodes = this.tree.nodes.asMutable();
-        version: number = this.tree.version + 1;
-        private mutations: Map<ImmutableTree.Ref, Node<T>> = new Map();
-
-        mutate(ref: ImmutableTree.Ref): MutableNode<T> {
-            return mutateNode(this.nodes, this.mutations, ref);
-        }
-
-        get rootRef() { return this.tree.rootRef; }
-        getRef(e: T) {
-            return this.tree.getRef(e);
-        }
-
-        getValue(ref: Ref) {
-            const n = this.nodes.get(ref);
-            return n ? n.value : void 0;
-        }
-
-        add(parentRef: ImmutableTree.Ref, value: T) {
-            const ref = this.getRef(value);
-            ensureNotPresent(this.nodes, ref);
-            const parent = this.mutate(parentRef);
-            const node: Node<T> = { ref, version: 0, value, parent: parent.ref, children: OrderedSet<string>().asMutable() };
-            this.mutations.set(ref, node);
-            parent.children.add(ref);
-            this.nodes.set(ref, node);
-            return node;
-        }
-
-        setValue(ref: ImmutableTree.Ref, value: T): Node<T> {
-            checkSetRef(ref, this.getRef(value));
-            const node = this.mutate(ref);
-            node.value = value;
-            return node;
-        }
-
-        remove(ref: ImmutableTree.Ref): Node<T>[] {
-            if (ref === this.rootRef) {
-                return this.removeChildren(ref);
-            }
-
-            const { nodes, mutations } = this;
-            const node = nodes.get(ref);
-            if (!node) return [];
-            const parent = nodes.get(node.parent)!;
-            this.mutate(parent.ref).children.delete(ref);
-
-            const st = subtreePostOrder(this, node);
-            for (const n of st) {
-                nodes.delete(n.ref);
-                mutations.delete(n.ref);
-            }
-
-            return st;
-        }
-
-        removeChildren(ref: ImmutableTree.Ref): Node<T>[] {
-            const { nodes, mutations } = this;
-            let node = nodes.get(ref);
-            if (!node || !node.children.size) return [];
-            node = this.mutate(ref);
-            const st = subtreePostOrder(this, node);
-            // remove the last node which is the parent
-            st.pop();
-            node.children.clear();
-            for (const n of st) {
-                nodes.delete(n.ref);
-                mutations.delete(n.ref);
-            }
-            return st;
-        }
-
-        asImmutable() {
-            if (this.mutations.size === 0) return this.tree;
-
-            this.mutations.forEach(m => (m as MutableNode<T>).children = m.children.asImmutable());
-            return new Impl<T>(this.tree.rootRef, this.nodes.asImmutable(), this.tree.getRef, this.version);
-        }
-
-        constructor(private tree: ImmutableTree<T>) {
-
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/mol-state/selection.ts b/src/mol-state/selection.ts
index 908e6b0db..e2863f368 100644
--- a/src/mol-state/selection.ts
+++ b/src/mol-state/selection.ts
@@ -6,7 +6,7 @@
 
 import { StateObject, StateObjectCell } from './object';
 import { State } from './state';
-import { ImmutableTree } from './immutable-tree';
+import { ImmutableTree } from './tree';
 import { Transform } from './transform';
 
 namespace StateSelection {
@@ -71,7 +71,7 @@ namespace StateSelection {
         return Object.create(BuilderPrototype, { compile: { writable: false, configurable: false, value: compile } });
     }
 
-    export function root() { return build(() => (state: State) => [state.cells.get(state.tree.rootRef)!]) }
+    export function root() { return build(() => (state: State) => [state.cells.get(state.tree.root.ref)!]) }
 
 
     export function byRef(...refs: Transform.Ref[]) {
@@ -159,7 +159,7 @@ namespace StateSelection {
     export function children(b: Selector) {
         return flatMap(b, (n, s) => {
             const nodes: StateObjectCell[] = [];
-            s.tree.nodes.get(n.ref)!.children.forEach(c => nodes.push(s.cells.get(c!)!));
+            s.tree.children.get(n.ref).forEach(c => nodes.push(s.cells.get(c!)!));
             return nodes;
         });
     }
@@ -177,8 +177,8 @@ namespace StateSelection {
         let current = tree.nodes.get(root)!, len = types.length;
         while (true) {
             current = tree.nodes.get(current.parent)!;
-            if (current.ref === tree.rootRef) {
-                return cells.get(tree.rootRef);
+            if (current.ref === Transform.RootRef) {
+                return cells.get(Transform.RootRef);
             }
             const obj = cells.get(current.ref)!.obj!;
             for (let i = 0; i < len; i++) {
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index 2c42a29e7..5c9aecdf5 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -5,9 +5,8 @@
  */
 
 import { StateObject, StateObjectCell } from './object';
-import { StateTree } from './tree';
+import { StateTree, ImmutableTree } from './tree';
 import { Transform } from './transform';
-import { ImmutableTree } from './immutable-tree';
 import { Transformer } from './transformer';
 import { StateContext } from './context';
 import { UUID } from 'mol-util';
@@ -17,7 +16,7 @@ export { State }
 
 class State {
     private _tree: StateTree = StateTree.create();
-    private _current: Transform.Ref = this._tree.rootRef;
+    private _current: Transform.Ref = this._tree.root.ref;
     private transformCache = new Map<Transform.Ref, unknown>();
 
     get tree() { return this._tree; }
@@ -67,7 +66,7 @@ class State {
                     stateCtx: this.context,
                     taskCtx,
                     oldTree,
-                    tree: tree,
+                    tree,
                     cells: this.cells,
                     transformCache: this.transformCache
                 };
@@ -81,11 +80,11 @@ class State {
 
     constructor(rootObject: StateObject, params?: { globalContext?: unknown, defaultCellState?: unknown }) {
         const tree = this._tree;
-        const root = tree.getValue(tree.rootRef)!;
+        const root = tree.root;
         const defaultCellState = (params && params.defaultCellState) || { }
 
-        this.cells.set(tree.rootRef, {
-            ref: tree.rootRef,
+        this.cells.set(root.ref, {
+            ref: root.ref,
             obj: rootObject,
             status: 'ok',
             version: root.version,
@@ -94,8 +93,7 @@ class State {
 
         this.context = new StateContext({
             globalContext: params && params.globalContext,
-            defaultCellState,
-            rootRef: tree.rootRef
+            defaultCellState
         });
     }
 }
@@ -142,36 +140,33 @@ namespace State {
         }
     }
 
-    function findUpdateRoots(objects: State.Cells, tree: StateTree) {
-        const findState = {
-            roots: [] as Ref[],
-            objects
-        };
-
-        ImmutableTree.doPreOrder(tree, tree.nodes.get(tree.rootRef)!, findState, (n, _, s) => {
-            if (!s.objects.has(n.ref)) {
-                s.roots.push(n.ref);
-                return false;
-            }
-            const o = s.objects.get(n.ref)!;
-            if (o.version !== n.value.version) {
-                s.roots.push(n.ref);
-                return false;
-            }
+    function findUpdateRoots(cells: State.Cells, tree: StateTree) {
+        const findState = { roots: [] as Ref[], cells };
+        ImmutableTree.doPreOrder(tree, tree.root, findState, _findUpdateRoots);
+        return findState.roots;
+    }
 
-            return true;
-        });
+    function _findUpdateRoots(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
+        if (!s.cells.has(n.ref)) {
+            s.roots.push(n.ref);
+            return false;
+        }
+        const o = s.cells.get(n.ref)!;
+        if (o.version !== n.version) {
+            s.roots.push(n.ref);
+            return false;
+        }
 
-        return findState.roots;
+        return true;
     }
 
     type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
-    function _visitCheckDelete(n: ImmutableTree.Node<any>, _: any, ctx: FindDeletesCtx) {
+    function _visitCheckDelete(n: Transform, _: any, ctx: FindDeletesCtx) {
         if (!ctx.newTree.nodes.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
     }
     function findDeletes(ctx: UpdateContext): Ref[] {
         const deleteCtx: FindDeletesCtx = { newTree: ctx.tree, cells: ctx.cells, deletes: [] };
-        ImmutableTree.doPostOrder(ctx.oldTree, ctx.oldTree.nodes.get(ctx.oldTree.rootRef), deleteCtx, _visitCheckDelete);
+        ImmutableTree.doPostOrder(ctx.oldTree, ctx.oldTree.root, deleteCtx, _visitCheckDelete);
         return deleteCtx.deletes;
     }
 
@@ -190,7 +185,7 @@ namespace State {
         if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref });
     }
 
-    function _initVisitor(t: ImmutableTree.Node<Transform>, _: any, ctx: UpdateContext) {
+    function _initVisitor(t: Transform, _: any, ctx: UpdateContext) {
         setObjectState(ctx, t.ref, 'pending');
     }
     /** Return "resolve set" */
@@ -209,7 +204,7 @@ namespace State {
             wrap.obj = void 0;
         }
 
-        const children = ctx.tree.nodes.get(ref)!.children.values();
+        const children = ctx.tree.children.get(ref).values();
         while (true) {
             const next = children.next();
             if (next.done) return;
@@ -217,15 +212,15 @@ namespace State {
         }
     }
 
-    function findAncestor(tree: StateTree, objects: State.Cells, root: Ref, types: { type: StateObject.Type }[]): StateObject {
+    function findAncestor(tree: StateTree, cells: State.Cells, root: Ref, types: { type: StateObject.Type }[]): StateObject {
         let current = tree.nodes.get(root)!;
         while (true) {
             current = tree.nodes.get(current.parent)!;
-            if (current.ref === tree.rootRef) {
-                return objects.get(tree.rootRef)!.obj!;
+            if (current.ref === Transform.RootRef) {
+                return cells.get(Transform.RootRef)!.obj!;
             }
-            const obj = objects.get(current.ref)!.obj!;
-            for (const t of types) if (obj.type === t.type) return objects.get(current.ref)!.obj!;
+            const obj = cells.get(current.ref)!.obj!;
+            for (const t of types) if (obj.type === t.type) return cells.get(current.ref)!.obj!;
         }
     }
 
@@ -247,7 +242,7 @@ namespace State {
             return;
         }
 
-        const children = ctx.tree.nodes.get(root)!.children.values();
+        const children = ctx.tree.children.get(root).values();
         while (true) {
             const next = children.next();
             if (next.done) return;
@@ -257,7 +252,7 @@ namespace State {
 
     async function updateNode(ctx: UpdateContext, currentRef: Ref) {
         const { oldTree, tree, cells } = ctx;
-        const transform = tree.getValue(currentRef)!;
+        const transform = tree.nodes.get(currentRef);
         const parent = findAncestor(tree, cells, currentRef, transform.transformer.definition.from);
         // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
         if (!oldTree.nodes.has(currentRef) || !cells.has(currentRef)) {
@@ -274,9 +269,9 @@ namespace State {
         } else {
             // console.log('updating...', transform.transformer.id);
             const current = cells.get(currentRef)!;
-            const oldParams = oldTree.getValue(currentRef)!.params;
+            const oldParams = oldTree.nodes.get(currentRef)!.params;
 
-            const updateKind = current.status === 'ok' || current.ref === ctx.tree.rootRef
+            const updateKind = current.status === 'ok' || current.ref === Transform.RootRef
                 ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)
                 : Transformer.UpdateResult.Recreate;
 
diff --git a/src/mol-state/tree.ts b/src/mol-state/tree.ts
index 5c71179b0..d13575dbe 100644
--- a/src/mol-state/tree.ts
+++ b/src/mol-state/tree.ts
@@ -5,36 +5,34 @@
  */
 
 import { Transform } from './transform';
-import { ImmutableTree } from './immutable-tree';
+import { ImmutableTree, TransientTree } from './tree/base';
 import { Transformer } from './transformer';
 import { StateObject } from './object';
 
-interface StateTree extends ImmutableTree<Transform> { }
+export { StateTree, ImmutableTree, TransientTree }
+
+interface StateTree extends ImmutableTree { }
 
 namespace StateTree {
-    export interface Transient extends ImmutableTree.Transient<Transform> { }
+    export interface Transient extends TransientTree { }
     export interface Serialized extends ImmutableTree.Serialized { }
 
-    function _getRef(t: Transform) { return t.ref; }
-
     export function create() {
-        return ImmutableTree.create<Transform>(Transform.createRoot('<:root:>'), _getRef);
+        return ImmutableTree.create(Transform.createRoot(Transform.RootRef));
     }
 
     export function updateParams<T extends Transformer = Transformer>(tree: StateTree, ref: Transform.Ref, params: Transformer.Params<T>): StateTree {
-        const t = tree.nodes.get(ref)!.value;
+        const t = tree.nodes.get(ref)!;
         const newTransform = Transform.updateParams(t, params);
-        const newTree = ImmutableTree.asTransient(tree);
-        newTree.setValue(ref, newTransform);
-        return newTree.asImmutable();
+        return tree.asTransient().set(newTransform).asImmutable();
     }
 
     export function toJSON(tree: StateTree) {
-        return ImmutableTree.toJSON(tree, Transform.toJSON) as Serialized;
+        return ImmutableTree.toJSON(tree) as Serialized;
     }
 
     export function fromJSON(data: Serialized): StateTree {
-        return ImmutableTree.fromJSON(data, _getRef, Transform.fromJSON);
+        return ImmutableTree.fromJSON(data);
     }
 
     export interface Builder {
@@ -53,19 +51,19 @@ namespace StateTree {
         export class Root implements Builder {
             private state: State;
             to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); }
-            toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.rootRef as any, this); }
+            toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
             delete(ref: Transform.Ref) {
                 this.state.tree.remove(ref);
                 return this;
             }
             getTree(): StateTree { return this.state.tree.asImmutable(); }
-            constructor(tree: StateTree) { this.state = { tree: ImmutableTree.asTransient(tree) } }
+            constructor(tree: StateTree) { this.state = { tree: tree.asTransient() } }
         }
 
         export class To<A extends StateObject> implements Builder {
             apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Options>): To<Transformer.To<T>> {
                 const t = tr.apply(this.ref, params, props);
-                this.state.tree.add(this.ref, t);
+                this.state.tree.add(t);
                 return new To(this.state, t.ref, this.root);
             }
 
@@ -80,6 +78,4 @@ namespace StateTree {
             }
         }
     }
-}
-
-export { StateTree }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/mol-state/immutable-tree.1.ts b/src/mol-state/tree/base.ts
similarity index 90%
rename from src/mol-state/immutable-tree.1.ts
rename to src/mol-state/tree/base.ts
index 88c807a9d..ca44d05d5 100644
--- a/src/mol-state/immutable-tree.1.ts
+++ b/src/mol-state/tree/base.ts
@@ -5,7 +5,7 @@
  */
 
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from './transform';
+import { Transform } from '../transform';
 
 export { ImmutableTree, TransientTree }
 
@@ -28,7 +28,8 @@ namespace ImmutableTree {
         readonly size: number,
         readonly values: OrderedSet<Ref>['values'],
         has(ref: Ref): boolean,
-        readonly forEach: OrderedSet<Ref>['forEach']
+        readonly forEach: OrderedSet<Ref>['forEach'],
+        readonly map: OrderedSet<Ref>['map']
     }
 
     export type Node = Transform
@@ -49,11 +50,11 @@ namespace ImmutableTree {
     /**
      * Create an instance of an immutable tree.
      */
-    export function create<T>(root: Transform): ImmutableTree {
+    export function create(root: Transform): ImmutableTree {
         return new Impl(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), 0);
     }
 
-    export function construct<T>(nodes: Nodes, children: Children, version: number): ImmutableTree {
+    export function construct(nodes: Nodes, children: Children, version: number): ImmutableTree {
         return new Impl(nodes, children, version);
     }
 
@@ -160,6 +161,7 @@ class TransientTree implements ImmutableTree {
 
     version: number = this.tree.version + 1;
 
+    private changedValue = false;
     private mutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> = new Map();
 
     get root() { return this.nodes.get(Transform.RootRef)! }
@@ -213,12 +215,14 @@ class TransientTree implements ImmutableTree {
         }
 
         this.nodes.set(ref, transform);
-        return transform;
+        return this;
     }
 
     set(transform: Transform) {
         ensurePresent(this.nodes, transform.ref);
+        this.changedValue = true;
         this.nodes.set(transform.ref, transform);
+        return this;
     }
 
     remove(ref: Transform.Ref): Transform[] {
@@ -243,24 +247,8 @@ class TransientTree implements ImmutableTree {
         return st;
     }
 
-    // removeChildren(ref: ImmutableTree.Ref): TransientTree.Node[] {
-    //     const { nodes, mutations } = this;
-    //     let node = nodes.get(ref);
-    //     if (!node || !node.children.size) return [];
-    //     node = this.mutate(ref);
-    //     const st = ImmutableTree.subtreePostOrder(this, node);
-    //     // remove the last node which is the parent
-    //     st.pop();
-    //     node.children.clear();
-    //     for (const n of st) {
-    //         nodes.delete(n.value.ref);
-    //         mutations.delete(n.value.ref);
-    //     }
-    //     return st;
-    // }
-
     asImmutable() {
-        if (this.mutations.size === 0) return this.tree;
+        if (!this.changedValue && this.mutations.size === 0) return this.tree;
 
         this.mutations.forEach(fixChildMutations, this.children);
         return ImmutableTree.construct(this.nodes.asImmutable(), this.children.asImmutable(), this.version);
diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts
index 799c28dc6..11ddf2094 100644
--- a/src/perf-tests/state.ts
+++ b/src/perf-tests/state.ts
@@ -121,14 +121,14 @@ export function printTTree(tree: StateTree) {
     let lines: string[] = [];
     function print(offset: string, ref: any) {
         const t = tree.nodes.get(ref)!;
-        const tr = t.value;
+        const tr = t;
 
         const name = tr.transformer.id;
-        lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.value.version}`);
+        lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.version}`);
         offset += '   ';
 
-        t.children.forEach(c => print(offset, c!));
+        tree.children.get(ref).forEach(c => print(offset, c!));
     }
-    print('', tree.rootRef);
+    print('', tree.root.ref);
     console.log(lines.join('\n'));
 }
\ No newline at end of file
-- 
GitLab