diff --git a/src/apps/viewer/index.html b/src/apps/viewer/index.html index e65ee3281561d7e5cf111c03740d03ffa38072fb..9a57c1d04577e84284f8bb4b9fcf4318a918b97f 100644 --- a/src/apps/viewer/index.html +++ b/src/apps/viewer/index.html @@ -8,6 +8,7 @@ * { margin: 0; padding: 0; + box-sizing: border-box; } html, body { width: 100%; diff --git a/src/mol-app/component/parameters.tsx b/src/mol-app/component/parameters.tsx index 02e47939a7d0a7b064c8482d3ea8ab88917a6347..c5c0c673ccecee6122783cfe0e0ca61427dd4d9e 100644 --- a/src/mol-app/component/parameters.tsx +++ b/src/mol-app/component/parameters.tsx @@ -48,7 +48,7 @@ export class ParametersComponent<P extends PD.Params> extends React.Component<Pa } render() { - return <div> + return <div style={{ width: '100%' }}> { Object.keys(this.props.params).map(k => { const param = this.props.params[k] const value = this.props.values[k] diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 50718e3e7e9d8d888ff52ec89236b47e8c8cf779..12f4a76b64c6d6f806b3b9d00fea8ce0ab8da522 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -100,6 +100,11 @@ export class PluginContext { PluginCommands.Data.Update.dispatch(this, { tree }); } + _test_updateTransform(a: Transform.Ref, params: any) { + const tree = StateTree.updateParams(this.state.data.tree, a, params); + PluginCommands.Data.Update.dispatch(this, { tree }); + } + _test_createState(url: string) { const b = StateTree.build(this.state.data.tree); diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 71723f5b754201b6d07b7c522d86ac8613d3ed69..ccf64d214db9be04d8a9f7e4f2d6256f45514526 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { PluginContext } from '../context'; -import { Transform, Transformer } from 'mol-state'; +import { Transform, Transformer, StateSelection } from 'mol-state'; import { ParametersComponent } from 'mol-app/component/parameters'; export class Controls extends React.Component<{ plugin: PluginContext }, { id: string }> { @@ -88,4 +88,55 @@ export class _test_CreateTransform extends React.Component<{ plugin: PluginConte <button onClick={() => this.create()} style={{ width: '100%' }}>Create</button> </div> } +} + +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; + } + + private getParamDef() { + const def = this.getTransform().transformer.definition; + if (!def.params || !def.params.controls) return void 0; + + const src = StateSelection.ancestorOfType(this.props.nodeRef, def.from).select(this.props.plugin.state.data)[0]; + + console.log(src, def.from); + + if (!src || !src.obj) return void 0; + return def.params.controls(src.obj, this.props.plugin); + } + + private update() { + console.log(this.props.nodeRef, this.state.params); + this.props.plugin._test_updateTransform(this.props.nodeRef, this.state.params); + } + + // componentDidMount() { + // const t = this.props.plugin.state.data.tree.nodes.get(this.props.nodeRef)!; + // if (t) this.setState({ params: t.value.params }); + // } + + state = { params: this.getTransform() ? this.getTransform().params : { } }; + + render() { + const transform = this.getTransform(); + if (!transform || transform.ref === this.props.plugin.state.data.tree.rootRef) { + return <div />; + } + + const params = this.getParamDef(); + if (!params) return <div />; + + const tr = transform.transformer; + + return <div key={`${this.props.nodeRef} ${tr.id}`}> + <div style={{ borderBottom: '1px solid #999'}}>{(tr.definition.display && tr.definition.display.name) || tr.definition.name}</div> + <ParametersComponent params={params} values={this.state.params as any} onChange={(k, v) => { + this.setState({ params: { ...this.state.params, [k]: v } }); + }} /> + <button onClick={() => this.update()} style={{ width: '100%' }}>Update</button> + </div> + } } \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 520b55069ed12fa8ca601bbf27b02e6e93a66eb0..4c0eb7a9d4d515c2b2d08c07a18bc93a3123c979 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { PluginContext } from '../context'; import { StateTree } from './state-tree'; import { Viewport } from './viewport'; -import { Controls, _test_CreateTransform } from './controls'; +import { Controls, _test_CreateTransform, _test_UpdateTransform } from './controls'; import { Transformer } from 'mol-state'; // TODO: base object with subscribe helpers, separate behavior list etc @@ -52,6 +52,10 @@ export class _test_CurrentObject extends React.Component<{ plugin: PluginContext return <div> Current Ref: {this.props.plugin.behaviors.state.data.currentObject.value.ref} <hr /> + <h3>Update</h3> + <_test_UpdateTransform key={`${ref} update`} plugin={this.props.plugin} nodeRef={ref} /> + <hr /> + <h3>Create</h3> { transforms.map((t, i) => <_test_CreateTransform key={`${t.id} ${ref} ${i}`} plugin={this.props.plugin} transformer={t} nodeRef={ref} />) } diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index 7159725571a87b72d6f93e92dacb248046d57585..66967f956c17a548e29be316cc443e4e44a991f1 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state-tree.tsx @@ -35,19 +35,25 @@ export class StateTreeNode extends React.Component<{ plugin: PluginContext, node PluginCommands.Data.RemoveObject.dispatch(this.props.plugin, { ref: this.props.nodeRef }); }}>X</a>]</> + let label: any; if (cell.status !== 'ok' || !cell.obj) { - return <div style={{ borderLeft: '1px solid black', paddingLeft: '7px' }}> - {remove} {cell.status} {cell.errorText} - </div>; - } - const obj = cell.obj as PluginStateObject.Any; - const props = obj.props; - const type = obj.type; - return <div style={{ borderLeft: '0px solid #999', paddingLeft: '0px' }}> - {remove}[<span title={type.description}>{ type.shortName }</span>] <a href='#' onClick={e => { + const name = (n.value.transformer.definition.display && n.value.transformer.definition.display.name) || n.value.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 }); + }}>{name}</a>: <i>{cell.errorText}</i></>; + } else { + const obj = cell.obj as PluginStateObject.Any; + const props = obj.props; + const type = obj.type; + label = <>[<span title={type.description}>{ type.shortName }</span>] <a href='#' onClick={e => { e.preventDefault(); PluginCommands.Data.SetCurrentObject.dispatch(this.props.plugin, { ref: this.props.nodeRef }); - }}>{props.label}</a> {props.description ? <small>{props.description}</small> : void 0} + }}>{props.label}</a> {props.description ? <small>{props.description}</small> : void 0}</>; + } + + return <div> + {remove}{label} {n.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> diff --git a/src/mol-state/immutable-tree.ts b/src/mol-state/immutable-tree.ts index dd7ce8f90afa53c5580857edfff29f1b585e1c4f..3d94a90d5a2fc828438e3b96b6057ed1e964afaf 100644 --- a/src/mol-state/immutable-tree.ts +++ b/src/mol-state/immutable-tree.ts @@ -72,7 +72,7 @@ export namespace ImmutableTree { /** * 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) { + 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; @@ -91,7 +91,7 @@ export namespace ImmutableTree { * 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) { + 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; @@ -218,18 +218,23 @@ export namespace ImmutableTree { return node; } - remove<T>(ref: ImmutableTree.Ref): Node<T>[] { + 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)!; - const children = this.mutate(parent.ref).children; + this.mutate(parent.ref).children.delete(ref); + const st = subtreePostOrder(this, node); - if (ref !== this.rootRef) children.delete(ref); for (const n of st) { - nodes.delete(n.value.ref); - mutations.delete(n.value.ref); + nodes.delete(n.ref); + mutations.delete(n.ref); } + return st; } @@ -239,11 +244,12 @@ export namespace ImmutableTree { 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) { - if (n === node) continue; - nodes.delete(n.value.ref); - mutations.delete(n.value.ref); + nodes.delete(n.ref); + mutations.delete(n.ref); } return st; } diff --git a/src/mol-state/selection.ts b/src/mol-state/selection.ts index e7eb61d63f257c04a176925101a334ae464f026f..e61dd2611e541476f57e2c7419684974f49bdb3a 100644 --- a/src/mol-state/selection.ts +++ b/src/mol-state/selection.ts @@ -47,13 +47,20 @@ namespace StateSelection { parent(): Builder; first(): Builder; filter(p: (n: StateObjectCell) => boolean): Builder; + withStatus(s: StateObjectCell.Status): Builder; subtree(): Builder; children(): Builder; ofType(t: StateObject.Type): Builder; ancestorOfType(t: StateObject.Type): Builder; + + select(state: State): CellSeq } - const BuilderPrototype: any = {}; + const BuilderPrototype: any = { + select(state: State) { + return select(this, state); + } + }; function registerModifier(name: string, f: Function) { BuilderPrototype[name] = function (this: any, ...args: any[]) { return f.call(void 0, this, ...args) }; @@ -135,6 +142,9 @@ namespace StateSelection { registerModifier('filter', filter); export function filter(b: Selector, p: (n: StateObjectCell) => boolean) { return flatMap(b, n => p(n) ? [n] : []); } + registerModifier('withStatus', withStatus); + export function withStatus(b: Selector, s: StateObjectCell.Status) { return filter(b, n => n.status === s); } + registerModifier('subtree', subtree); export function subtree(b: Selector) { return flatMap(b, (n, s) => { @@ -157,20 +167,22 @@ namespace StateSelection { 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))); } + export function ancestorOfType(b: Selector, types: StateObject.Ctor[]) { return unique(mapEntity(b, (n, s) => findAncestorOfType(s, n.ref, types))); } registerModifier('parent', parent); export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.cells.get(s.tree.nodes.get(n.ref)!.parent))); } - function findAncestorOfType({ tree, cells }: State, root: string, type: StateObject.Type): StateObjectCell | undefined { - let current = tree.nodes.get(root)!; + function findAncestorOfType({ tree, cells }: State, root: string, types: StateObject.Ctor[]): StateObjectCell | undefined { + 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); } const obj = cells.get(current.ref)!.obj!; - if (obj.type === type) return cells.get(current.ref); + for (let i = 0; i < len; i++) { + if (obj.type === types[i].type) return cells.get(current.ref); + } } } } diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 5851edbd99380b66604ee36b49ac837c6704db1f..0176e2fefabd1e7463675e81492dbe851afc3402 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -68,7 +68,7 @@ class State { taskCtx, oldTree, tree: tree, - objects: this.cells, + cells: this.cells, transformCache: this.transformCache }; // TODO: have "cancelled" error? Or would this be handled automatically? @@ -120,16 +120,16 @@ namespace State { taskCtx: RuntimeContext, oldTree: StateTree, tree: StateTree, - objects: State.Cells, + cells: State.Cells, transformCache: Map<Ref, unknown> } async function update(ctx: UpdateContext) { - const roots = findUpdateRoots(ctx.objects, ctx.tree); + const roots = findUpdateRoots(ctx.cells, ctx.tree); const deletes = findDeletes(ctx); for (const d of deletes) { - const obj = ctx.objects.has(d) ? ctx.objects.get(d)!.obj : void 0; - ctx.objects.delete(d); + const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0; + ctx.cells.delete(d); ctx.transformCache.delete(d); ctx.stateCtx.events.object.removed.next({ ref: d, obj }); // TODO: handle current object change @@ -168,7 +168,7 @@ namespace State { function findDeletes(ctx: UpdateContext): Ref[] { // TODO: do this in some sort of "tree order"? const deletes: Ref[] = []; - const keys = ctx.objects.keys(); + const keys = ctx.cells.keys(); while (true) { const key = keys.next(); if (key.done) break; @@ -179,14 +179,14 @@ namespace State { function setObjectState(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) { let changed = false; - if (ctx.objects.has(ref)) { - const obj = ctx.objects.get(ref)!; + if (ctx.cells.has(ref)) { + const obj = ctx.cells.get(ref)!; changed = obj.status !== status; obj.status = status; obj.errorText = errorText; } else { const obj: StateObjectCell = { ref, status, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } }; - ctx.objects.set(ref, obj); + ctx.cells.set(ref, obj); changed = true; } if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref }); @@ -204,7 +204,7 @@ namespace State { function doError(ctx: UpdateContext, ref: Ref, errorText: string) { setObjectState(ctx, ref, 'error', errorText); - const wrap = ctx.objects.get(ref)!; + const wrap = ctx.cells.get(ref)!; if (wrap.obj) { ctx.stateCtx.events.object.removed.next({ ref }); ctx.transformCache.delete(ref); @@ -258,14 +258,14 @@ namespace State { } async function updateNode(ctx: UpdateContext, currentRef: Ref) { - const { oldTree, tree, objects } = ctx; + const { oldTree, tree, cells } = ctx; const transform = tree.getValue(currentRef)!; - const parent = findAncestor(tree, objects, currentRef, transform.transformer.definition.from); + 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) || !objects.has(currentRef)) { + if (!oldTree.nodes.has(currentRef) || !cells.has(currentRef)) { // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef)); const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); - objects.set(currentRef, { + cells.set(currentRef, { ref: currentRef, obj, status: 'ok', @@ -275,12 +275,17 @@ namespace State { return { action: 'created', obj }; } else { // console.log('updating...', transform.transformer.id); - const current = objects.get(currentRef)!; + const current = cells.get(currentRef)!; const oldParams = oldTree.getValue(currentRef)!.params; - switch (await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)) { + + const updateKind = current.status === 'ok' + ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params) + : Transformer.UpdateResult.Recreate; + + switch (updateKind) { case Transformer.UpdateResult.Recreate: { const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); - objects.set(currentRef, { + cells.set(currentRef, { ref: currentRef, obj, status: 'ok', diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index f58771db7f445eb6524acbb892c94aee27acf241..914dda1111f260e9a083fa666a395057ad9a49b7 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -42,8 +42,8 @@ export namespace Transformer { export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { readonly name: string, - readonly from: { type: StateObject.Type }[], - readonly to: { type: StateObject.Type }[], + readonly from: StateObject.Ctor[], + readonly to: StateObject.Ctor[], readonly display?: { readonly name: string, readonly description?: string }, /**