From f7759c8f328c3fa0d8c9e8bb71fcb28554654f2a Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Mon, 29 Oct 2018 18:58:06 +0100 Subject: [PATCH] mol-state: tree selection --- src/mol-state/context.ts | 2 +- src/mol-state/index.ts | 4 +- src/mol-state/object.ts | 3 +- src/mol-state/selection.ts | 178 ++++++++++++++++++++++++++ src/mol-state/state.ts | 21 ++- src/mol-state/{tree => }/transform.ts | 22 ++-- src/mol-state/transformer.ts | 4 +- src/mol-state/tree.ts | 4 +- src/mol-state/tree/selection.ts | 11 -- src/perf-tests/state.ts | 10 +- 10 files changed, 218 insertions(+), 41 deletions(-) create mode 100644 src/mol-state/selection.ts rename src/mol-state/{tree => }/transform.ts (78%) delete mode 100644 src/mol-state/tree/selection.ts diff --git a/src/mol-state/context.ts b/src/mol-state/context.ts index bb04a1de1..4ab3b9d32 100644 --- a/src/mol-state/context.ts +++ b/src/mol-state/context.ts @@ -6,7 +6,7 @@ import { Subject } from 'rxjs' import { StateObject } from './object'; -import { Transform } from './tree/transform'; +import { Transform } from './transform'; interface StateContext { events: { diff --git a/src/mol-state/index.ts b/src/mol-state/index.ts index 0ebc69a69..89b5ea1a5 100644 --- a/src/mol-state/index.ts +++ b/src/mol-state/index.ts @@ -9,5 +9,5 @@ export * from './state' export * from './transformer' export * from './tree' export * from './context' -export * from './tree/transform' -export * from './tree/selection' \ No newline at end of file +export * from './transform' +export * from './selection' \ No newline at end of file diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index 61f9142cf..c721b5826 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -5,7 +5,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Transform } from './tree/transform'; +import { Transform } from './transform'; /** A mutable state object */ export interface StateObject<P = unknown, D = unknown> { @@ -47,6 +47,7 @@ export namespace StateObject { } export interface Node { + ref: Transform.Ref, state: StateType, props: unknown, errorText?: string, diff --git a/src/mol-state/selection.ts b/src/mol-state/selection.ts new file mode 100644 index 000000000..b442e5623 --- /dev/null +++ b/src/mol-state/selection.ts @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StateObject } from './object'; +import { State } from './state'; +import { ImmutableTree } from './util/immutable-tree'; + +namespace StateSelection { + export type Selector = Query | Builder | string | StateObject.Node; + export type NodeSeq = StateObject.Node[] + export type Query = (state: State) => NodeSeq; + + export function select(s: Selector, state: State) { + return compile(s)(state); + } + + export function compile(s: Selector): Query { + const selector = s ? s : root(); + let query: Query; + if (isBuilder(selector)) query = (selector as any).compile(); + else if (isObj(selector)) query = (byValue(selector) as any).compile(); + else if (isQuery(selector)) query = selector; + else query = (byRef(selector as string) as any).compile(); + return query; + } + + function isObj(arg: any): arg is StateObject.Node { + return (arg as StateObject.Node).version !== void 0; + } + + function isBuilder(arg: any): arg is Builder { + return arg.compile !== void 0; + } + + function isQuery(arg: any): arg is Query { + return typeof arg === 'function'; + } + + export interface Builder { + flatMap(f: (n: Node) => Node[]): Builder; + mapEntity(f: (n: Node) => Node): Builder; + unique(): Builder; + + parent(): Builder; + first(): Builder; + filter(p: (n: Node) => boolean): Builder; + subtree(): Builder; + children(): Builder; + ofType(t: StateObject.Type): Builder; + ancestorOfType(t: StateObject.Type): Builder; + } + + const BuilderPrototype: any = {}; + + function registerModifier(name: string, f: Function) { + BuilderPrototype[name] = function (this: any, ...args: any[]) { return f.call(void 0, this, ...args) }; + } + + function build(compile: () => Query): Builder { + return Object.create(BuilderPrototype, { compile: { writable: false, configurable: false, value: compile } }); + } + + export function root() { return build(() => (state: State) => [state.objects.get(state.tree.rootRef)!]) } + + + export function byRef(...refs: string[]) { + return build(() => (state: State) => { + const ret: StateObject.Node[] = []; + for (const ref of refs) { + const n = state.objects.get(ref); + if (!n) continue; + ret.push(n); + } + return ret; + }); + } + + export function byValue(...objects: StateObject.Node[]) { return build(() => (state: State) => objects); } + + registerModifier('flatMap', flatMap); + export function flatMap(b: Selector, f: (obj: StateObject.Node, state: State) => NodeSeq) { + const q = compile(b); + return build(() => (state: State) => { + const ret: StateObject.Node[] = []; + for (const n of q(state)) { + for (const m of f(n, state)) { + ret.push(m); + } + } + return ret; + }); + } + + registerModifier('mapEntity', mapEntity); + export function mapEntity(b: Selector, f: (n: StateObject.Node, state: State) => StateObject.Node | undefined) { + const q = compile(b); + return build(() => (state: State) => { + const ret: StateObject.Node[] = []; + for (const n of q(state)) { + const x = f(n, state); + if (x) ret.push(x); + } + return ret; + }); + } + + registerModifier('unique', unique); + export function unique(b: Selector) { + const q = compile(b); + return build(() => (state: State) => { + const set = new Set<string>(); + const ret: StateObject.Node[] = []; + for (const n of q(state)) { + if (!set.has(n.ref)) { + set.add(n.ref); + ret.push(n); + } + } + return ret; + }) + } + + registerModifier('first', first); + export function first(b: Selector) { + const q = compile(b); + return build(() => (state: State) => { + const r = q(state); + return r.length ? [r[0]] : []; + }); + } + + registerModifier('filter', filter); + export function filter(b: Selector, p: (n: StateObject.Node) => boolean) { return flatMap(b, n => p(n) ? [n] : []); } + + registerModifier('subtree', subtree); + export function subtree(b: Selector) { + return flatMap(b, (n, s) => { + const nodes = [] as string[]; + ImmutableTree.doPreOrder(s.tree, s.tree.nodes.get(n.ref), nodes, (x, _, ctx) => { ctx.push(x.ref) }); + return nodes.map(x => s.objects.get(x)!); + }); + } + + registerModifier('children', children); + export function children(b: Selector) { + return flatMap(b, (n, s) => { + const nodes: StateObject.Node[] = []; + s.tree.nodes.get(n.ref)!.children.forEach(c => nodes.push(s.objects.get(c!)!)); + return nodes; + }); + } + + registerModifier('ofType', ofType); + export function ofType(b: Selector, t: StateObject.Type) { return filter(b, n => n.obj ? n.obj.type === t : false); } + + registerModifier('ancestorOfType', ancestorOfType); + export function ancestorOfType(b: Selector, t: StateObject.Type) { return unique(mapEntity(b, (n, s) => findAncestorOfType(s, n.ref, t))); } + + registerModifier('parent', parent); + export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.objects.get(s.tree.nodes.get(n.ref)!.parent))); } + + function findAncestorOfType({ tree, objects }: State, root: string, type: StateObject.Type): StateObject.Node | undefined { + let current = tree.nodes.get(root)!; + while (true) { + current = tree.nodes.get(current.parent)!; + if (current.ref === tree.rootRef) { + return objects.get(tree.rootRef); + } + const obj = objects.get(current.ref)!.obj!; + if (obj.type === type) return objects.get(current.ref); + } + } +} + +export { StateSelection } \ No newline at end of file diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index b586e5c2c..873579731 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -6,7 +6,7 @@ import { StateObject } from './object'; import { StateTree } from './tree'; -import { Transform } from './tree/transform'; +import { Transform } from './transform'; import { ImmutableTree } from './util/immutable-tree'; import { Transformer } from './transformer'; import { StateContext } from './context'; @@ -23,13 +23,20 @@ export namespace State { export type Ref = Transform.Ref export type Objects = Map<Ref, StateObject.Node> - export function create(params?: { globalContext?: unknown, defaultObjectProps: unknown }) { + export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps: unknown }) { const tree = StateTree.create(); const objects: Objects = new Map(); const root = tree.getValue(tree.rootRef)!; const defaultObjectProps = (params && params.defaultObjectProps) || { } - objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version, props: { ...defaultObjectProps } }); + rootObject.ref = tree.rootRef; + objects.set(tree.rootRef, { + ref: tree.rootRef, + obj: rootObject, + state: StateObject.StateType.Ok, + version: root.version, + props: { ...defaultObjectProps } + }); return { tree, @@ -126,7 +133,7 @@ export namespace State { obj.state = state; obj.errorText = errorText; } else { - const obj = { state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } }; + const obj: StateObject.Node = { ref, state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } }; ctx.objects.set(ref, obj); changed = true; } @@ -159,7 +166,7 @@ export namespace State { } } - function findParent(tree: StateTree, objects: Objects, root: Ref, types: { type: StateObject.Type }[]): StateObject { + function findAncestor(tree: StateTree, objects: Objects, root: Ref, types: { type: StateObject.Type }[]): StateObject { let current = tree.nodes.get(root)!; while (true) { current = tree.nodes.get(current.parent)!; @@ -200,13 +207,14 @@ export namespace State { async function updateNode(ctx: UpdateContext, currentRef: Ref) { const { oldTree, tree, objects } = ctx; const transform = tree.getValue(currentRef)!; - const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from); + const parent = findAncestor(tree, objects, 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) || !objects.has(currentRef)) { // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef)); const obj = await createObject(ctx, transform.transformer, parent, transform.params); obj.ref = currentRef; objects.set(currentRef, { + ref: currentRef, obj, state: StateObject.StateType.Ok, version: transform.version, @@ -222,6 +230,7 @@ export namespace State { const obj = await createObject(ctx, transform.transformer, parent, transform.params); obj.ref = currentRef; objects.set(currentRef, { + ref: currentRef, obj, state: StateObject.StateType.Ok, version: transform.version, diff --git a/src/mol-state/tree/transform.ts b/src/mol-state/transform.ts similarity index 78% rename from src/mol-state/tree/transform.ts rename to src/mol-state/transform.ts index 6e494e9d2..f80137241 100644 --- a/src/mol-state/tree/transform.ts +++ b/src/mol-state/transform.ts @@ -4,8 +4,8 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { StateObject } from '../object'; -import { Transformer } from '../transformer'; +import { StateObject } from './object'; +import { Transformer } from './transformer'; import { UUID } from 'mol-util'; export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { @@ -19,22 +19,16 @@ export interface Transform<A extends StateObject = StateObject, B extends StateO export namespace Transform { export type Ref = string - export interface Props { - ref: Ref - } - - export enum Flags { - // Indicates that the transform was generated by a behaviour and should not be automatically updated - Generated - } + export interface Options { ref?: Ref, defaultProps?: unknown } - export function create<A extends StateObject, B extends StateObject, P>(transformer: Transformer<A, B, P>, params?: P, props?: Partial<Props>): Transform<A, B, P> { - const ref = props && props.ref ? props.ref : UUID.create() as string as Ref; + export function create<A extends StateObject, B extends StateObject, P>(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; return { transformer, - params: params || { } as any, + params: params || {} as any, ref, - version: UUID.create() + version: UUID.create(), + defaultProps: options && options.defaultProps } } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 701085fff..972d21833 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -6,10 +6,10 @@ import { Task } from 'mol-task'; import { StateObject } from './object'; -import { Transform } from './tree/transform'; +import { Transform } from './transform'; export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { - apply(params?: P, props?: Partial<Transform.Props>): Transform<A, B, P>, + apply(params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, readonly namespace: string, readonly id: Transformer.Id, readonly definition: Transformer.Definition<A, B, P> diff --git a/src/mol-state/tree.ts b/src/mol-state/tree.ts index 06e83f0ae..4656b98f6 100644 --- a/src/mol-state/tree.ts +++ b/src/mol-state/tree.ts @@ -4,7 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Transform } from './tree/transform'; +import { Transform } from './transform'; import { ImmutableTree } from './util/immutable-tree'; import { Transformer } from './transformer'; import { StateObject } from './object'; @@ -59,7 +59,7 @@ namespace StateTree { } export class To<A extends StateObject> implements Builder { - apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Props>): To<Transformer.To<T>> { + 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(params, props); this.state.tree.add(this.ref, t); return new To(this.state, t.ref); diff --git a/src/mol-state/tree/selection.ts b/src/mol-state/tree/selection.ts deleted file mode 100644 index 84130cdc3..000000000 --- a/src/mol-state/tree/selection.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -namespace StateTreeSelection { - -} - -export { StateTreeSelection } \ No newline at end of file diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts index 0344b8dc4..183063a9a 100644 --- a/src/perf-tests/state.ts +++ b/src/perf-tests/state.ts @@ -1,4 +1,4 @@ -import { State, StateObject, StateTree, Transformer } from 'mol-state'; +import { State, StateObject, StateTree, Transformer, StateSelection } from 'mol-state'; import { Task } from 'mol-task'; import * as util from 'util'; @@ -74,7 +74,7 @@ function hookEvents(state: State) { } export async function testState() { - const state = State.create(); + const state = State.create(new Root({ label: 'Root' }, { })); hookEvents(state); const tree = state.tree; @@ -105,6 +105,12 @@ export async function testState() { console.log('----------------'); const state2 = await State.update(state1, treeFromJson).run(); console.log(util.inspect(state2.objects, true, 3, true)); + + console.log('----------------'); + + const q = StateSelection.byRef('square').parent(); + const sel = StateSelection.select(q, state2); + console.log(sel); } testState(); -- GitLab