/** * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal */ import { Task } from '../mol-task'; import { StateObject, StateObjectCell } from './object'; import { StateTransform } from './transform'; import { ParamDefinition as PD } from '../mol-util/param-definition'; import { StateAction } from './action'; import { capitalize } from '../mol-util/string'; import { StateTreeSpine } from './tree/spine'; export { Transformer as StateTransformer }; interface Transformer { apply(parent: StateTransform.Ref, params?: P, props?: Partial): StateTransform, toAction(): StateAction, readonly namespace: string, readonly id: Transformer.Id, readonly definition: Transformer.Definition, /** create a fresh copy of the params which can be edited in place */ createDefaultParams(a: A, globalCtx: unknown): P } namespace Transformer { export type Id = string & { '@type': 'transformer-id' } export type Params> = T extends Transformer ? P : unknown; export type From> = T extends Transformer ? A : unknown; export type To> = T extends Transformer ? B : unknown; export type Cell> = T extends Transformer ? StateObjectCell : unknown; export function getParamDefinition(t: T, a: From | undefined, globalCtx: unknown): PD.For> { return t.definition.params ? t.definition.params(a, globalCtx) as any : { } as any; } export function is(obj: any): obj is Transformer { return !!obj && typeof (obj as Transformer).toAction === 'function' && typeof (obj as Transformer).apply === 'function'; } export interface ApplyParams { a: A, params: P, /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */ cache: unknown, spine: StateTreeSpine, dependencies?: { [k: string]: StateObject } } export interface UpdateParams { a: A, b: B, oldParams: P, newParams: P, /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */ cache: unknown, spine: StateTreeSpine, dependencies?: { [k: string]: StateObject } } export interface AutoUpdateParams { a: A, b: B, oldParams: P, newParams: P } export interface DisposeParams { b: B | undefined, params: P | undefined, cache: unknown } export enum UpdateResult { Unchanged, Updated, Recreate, Null } /** Specify default control descriptors for the parameters */ // export type ParamsDefinition = (a: A, globalCtx: unknown) => { [K in keyof P]: PD.Any } export interface DefinitionBase { /** * Apply the actual transformation. It must be pure (i.e. with no side effects). * Returns a task that produces the result of the result directly. */ apply(params: ApplyParams, globalCtx: unknown): Task | B, /** * Attempts to update the entity in a non-destructive way. * For example changing a color scheme of a visual does not require computing new geometry. * Return/resolve to undefined if the update is not possible. */ update?(params: UpdateParams, globalCtx: unknown): Task | UpdateResult, /** Determine if the transformer can be applied automatically on UI change. Default is false. */ canAutoUpdate?(params: AutoUpdateParams, globalCtx: unknown): boolean, /** Test if the transform can be applied to a given node */ isApplicable?(a: A, globalCtx: unknown): boolean, /** By default, returns true */ isSerializable?(params: P): { isSerializable: true } | { isSerializable: false; reason: string }, /** Parameter interpolation */ interpolate?(src: P, target: P, t: number, globalCtx: unknown): P /** * Cleanup resources * * Automatically called on deleting an object and on recreating it * (i.e. when update returns UpdateResult.Recreate or UpdateResult.Null) * * Not called on UpdateResult.Updated because the resources might not * have been invalidated. In this case, the possible cleanup has to be handled * manually. */ dispose?(params: DisposeParams, globalCtx: unknown): void /** Custom conversion to and from JSON */ readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P } } export interface Definition extends DefinitionBase { readonly name: string, readonly from: StateObject.Ctor[], readonly to: StateObject.Ctor[], readonly display: { readonly name: string, readonly description?: string }, params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any }, /** * Decorators are special Transformers mapping the object to the same type. * * Special rules apply: * - applying decorator always "inserts" it instead * - applying to a decorated Transform is applied to the decorator instead (transitive) */ readonly isDecorator?: boolean } const registry = new Map>(); const fromTypeIndex: Map = new Map(); function _index(tr: Transformer) { for (const t of tr.definition.from) { if (fromTypeIndex.has(t.type)) { fromTypeIndex.get(t.type)!.push(tr); } else { fromTypeIndex.set(t.type, [tr]); } } } export function getAll() { return Array.from(registry.values()); } export function get(id: string): Transformer { const t = registry.get(id as Id); if (!t) { throw new Error(`A transformer with signature '${id}' is not registered.`); } return t; } export function fromType(type: StateObject.Type): ReadonlyArray { return fromTypeIndex.get(type) || []; } export function create(namespace: string, definition: Definition) { const { name } = definition; const id = `${namespace}.${name}` as Id; if (registry.has(id)) { throw new Error(`A transform with id '${name}' is already registered. Please pick a unique identifier for your transforms and/or register them only once. This is to ensure that transforms can be serialized and replayed.`); } const t: Transformer = { apply(parent, params, props) { return StateTransform.create>(parent, t, params, props); }, toAction() { return StateAction.fromTransformer(t); }, namespace, id, definition, createDefaultParams(a, globalCtx) { return definition.params ? PD.getDefaultValues(definition.params(a, globalCtx)) : {} as any; } }; registry.set(id, t); _index(t); return t; } export function factory(namespace: string) { return (definition: Definition) => create(namespace, definition); } export function builderFactory(namespace: string) { return Builder.build(namespace); } export namespace Builder { export interface Type { name: string, from: A | A[], to: B | B[], /** The source StateObject can be undefined: used for generating docs. */ params?: PD.For

| ((a: StateObject.From | undefined, globalCtx: any) => PD.For

), display?: string | { name: string, description?: string }, isDecorator?: boolean } export interface Root { (info: Type): Define, StateObject.From, PD.Normalize

> } export interface Define { (def: DefinitionBase): Transformer } function root(namespace: string, info: Type): Define { return def => create(namespace, { name: info.name, from: info.from instanceof Array ? info.from : [info.from], to: info.to instanceof Array ? info.to : [info.to], display: typeof info.display === 'string' ? { name: info.display } : !!info.display ? info.display : { name: capitalize(info.name.replace(/[-]/g, ' ')) }, params: typeof info.params === 'object' ? () => info.params as any : !!info.params ? info.params as any : void 0, isDecorator: info.isDecorator, ...def }); } export function build(namespace: string): Root { return (info: any) => root(namespace, info); } } export function build(namespace: string): Builder.Root { return Builder.build(namespace); } export const ROOT = create('build-in', { name: 'root', from: [], to: [], display: { name: 'Root', description: 'For internal use.' }, apply() { throw new Error('should never be applied'); }, update() { return UpdateResult.Unchanged; } }); }