@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
175 lines (159 loc) • 5.56 kB
text/typescript
import React, { useEffect, useState, useRef } from "react";
import { loadMainProject, config } from "./loaders";
import Context from "./context";
import { Item, Network, Node, LiteralValue } from "./types";
import { renderShape, renderDefs } from "./render";
import { evaluateItem } from "./evaluation";
import { Context as GraphicsContext, Paint, Shape } from "@ndbx/g";
import { markItemParameterDirty } from "./mutation";
import { Spec } from "vega";
import { vegaToShape } from "./vega-to-shape";
function isVegaSpec(value: unknown): value is Spec {
return typeof value === "object" && value !== null && "$schema" in (value as Record<string, unknown>);
}
interface PlayerProps {
userId: string;
projectId: string;
version?: string;
item?: string;
values?: Record<string, LiteralValue>;
apiRoot?: string;
publishedUrlTemplate?: string;
assetsUrlTemplate?: string;
libUrlTemplate?: string;
onProjectLoaded?: (context: Context) => void;
onProjectError?: (message: string) => void;
}
const NodeBoxPlayer: React.FC<PlayerProps> = ({
userId,
projectId,
version,
item,
values,
apiRoot,
publishedUrlTemplate,
assetsUrlTemplate,
libUrlTemplate,
onProjectLoaded,
onProjectError,
}) => {
const contextRef = useRef<Context | undefined>();
const [activeItem, setActiveItem] = useState<Item | undefined>();
const [result, setResult] = useState<Shape | undefined>();
useEffect(() => {
(async () => {
// Apply custom configuration values if provided
if (apiRoot !== undefined) config.apiRoot = apiRoot;
if (publishedUrlTemplate !== undefined) config.publishedUrlTemplate = publishedUrlTemplate;
if (assetsUrlTemplate !== undefined) config.assetsUrlTemplate = assetsUrlTemplate;
if (libUrlTemplate !== undefined) config.libUrlTemplate = libUrlTemplate;
let cx;
try {
cx = await loadMainProject(userId, projectId, version || "published");
contextRef.current = cx;
} catch (e) {
if (onProjectError) {
const errorMessage = e instanceof Error ? e.message : String(e);
onProjectError(errorMessage);
return;
} else {
throw e;
}
}
let projectItem;
if (!item) {
projectItem = cx.project.items[0];
} else {
projectItem = cx.project.items.find((i) => i.name === item) as Item;
}
if (!projectItem) {
if (onProjectError) {
onProjectError(`Item not found: ${item}`);
return;
} else {
throw new Error(`Item not found: ${item}`);
}
}
setActiveItem(projectItem);
if (onProjectLoaded) {
onProjectLoaded(cx);
}
})();
}, [userId, projectId, item, version, apiRoot, publishedUrlTemplate, assetsUrlTemplate, libUrlTemplate]);
useEffect(() => {
(async () => {
const cx = contextRef.current;
if (!cx || !activeItem) return;
if (activeItem.type === "NETWORK") {
const network = activeItem as Network;
if (values) {
for (const [key, value] of Object.entries(values)) {
markItemParameterDirty(cx, network, key);
}
}
}
const fqId = `self/self/${item}`;
await evaluateItem(cx, fqId, activeItem, values);
if (activeItem.type === "NETWORK") {
const network = activeItem as Network;
if (!network.outputPorts || network.outputPorts.length === 0) {
const renderedNode = network.children.find((node) => node.id === network.renderedNode)! as Node;
if (!renderedNode) {
const errorMessage = `No rendered node found in network ${network.name}.`;
console.error(errorMessage);
onProjectError && onProjectError(errorMessage);
return;
}
const renderedNodeFn = cx.lookupItemByName(renderedNode.fn);
if (renderedNodeFn) {
const outputs = renderedNodeFn.outputPorts.map((port) =>
cx.portValues.get(`${renderedNode.id}/${port.name}`),
);
if (outputs.length > 0) setResult(outputs[0] as Shape);
}
} else {
const outputs = network.outputPorts.map((port) => cx.portValues.get(`${network.id}/${port.name}`));
if (outputs.length > 0) setResult(outputs[0] as Shape);
}
}
})();
}, [activeItem, values, onProjectError]);
let shape = null;
let shapeElement = null;
let defsElement = null;
let graphicsContext = new GraphicsContext();
if (result) {
shape = result;
if (isVegaSpec(result)) {
shape = vegaToShape(result);
} else {
shape = result;
}
try {
shapeElement = renderShape(shape, graphicsContext);
defsElement = renderDefs(graphicsContext);
} catch (e) {
console.error(e);
}
}
if (!activeItem) {
return React.createElement("div", {}, "loading....");
}
const style: React.CSSProperties = {};
if (activeItem.background) {
style.backgroundColor = Paint.parse(activeItem.background).toString();
}
let width = activeItem.width ?? 1000;
let height = activeItem.height ?? 1000;
if ((activeItem as Network).canvasSize === "auto" && shape) {
const bounds = shape.getBounds();
width = bounds.right - bounds.left;
height = bounds.bottom - bounds.top;
}
return React.createElement(
"div",
{ className: "ndbx-wrapper" },
React.createElement("svg", { width, height, style }, shapeElement, defsElement),
);
};
export default NodeBoxPlayer;