UNPKG

@ndbx/runtime

Version:

The `@ndbx/runtime` package provides a runtime environment to embed NodeBox visualizations directly into React applications. NodeBox is a powerful tool for creating interactive and generative visualizations, and this runtime allows you to integrate those

359 lines (324 loc) 9.87 kB
import { Item, Port as IPort, PortType, Parameter as IParameter, ParameterType, ParameterValue, Color, Choice, PortValue, LiteralValue, WidgetType, ContextGlobals, Section, } from "./types"; import { startCase } from "./string-utils"; import Context from "./context"; import { evaluateExpression } from "./expression"; import { Paint } from "@ndbx/g"; type ParameterChoices = string[] | string[][]; export function parseChoices(choices: string[] | string[][]): Choice[] { if (choices.length === 0) return []; if (typeof choices[0] === "string") { return (choices as string[]).map((name) => ({ name, label: startCase(name) })); } else { return (choices as string[][]).map(([name, label]) => ({ name, label })); } } export function defaultValueForType(type: ParameterType): LiteralValue { if (type === ParameterType.Number) { return 0; } else if (type === ParameterType.String || type === ParameterType.File) { return ""; } else if (type === ParameterType.Boolean) { return false; } else if (type === ParameterType.Point) { return { x: 0, y: 0 }; } else if (type === ParameterType.Color) { return { r: 0, g: 0, b: 0, a: 1 }; } else { throw new Error(`Invalid parameter type: ${type}`); } } export function defaultWidgetForType(type: ParameterType): WidgetType { return type as unknown as WidgetType; } export function createExpressionContext(_cx: Context, globals: ContextGlobals): ContextGlobals { const networkProxy = new Proxy(globals.network, { get(target, prop: string) { if (target.hasOwnProperty(prop)) { return (target as Record<string, any>)[prop]; } const param = target.parameters.find((p) => p.name === prop); if (param) { if (globals.values && globals.values.hasOwnProperty(prop)) { return globals.values[prop]; } else { return param.defaultValue; } } }, }); return { network: networkProxy, values: globals.values }; } class Parameter implements IParameter { node: RuntimeNode; type: ParameterType; widget: WidgetType; name: string; label: string; section?: string; defaultValue: LiteralValue; choices?: Choice[]; min: number; max: number; step: number; constructor( node: RuntimeNode, name: string, type: ParameterType, defaultValue: LiteralValue | undefined = undefined, choices?: Choice[], ) { this.node = node; this.type = type; this.widget = defaultWidgetForType(type); this.name = name; this.label = startCase(name); this.defaultValue = typeof defaultValue !== "undefined" ? defaultValue : defaultValueForType(type); this.choices = choices; this.min = -Infinity; this.max = Infinity; this.step = 1; this.section = undefined; } get value() { const nodeValue = this.node.values?.[this.name]; if (nodeValue !== undefined) { if (nodeValue.type === "VALUE") { if (this.type === ParameterType.Color) { return Paint.parse(nodeValue.value as unknown as Color); } return nodeValue.value; } else { // HACK This is to make network expressions work, without breaking the example plot projects if (nodeValue.expression.startsWith("network.")) { const ctx = this.node.globals; let result = evaluateExpression(nodeValue.expression, ctx as ContextGlobals); if (this.type === ParameterType.Color) { result = Paint.parse(result as unknown as Color); } return result; } else { return nodeValue.expression; } } } else { return this.defaultValue; } } get fn() { return (d: Record<string, any>) => { const nodeValue = this.node.values?.[this.name]; if (nodeValue === undefined) return this.defaultValue; if (nodeValue.type === "VALUE") { if (this.type === ParameterType.Color) { return Paint.parse(nodeValue.value as unknown as Color); } return nodeValue.value; } else { const ctx = { ...d, ...this.node.globals }; let result = evaluateExpression(nodeValue.expression, ctx as ContextGlobals); if (this.type === ParameterType.Color) { result = Paint.parse(result as unknown as Color); } return result; } }; } get timeDependent() { const nodeValue = this.node.values?.[this.name]; if (nodeValue === undefined) return false; return nodeValue.type === "EXPRESSION" && /(\$FRAME|\$TIME|\$NOW|osc)/.test(nodeValue.expression); } } class Port implements IPort { node: RuntimeNode; type: PortType; name: string; _value: PortValue; constructor(node: RuntimeNode, name: string, type: PortType) { this.node = node; this.name = name; this.type = type; this._value = null; } get value(): PortValue { const portValue = this.node.cx.portValues.get(`${this.node.nodeId}/${this.name}`); if (portValue !== undefined) return portValue; return this._value; } set(value: PortValue) { this._value = value; } } export default class RuntimeNode { cx: Context; nodeId: string; nodeFn: Item; inputPorts: Port[]; outputPorts: Port[]; parameters: Parameter[]; values: Record<string, ParameterValue>; _timeDependent: boolean; dirty: boolean; globals: ContextGlobals | null; sections: Section[]; _currentSection?: string; message?: string; constructor(cx: Context, nodeId: string, nodeFn: Item) { this.cx = cx; this.nodeId = nodeId; this.nodeFn = nodeFn; this.inputPorts = []; this.outputPorts = []; this.parameters = []; this.values = {}; this._timeDependent = false; this.dirty = true; this.globals = null; this.sections = []; this._currentSection = undefined; } get timeDependent() { if (this._timeDependent) { return true; } return this.parameters.some((p) => p.timeDependent); } set timeDependent(value) { this._timeDependent = value; } numberIn({ name, value, min, max, step, }: { name: string; value: LiteralValue | undefined; min?: number; max?: number; step?: number; }): Parameter { const parameter = new Parameter(this, name, ParameterType.Number, value); parameter.section = this._currentSection; if (min !== undefined) { parameter.min = min; } if (max !== undefined) { parameter.max = max; } if (step !== undefined) { parameter.step = step; } this.parameters.push(parameter); return parameter; } stringIn({ name, value, widget, choices, }: { name: string; value: LiteralValue | undefined; widget?: WidgetType; choices?: ParameterChoices; }): Parameter { const parameter = new Parameter(this, name, ParameterType.String, value); parameter.section = this._currentSection; if (widget) { parameter.widget = widget; } if (choices) { let realChoices: Choice[] = parseChoices(choices); parameter.choices = realChoices; } this.parameters.push(parameter); return parameter; } booleanIn({ name, value }: { name: string; value: LiteralValue | undefined }): Parameter { const parameter = new Parameter(this, name, ParameterType.Boolean, value); parameter.section = this._currentSection; this.parameters.push(parameter); return parameter; } colorIn({ name, value }: { name: string; value: LiteralValue | undefined }): Parameter { if (typeof value === "string") { value = Paint.parse(value) as unknown as Color; } const parameter = new Parameter(this, name, ParameterType.Color, value); parameter.section = this._currentSection; this.parameters.push(parameter); return parameter; } choiceIn({ name, value, choices }: { name: string; value: LiteralValue | undefined; choices: Choice[] }): Parameter { const parameter = new Parameter(this, name, ParameterType.Choice, value, choices); parameter.section = this._currentSection; this.parameters.push(parameter); return parameter; } fileIn({ name, value }: { name: string; value: LiteralValue | undefined }): Parameter { const parameter = new Parameter(this, name, ParameterType.File, value); parameter.section = this._currentSection; this.parameters.push(parameter); return parameter; } tableIn({ name }: { name: string }): Port { const port = new Port(this, name, PortType.Table); this.inputPorts.push(port); return port; } shapeIn({ name }: { name: string }): Port { const port = new Port(this, name, PortType.Shape); this.inputPorts.push(port); return port; } specIn({ name }: { name: string }): Port { const port = new Port(this, name, PortType.Spec); this.inputPorts.push(port); return port; } tableOut({ name }: { name: string }): Port { const port = new Port(this, name, PortType.Table); this.outputPorts.push(port); return port; } shapeOut({ name }: { name: string }): Port { const port = new Port(this, name, PortType.Shape); this.outputPorts.push(port); return port; } specOut({ name }: { name: string }): Port { const port = new Port(this, name, PortType.Spec); this.outputPorts.push(port); return port; } pushSection({ name, collapsed = false }: { name: string; collapsed: boolean }): void { if (this._currentSection) { throw new Error(`Section ${this._currentSection} is not closed`); } this.sections.push({ name, collapsed }); this._currentSection = name; } popSection() { this._currentSection = undefined; } onRender(_cx: Context) { throw new Error("Not implemented"); } onChange(_cx: Context, _parameterName: string) {} }