Skip to content
Snippets Groups Projects
Commit d120039e authored by David Sehnal's avatar David Sehnal
Browse files

wip state & plugin

parent 2d00b5d2
No related branches found
No related tags found
No related merge requests found
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
export { PluginBehaviour }
interface PluginBehaviour<P> {
register(): void,
unregister(): void,
/** Update params in place. Optionally return a promise if it depends on an async action. */
update(params: P): void | Promise<void>
}
namespace PluginBehaviour {
export interface Ctor<P> {
create(params: P): PluginBehaviour<P>
}
}
\ No newline at end of file
// TODO: command interface and queue.
// How to handle command resolving? Track how many subscriptions a command has?
\ No newline at end of file
......@@ -35,6 +35,15 @@ export class PluginContext {
}
}
/**
* This should be used in all transform related request so that it could be "spoofed" to allow
* "static" access to resources.
*/
async fetch(url: string, type: 'string' | 'binary' = 'string'): Promise<string | Uint8Array> {
const req = await fetch(url);
return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer());
}
dispose() {
if (this.disposed) return;
this.canvas3d.dispose();
......
......@@ -8,24 +8,37 @@ import { PluginStateTransform } from '../base';
import { PluginStateObjects as SO } from '../objects';
import { Task } from 'mol-task';
import CIF from 'mol-io/reader/cif'
import { PluginContext } from 'mol-plugin/context';
export const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, { url: string, isBinary?: boolean, label?: string }>({
export { Download }
namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } }
const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, Download.Params>({
name: 'download',
display: {
name: 'Download',
description: 'Download string or binary data from the specified URL'
},
from: [SO.Root],
to: [SO.Data.String, SO.Data.Binary],
apply({ params: p }) {
apply({ params: p }, globalCtx: PluginContext) {
return Task.create('Download', async ctx => {
// TODO: track progress
const req = await fetch(p.url);
const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string');
return p.isBinary
? new SO.Data.Binary({ label: p.label ? p.label : p.url }, new Uint8Array(await req.arrayBuffer()))
: new SO.Data.String({ label: p.label ? p.label : p.url }, await req.text());
? new SO.Data.Binary({ label: p.label ? p.label : p.url }, data as Uint8Array)
: new SO.Data.String({ label: p.label ? p.label : p.url }, data as string);
});
}
});
export const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, { }>({
export { ParseCif }
namespace ParseCif { export interface Params { } }
const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, ParseCif.Params>({
name: 'parse-cif',
display: {
name: 'Parse CIF',
description: 'Parse CIF from String or Binary data'
},
from: [SO.Data.String, SO.Data.Binary],
to: [SO.Data.Cif],
apply({ a }) {
......
......@@ -9,11 +9,17 @@ import { PluginStateObjects as SO } from '../objects';
import { Task } from 'mol-task';
import { Model, Format, Structure } from 'mol-model/structure';
export const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, { blockHeader?: string }>({
export { CreateModelsFromMmCif }
namespace CreateModelsFromMmCif { export interface Params { blockHeader?: string } }
const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, CreateModelsFromMmCif.Params>({
name: 'create-models-from-mmcif',
display: {
name: 'Models from mmCIF',
description: 'Identify and create all separate models in the specified CIF data block'
},
from: [SO.Data.Cif],
to: [SO.Models],
defaultParams: a => ({ blockHeader: a.data.blocks[0].header }),
params: { default: a => ({ blockHeader: a.data.blocks[0].header }) },
apply({ a, params }) {
return Task.create('Parse mmCIF', async ctx => {
const header = params.blockHeader || a.data.blocks[0].header;
......@@ -27,11 +33,17 @@ export const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO
}
});
export const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, { modelIndex: number }>({
export { CreateStructureFromModel }
namespace CreateStructureFromModel { export interface Params { modelIndex: number } }
const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, CreateStructureFromModel.Params>({
name: 'structure-from-model',
display: {
name: 'Structure from Model',
description: 'Create a molecular structure from the specified model.'
},
from: [SO.Models],
to: [SO.Structure],
defaultParams: () => ({ modelIndex: 0 }),
params: { default: () => ({ modelIndex: 0 }) },
apply({ a, params }) {
if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
// TODO: make Structure.ofModel async?
......
......@@ -10,11 +10,12 @@ import { PluginStateTransform } from '../base';
import { PluginStateObjects as SO } from '../objects';
import { CartoonRepresentation, DefaultCartoonProps } from 'mol-repr/structure/representation/cartoon';
export const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, { }>({
export { CreateStructureRepresentation }
namespace CreateStructureRepresentation { export interface Params { } }
const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, CreateStructureRepresentation.Params>({
name: 'create-structure-representation',
from: [SO.Structure],
to: [SO.StructureRepresentation3D],
defaultParams: () => ({ modelIndex: 0 }),
apply({ a, params }) {
return Task.create('Structure Representation', async ctx => {
const repr = CartoonRepresentation();
......
......@@ -9,7 +9,7 @@ import { Transform } from './transform';
import { UUID } from 'mol-util';
/** A mutable state object */
export interface StateObject<P = unknown, D = unknown> {
export interface StateObject<P = any, D = any> {
readonly id: UUID,
readonly type: StateObject.Type,
readonly props: P,
......@@ -45,7 +45,6 @@ export namespace StateObject {
static is(obj?: StateObject): obj is StateObject<Props, Data> { return !!obj && dataType === obj.type; }
id = UUID.create();
type = dataType;
ref = 'not set' as Transform.Ref;
constructor(public props: Props, public data: Data) { }
}
}
......
......@@ -17,6 +17,8 @@ export { State }
class State {
private _tree: StateTree = StateTree.create();
private transformCache = new Map<Transform.Ref, unknown>();
get tree() { return this._tree; }
readonly objects: State.Objects = new Map();
......@@ -44,7 +46,8 @@ class State {
taskCtx,
oldTree,
tree: tree,
objects: this.objects
objects: this.objects,
transformCache: this.transformCache
};
// TODO: have "cancelled" error? Or would this be handled automatically?
return update(ctx);
......@@ -71,7 +74,7 @@ class State {
}
}
namespace State {
namespace State {
export type Objects = Map<Transform.Ref, StateObject.Node>
export interface Snapshot {
......@@ -91,7 +94,8 @@ namespace State {
taskCtx: RuntimeContext,
oldTree: StateTree,
tree: StateTree,
objects: State.Objects
objects: State.Objects,
transformCache: Map<Ref, unknown>
}
async function update(ctx: UpdateContext) {
......@@ -99,6 +103,7 @@ namespace State {
const deletes = findDeletes(ctx);
for (const d of deletes) {
ctx.objects.delete(d);
ctx.transformCache.delete(d);
ctx.stateCtx.events.object.removed.next({ ref: d });
}
......@@ -174,6 +179,7 @@ namespace State {
const wrap = ctx.objects.get(ref)!;
if (wrap.obj) {
ctx.stateCtx.events.object.removed.next({ ref });
ctx.transformCache.delete(ref);
wrap.obj = void 0;
}
......@@ -230,7 +236,7 @@ namespace State {
// 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);
const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
objects.set(currentRef, {
ref: currentRef,
obj,
......@@ -243,9 +249,9 @@ namespace State {
// console.log('updating...', transform.transformer.id);
const current = objects.get(currentRef)!;
const oldParams = oldTree.getValue(currentRef)!.params;
switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
switch (await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
case Transformer.UpdateResult.Recreate: {
const obj = await createObject(ctx, transform.transformer, parent, transform.params);
const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
objects.set(currentRef, {
ref: currentRef,
obj,
......@@ -271,13 +277,20 @@ namespace State {
return t as T;
}
function createObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, params: any) {
return runTask(transformer.definition.apply({ a, params }, ctx.stateCtx.globalContext), ctx.taskCtx);
function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
const cache = { };
ctx.transformCache.set(ref, cache);
return runTask(transformer.definition.apply({ a, params, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
}
async function updateObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
if (!transformer.definition.update) {
return Transformer.UpdateResult.Recreate;
}
return runTask(transformer.definition.update({ a, oldParams, b, newParams }, ctx.stateCtx.globalContext), ctx.taskCtx);
let cache = ctx.transformCache.get(ref);
if (!cache) {
cache = { };
ctx.transformCache.set(ref, cache);
}
return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
}
\ No newline at end of file
......@@ -7,6 +7,7 @@
import { Task } from 'mol-task';
import { StateObject } from './object';
import { Transform } from './transform';
import { Param } from 'mol-util/parameter';
export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
apply(params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>,
......@@ -19,18 +20,22 @@ export namespace Transformer {
export type Id = string & { '@type': 'transformer-id' }
export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown;
export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown;
export type ControlsFor<Props> = { [P in keyof Props]?: any }
export type ControlsFor<A extends StateObject, Props> = { [P in keyof Props]?: ((a: A, globalCtx: unknown) => Param) }
export interface ApplyParams<A extends StateObject = StateObject, P = unknown> {
a: A,
params: P
params: P,
/** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
cache: unknown
}
export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
a: A,
b: B,
oldParams: P,
newParams: P
newParams: P,
/** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
cache: unknown
}
export enum UpdateResult { Unchanged, Updated, Recreate }
......@@ -39,6 +44,7 @@ export namespace Transformer {
readonly name: string,
readonly from: { type: StateObject.Type }[],
readonly to: { type: StateObject.Type }[],
readonly display?: { readonly name: string, readonly description?: string },
/**
* Apply the actual transformation. It must be pure (i.e. with no side effects).
......@@ -53,17 +59,16 @@ export namespace Transformer {
*/
update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult,
/** Check the parameters and return a list of errors if the are not valid. */
defaultParams?(a: A, globalCtx: unknown): P,
/** Specify default control descriptors for the parameters */
defaultControls?(a: A, globalCtx: unknown): Transformer.ControlsFor<P>,
/** Check the parameters and return a list of errors if the are not valid. */
validateParams?(a: A, params: P, globalCtx: unknown): string[] | undefined,
/** Optional custom parameter equality. Use deep structural equal by default. */
areParamsEqual?(oldParams: P, newParams: P): boolean,
params?: {
/** Check the parameters and return a list of errors if the are not valid. */
default?(a: A, globalCtx: unknown): P,
/** Specify default control descriptors for the parameters */
controls?(a: A, globalCtx: unknown): ControlsFor<A, P>,
/** Check the parameters and return a list of errors if the are not valid. */
validate?(a: A, params: P, globalCtx: unknown): string[] | undefined,
/** Optional custom parameter equality. Use deep structural equal by default. */
areEqual?(oldParams: P, newParams: P): boolean
}
/** Test if the transform can be applied to a given node */
isApplicable?(a: A, globalCtx: unknown): boolean,
......@@ -75,7 +80,7 @@ export namespace Transformer {
customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P }
}
const registry = new Map<Id, Transformer>();
const registry = new Map<Id, Transformer<any, any>>();
export function get(id: string): Transformer {
const t = registry.get(id as Id);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment