From ac83dd4a08bdbab6df270d6dae8d9e2d6d619342 Mon Sep 17 00:00:00 2001 From: Sam Nystrom Date: Wed, 6 Mar 2024 20:52:45 +0000 Subject: init --- src/AddNodeMenu.tsx | 21 ++++ src/NodeEditor.module.css | 5 + src/NodeEditor.tsx | 245 +++++++++++++++++++++++++++++++++++++++++++++ src/assets/down-arrow.svg | 3 + src/assets/right-arrow.svg | 3 + src/cssmodules.d.ts | 4 + src/dataflow.ts | 19 ++++ src/index.css | 28 ++++++ src/index.tsx | 13 +++ src/node.module.css | 145 +++++++++++++++++++++++++++ src/node.tsx | 208 ++++++++++++++++++++++++++++++++++++++ src/nodes/CombineXYZ.tsx | 30 ++++++ src/nodes/Fourier.tsx | 25 +++++ src/nodes/Intersperse.tsx | 27 +++++ src/nodes/Linspace.tsx | 29 ++++++ src/nodes/Math.tsx | 157 +++++++++++++++++++++++++++++ src/nodes/Plot.tsx | 35 +++++++ src/nodes/SeparateXYZ.tsx | 30 ++++++ src/nodes/Viewer.tsx | 28 ++++++ src/nodes/index.ts | 21 ++++ src/util.ts | 2 + src/wasm.ts | 9 ++ 22 files changed, 1087 insertions(+) create mode 100644 src/AddNodeMenu.tsx create mode 100644 src/NodeEditor.module.css create mode 100644 src/NodeEditor.tsx create mode 100644 src/assets/down-arrow.svg create mode 100644 src/assets/right-arrow.svg create mode 100644 src/cssmodules.d.ts create mode 100644 src/dataflow.ts create mode 100644 src/index.css create mode 100644 src/index.tsx create mode 100644 src/node.module.css create mode 100644 src/node.tsx create mode 100644 src/nodes/CombineXYZ.tsx create mode 100644 src/nodes/Fourier.tsx create mode 100644 src/nodes/Intersperse.tsx create mode 100644 src/nodes/Linspace.tsx create mode 100644 src/nodes/Math.tsx create mode 100644 src/nodes/Plot.tsx create mode 100644 src/nodes/SeparateXYZ.tsx create mode 100644 src/nodes/Viewer.tsx create mode 100644 src/nodes/index.ts create mode 100644 src/util.ts create mode 100644 src/wasm.ts (limited to 'src') diff --git a/src/AddNodeMenu.tsx b/src/AddNodeMenu.tsx new file mode 100644 index 0000000..197f56d --- /dev/null +++ b/src/AddNodeMenu.tsx @@ -0,0 +1,21 @@ +import { useId } from 'preact/hooks'; +import { NodeInfo } from './node.tsx'; + +export interface AddNodeMenuProps { + nodes: Record; + onClick?: (NodeInfo) => void; +} + +export const AddNodeMenu = ({ nodes, onClick = _ => {} }: AddNodeMenuProps) => { + const id = useId(); + return ( + <> + + {Object.entries(nodes).map(([name, node]) => ( +
  • + ))} +
    + + + ); +}; \ No newline at end of file diff --git a/src/NodeEditor.module.css b/src/NodeEditor.module.css new file mode 100644 index 0000000..caa549e --- /dev/null +++ b/src/NodeEditor.module.css @@ -0,0 +1,5 @@ +.link { + fill: none; + stroke: var(--data-float); + stroke-width: 2; +} \ No newline at end of file diff --git a/src/NodeEditor.tsx b/src/NodeEditor.tsx new file mode 100644 index 0000000..83d81bd --- /dev/null +++ b/src/NodeEditor.tsx @@ -0,0 +1,245 @@ +import { useMemo, useRef } from 'preact/hooks'; +import { signal, computed, batch, useSignal, useComputed, Signal } from '@preact/signals'; +import { nodeRegistry } from './nodes'; +import { SocketHandlers, SocketHandler, NodeInfo } from './node.tsx'; +import { InputSocket } from './dataflow.ts'; +import { AddNodeMenu } from './AddNodeMenu.tsx'; +import styles from './NodeEditor.module.css'; + +export const nodeFactory = () => { + let nextNodeId = 0; + return (x: number, y: number, { component, func, inputs }: NodeInfo) => { + const mapEntries = (obj: {}, f: (x: [string, any]) => [string, any]) => ( + Object.fromEntries(Object.entries(obj).map(f)) + ); + const instanceInputs = mapEntries(inputs, ([k, v]) => [k, new InputSocket(v)]); + const output = computed(() => func(mapEntries(instanceInputs, ([k, v]) => [k, v.value]))); + return { + id: nextNodeId++, + component, + x: signal(x), + y: signal(y), + inputs: instanceInputs, + outputs: mapEntries(output.value, ([k, _]) => [k, computed(() => output.value[k])]), + }; + }; +}; + +interface LinkProps { + fromX: Signal; + fromY: Signal; + toX: Signal; + toY: Signal; +} + +const Link = ({ fromX, fromY, toX, toY }: LinkProps) => { + const c1x = fromX.value + Math.abs(toX.value - fromX.value) / 3; + const c2x = toX.value - Math.abs(toX.value - fromX.value) / 3; + return ( + + ); +}; + +const getPos = (elem: Element) => { + const rect = elem.getBoundingClientRect(); + const x = rect.x + (rect.right - rect.x) / 2; + const y = rect.y + (rect.bottom - rect.y) / 2; + return {x, y}; +}; + +interface LinkData extends LinkProps { + from: { nodeId: number, socket: string }; + to: { nodeId: number, socket: string }; +} + +export const NodeEditor = () => { + const offsetX = useSignal(0); + const offsetY = useSignal(0); + const scale = useSignal(1); + + const instantiateNode = useMemo(nodeFactory, []); + const svgRef = useRef(null); + + const nodes = useSignal([ + instantiateNode(100, 100, nodeRegistry['Linspace']), + instantiateNode(350, 200, nodeRegistry['Math']), + instantiateNode(350, 50, nodeRegistry['Intersperse']), + instantiateNode(600, 100, nodeRegistry['Fourier Transform']), + instantiateNode(900, 100, nodeRegistry['Viewer']), + instantiateNode(900, 250, nodeRegistry['Plot']), + ]); + + const currentLink = useSignal>(null); + const links = useSignal([]); + const allLinks = useComputed(() => (links.value as LinkProps[]).concat(currentLink.value as LinkProps ?? [])); + + const onOutMouseDown: SocketHandler = (nodeId, socket, event) => { + event.stopPropagation(); + const svgRect = svgRef.current?.getBoundingClientRect(); + const svgX = svgRect?.x ?? 0; + const svgY = svgRect?.y ?? 0; + const pos = getPos(event.target as Element); + pos.x -= svgX; + pos.y -= svgY; + const node = nodes.value.find(x => x.id === nodeId); + if (!node) throw new Error(); + + const xOffs = (pos.x - offsetX.value) / scale.value - node.x.value; + const yOffs = (pos.y - offsetY.value) / scale.value - node.y.value; + + const fromX = computed(() => node.x.value + xOffs); + const fromY = computed(() => node.y.value + yOffs); + + const mouseX = signal(event.clientX); + const mouseY = signal(event.clientY); + const toX = computed(() => (mouseX.value - svgX - offsetX.value) / scale.value); + const toY = computed(() => (mouseY.value - svgY - offsetY.value) / scale.value); + + const onMouseMove = (event: MouseEvent) => batch(() => { + mouseX.value += event.movementX; + mouseY.value += event.movementY; + }); + + const onMouseUp = () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + currentLink.value = null; + }; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + + currentLink.value = {from: {nodeId, socket}, fromX, fromY, toX, toY}; + }; + + const onInMouseDown: SocketHandler = (nodeId, socket, event) => { + event.stopPropagation(); + const i = links.value.findIndex(l => l.to.nodeId === nodeId && l.to.socket === socket); + if (i == -1) return; + const node = nodes.value.find(x => x.id === nodeId); + if (!node) throw new Error(); + + const svgRect = svgRef.current?.getBoundingClientRect(); + const svgX = svgRect?.x ?? 0; + const svgY = svgRect?.y ?? 0; + + const mouseX = signal(event.clientX); + const mouseY = signal(event.clientY); + const toX = computed(() => (mouseX.value - svgX - offsetX.value) / scale.value); + const toY = computed(() => (mouseY.value - svgY - offsetY.value) / scale.value); + + batch(() => { + node.inputs[socket].link.value = null; + currentLink.value = {...links.value[i], toX, toY}; + links.value = links.value.toSpliced(i, 1); + }); + + const onMouseMove = (event: MouseEvent) => batch(() => { + mouseX.value += event.movementX; + mouseY.value += event.movementY; + }); + + const onMouseUp = () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + currentLink.value = null; + }; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + }; + + const onInMouseUp: SocketHandler = (nodeId, socket, event) => { + if (!currentLink.value) return; + event.stopPropagation(); + const fromNode = nodes.value.find(x => x.id === currentLink.value!.from.nodeId); + const node = nodes.value.find(x => x.id === nodeId); + if (!node || !fromNode) throw new Error(); + + const svgRect = svgRef.current?.getBoundingClientRect(); + const svgX = svgRect?.x ?? 0; + const svgY = svgRect?.y ?? 0; + const pos = getPos(event.target as Element); + pos.x -= svgX; + pos.y -= svgY; + + const xOffs = (pos.x - offsetX.value) / scale.value - node.x.value; + const yOffs = (pos.y - offsetY.value) / scale.value - node.y.value; + + const toX = computed(() => node.x.value + xOffs); + const toY = computed(() => node.y.value + yOffs); + + batch(() => { + node.inputs[socket].link.value = fromNode.outputs[currentLink.value!.from.socket]; + links.value = [ + ...links.value.filter(l => l.to.nodeId !== nodeId || l.to.socket !== socket), + {...currentLink.value!, to: {nodeId, socket}, toX, toY}, + ]; + currentLink.value = null; + }); + }; + + const socketHandlers = { + onOutMouseDown, + onInMouseDown, + onInMouseUp, + }; + + const onBgMouseDown = () => { + const onMouseMove = (event: MouseEvent) => batch(() => { + offsetX.value += event.movementX; + offsetY.value += event.movementY; + }); + + const onMouseUp = () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + }; + + const onBgWheel = (event: WheelEvent) => batch(() => { + const delta = event.deltaY * 0.001; + offsetX.value -= (event.clientX - offsetX.value) * delta; + offsetY.value -= (event.clientY - offsetY.value) * delta; + scale.value *= 1 + delta; + }); + + return ( + <> + nodes.value = nodes.value.concat(instantiateNode(100, 100, node))} /> + + + + + + + + + + + + + {allLinks.value.map(({fromX, fromY, toX, toY}) => ( + + ))} + + {nodes.value.map(node => ( + + ))} + + + + + ); +}; \ No newline at end of file diff --git a/src/assets/down-arrow.svg b/src/assets/down-arrow.svg new file mode 100644 index 0000000..eefe5ef --- /dev/null +++ b/src/assets/down-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/right-arrow.svg b/src/assets/right-arrow.svg new file mode 100644 index 0000000..7ac9997 --- /dev/null +++ b/src/assets/right-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/cssmodules.d.ts b/src/cssmodules.d.ts new file mode 100644 index 0000000..b6dc193 --- /dev/null +++ b/src/cssmodules.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: Record; + export default content; +} \ No newline at end of file diff --git a/src/dataflow.ts b/src/dataflow.ts new file mode 100644 index 0000000..ffbab5d --- /dev/null +++ b/src/dataflow.ts @@ -0,0 +1,19 @@ +import { signal, Signal } from '@preact/signals'; + +export class InputSocket { + link: Signal>; + manual: Signal; + + constructor(initialValue: T) { + this.link = signal(null); + this.manual = signal(initialValue); + } + + get value() { + return this.link.value ? this.link.value.value : this.manual.value; + } + + set value(x: T) { + this.manual.value = x; + } +} \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..aac86b5 --- /dev/null +++ b/src/index.css @@ -0,0 +1,28 @@ +/*@font-face { + font-family: 'Inter'; + src: url('Inter.ttf'); +}*/ + +:root { + --base: #121212; + --surface: #242424; + --overlay: #484848; + --text: #ffffff; + --primary: #1c4c87; + + --data-any: #36c965; + --data-float: #a0a0a0; + --data-vector: #5e29d9; + + font-family: 'Inter', Helvetica, sans-serif; + box-sizing: border-box; +} + +body { + background-color: var(--base); + color: var(--text); + margin: 0; + width: 100vw; + height: 100vh; + overflow: hidden; +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..ea83e21 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,13 @@ +import { render } from 'preact'; +import { useErrorBoundary } from 'preact/hooks'; +import { NodeEditor } from './NodeEditor.tsx'; +import './index.css'; + +export const App = () => { + const [error] = useErrorBoundary(error => alert(error)); + return ( + + ); +}; + +render(, document.body); diff --git a/src/node.module.css b/src/node.module.css new file mode 100644 index 0000000..bfd5fb1 --- /dev/null +++ b/src/node.module.css @@ -0,0 +1,145 @@ +.foreignObject { + width: 0; + height: 0; + overflow: visible; +} + +.node { + background-color: var(--surface); + min-width: 180px; + font-size: 0.9rem; + margin: 4px; + padding-bottom: 8px; + display: flex; + flex-direction: column; + border-radius: 6px; + user-select: none; + --shadow-color: 0deg 0% 0%; + box-shadow: + 0.1px 0.4px 0.6px hsl(var(--shadow-color) / 0.04), + 0.5px 3.3px 4.8px -0.2px hsl(var(--shadow-color) / 0.15), + 1.4px 10.2px 14.7px -0.5px hsl(var(--shadow-color) / 0.27); + + &:focus-within { + outline: 1px white solid; + } + + input[type="number"] { + background-color: var(--overlay); + border: none; + border-radius: 4px; + outline: none; + color: var(--text); + font-size: 1em; + text-align: right; + width: 100%; + margin-right: 12px; + padding-top: 3px; + padding-bottom: 2px; + } + + summary { + background-color: var(--primary); + padding: 3px; + padding-left: 6px; + margin-bottom: 4px; + border-radius: 6px 6px 0 0; + list-style-image: url('assets/down-arrow.svg'); + } + &:not([open]) summary { + list-style-image: url('assets/right-arrow.svg'); + } + + ul { + list-style: none; + padding: 0; + margin: 0; + } + + ul li { + display: flex; + flex-direction: row; + align-items: center; + padding: 4px 0; + + circle { + fill: var(--data-float); + .number & { fill: var(--data-float) } + .vector & { fill: var(--data-vector) } + .select & { fill: var(--data-float) } + } + + &.out + &.in { + padding-top: 8px; + } + + &.out { + justify-content: flex-end; + + svg { + position: relative; + left: 4px; + } + } + + &.in { + justify-content: flex-start; + + svg { + position: relative; + right: 4px; + } + + &.number { + + .number { padding-top: 0 } + &.linked input { display: none } + + span { + position: relative; + left: 8px; + width: 0; + padding-bottom: 2px; + } + } + + &.vector { + flex-direction: column; + align-items: flex-start; + gap: 1px; + + & :first-child { + display: flex; + flex-direction: row; + align-items: center; + gap: 2px; + margin-bottom: 4px; + .linked & { margin-bottom: 0 } + } + + input { + width: 89%; + margin-left: 8px; + margin-right: 16px; + + &:nth-child(2) { border-radius: 4px 4px 0 0 } + &:nth-child(3) { border-radius: 0 } + &:nth-child(4) { border-radius: 0 0 4px 4px } + + .linked & { display: none } + } + } + + &.select select { + background-color: color-mix(in srgb, var(--base) 50%, var(--surface)); + color: var(--text); + border: 1px solid color-mix(in srgb, var(--overlay) 50%, var(--surface)); + border-radius: 4px; + width: 100%; + margin-right: 12px; + padding: 3px 1px; + + .linked & { display: none } + } + } + } +} \ No newline at end of file diff --git a/src/node.tsx b/src/node.tsx new file mode 100644 index 0000000..211398e --- /dev/null +++ b/src/node.tsx @@ -0,0 +1,208 @@ +import { createContext, FunctionComponent, ComponentChildren } from 'preact'; +import { useContext } from 'preact/hooks'; +import { batch, Signal } from '@preact/signals'; +import { InputSocket } from './dataflow.ts'; +import { cls } from './util.ts'; +import styles from './node.module.css'; + +export type SocketHandler = (nodeId: number, socket: string, event: MouseEvent) => void; +export interface SocketHandlers { + onOutMouseDown?: SocketHandler; + onOutMouseUp?: SocketHandler; + onInMouseDown?: SocketHandler; + onInMouseUp?: SocketHandler; +} + +export const SocketHandlers = createContext({}); +export const NodeId = createContext(0); + +export interface NodeComponentProps { + id: number; + x: Signal; + y: Signal; + inputs: { [Property in keyof I]: InputSocket }; +} +export type NodeComponent = FunctionComponent>; + +export interface NodeInfo { + component: NodeComponent; + func: (inputs: I) => O; + inputs: I; +} + +interface SocketProps { + name: string; + onMouseDown?: SocketHandler; + onMouseUp?: SocketHandler; +} + +const Socket = ({ name, onMouseDown, onMouseUp }: SocketProps) => { + const nodeId = useContext(NodeId); + const wrap = (func?: SocketHandler) => (event: MouseEvent) => func && func(nodeId, name, event); + return ( + + + + ); +}; + +const InSocket = ({ name }: { name: string }) => { + const handlers = useContext(SocketHandlers); + return ( + + ); +}; + +const OutSocket = ({ name }: { name: string }) => { + const handlers = useContext(SocketHandlers); + return ( + + ); +}; + +export interface InputProps { + name: string; + label: string; + value: InputSocket; +} + +export const InputAny = ({ name, label }: Omit, 'value'>) => { + return ( +
  • + + {label} +
  • + ); +}; + +export const InputArray = ({ name, label }: Omit, 'value'>) => { + return ( +
  • + + {label} +
  • + ); +}; + +const InputNum = (parseFunc: (string) => number) => ({ name, label, value }: InputProps) => { + const onInput = (event: InputEvent) => { + value.value = parseFunc((event.target as HTMLInputElement).value); + } + return ( +
  • + + {label} + +
  • + ); +}; + +export const InputNumber = InputNum(parseFloat); +export const InputInteger = InputNum(parseInt); + +export const InputVector = ({ name, label, value }: InputProps<[number, number, number]>) => { + const onInput = (i: 0 | 1 | 2) => (event: InputEvent) => { + const newValue: [number, number, number] = [...value.value]; + newValue[i] = parseFloat((event.target as HTMLInputElement).value); + value.value = newValue; + }; + return ( +
  • +
    + + {label} +
    + + + +
  • + ); +}; + +export interface InputSelectProps extends InputProps { + options: string[] | Record; +} + +export const InputSelect = ({ name, label, value, options }: InputSelectProps) => { + const onChange = (event: InputEvent) => { + value.value = (event.target as HTMLSelectElement).value; + } + return ( +
  • + + +
  • + ); +}; + +export interface OutputProps { + name: string; + label: string; +} + +const Output = ({ name, label, type }: OutputProps & { type: string }) => { + return ( +
  • + {label} + +
  • + ); +}; + +export const OutputNumber = (props: OutputProps) => ; +export const OutputVector = (props: OutputProps) => ; + +export interface NodeShellProps { + id: number; + name: string; + x: Signal; + y: Signal; + children: ComponentChildren; +} + +export const NodeShell = ({ id, name, x, y, children }: NodeShellProps) => { + const onMouseDown = (event: MouseEvent) => { + event.stopPropagation(); + + const onMouseMove = (event: MouseEvent) => batch(() => { + x.value += event.movementX; + y.value += event.movementY; + }); + + const onMouseUp = () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + }; + + return ( + +
    + e.stopPropagation()}>{name} +
      + + {children} + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/nodes/CombineXYZ.tsx b/src/nodes/CombineXYZ.tsx new file mode 100644 index 0000000..99823fd --- /dev/null +++ b/src/nodes/CombineXYZ.tsx @@ -0,0 +1,30 @@ +import { NodeShell, InputNumber, OutputVector, NodeComponentProps, NodeInfo } from '../node.tsx'; + +export interface CombineXYZInputs { + x: number; + y: number; + z: number; +} + +export interface CombineXYZOutputs { + vector: [number, number, number]; +} + +export type CombineXYZProps = NodeComponentProps; + +export const CombineXYZ = ({ id, x, y, inputs }: CombineXYZProps) => { + return ( + + + + + + + ); +}; + +export const CombineXYZNode: NodeInfo = { + component: CombineXYZ, + func: ({ x, y, z }) => ({ vector: [x, y, z] }), + inputs: { x: 0, y: 0, z: 0 }, +}; \ No newline at end of file diff --git a/src/nodes/Fourier.tsx b/src/nodes/Fourier.tsx new file mode 100644 index 0000000..3763402 --- /dev/null +++ b/src/nodes/Fourier.tsx @@ -0,0 +1,25 @@ +import { NodeShell, InputArray, OutputNumber, NodeComponentProps, NodeInfo } from '../node.tsx'; +import { fft } from '../wasm.ts'; + +export interface FourierInputs { + data: Float64Array, +} + +export interface FourierOutputs { + data: Float64Array, +} + +export const Fourier = ({ id, x, y, inputs }: NodeComponentProps) => { + return ( + + + + + ); +}; + +export const FourierNode: NodeInfo = { + component: Fourier, + func: ({ data }) => ({ data: fft(data) }), + inputs: { data: new Float64Array(0) }, +}; \ No newline at end of file diff --git a/src/nodes/Intersperse.tsx b/src/nodes/Intersperse.tsx new file mode 100644 index 0000000..7487a84 --- /dev/null +++ b/src/nodes/Intersperse.tsx @@ -0,0 +1,27 @@ +import { NodeShell, InputArray, OutputNumber, NodeComponentProps, NodeInfo } from '../node.tsx'; +import { intersperse } from '../wasm.ts'; + +export interface IntersperseInputs { + a: Float64Array, + b: Float64Array, +} + +export interface IntersperseOutputs { + out: Float64Array, +} + +export const Intersperse = ({ id, x, y, inputs }: NodeComponentProps) => { + return ( + + + + + + ); +}; + +export const IntersperseNode: NodeInfo = { + component: Intersperse, + func: ({ a, b }) => ({ out: a && b ? intersperse(a, b) : null }), + inputs: { a: null, b: null }, +}; \ No newline at end of file diff --git a/src/nodes/Linspace.tsx b/src/nodes/Linspace.tsx new file mode 100644 index 0000000..16d8ba6 --- /dev/null +++ b/src/nodes/Linspace.tsx @@ -0,0 +1,29 @@ +import { NodeShell, InputNumber, InputInteger, OutputNumber, NodeComponentProps, NodeInfo } from '../node.tsx'; +import { linspace } from '../wasm.ts'; + +export interface LinspaceInputs { + start: number, + stop: number, + n: number, +} + +export interface LinspaceOutputs { + data: Float64Array, +} + +export const Linspace = ({ id, x, y, inputs }: NodeComponentProps) => { + return ( + + + + + + + ); +}; + +export const LinspaceNode: NodeInfo = { + component: Linspace, + func: ({ start, stop, n }) => ({ data: linspace(start, stop, Math.floor(n)) }), + inputs: { start: 0, stop: 1, n: 10 }, +}; \ No newline at end of file diff --git a/src/nodes/Math.tsx b/src/nodes/Math.tsx new file mode 100644 index 0000000..e513538 --- /dev/null +++ b/src/nodes/Math.tsx @@ -0,0 +1,157 @@ +import { NodeShell, InputNumber, InputSelect, OutputNumber, NodeComponentProps, NodeInfo } from '../node.tsx'; + +export enum MathOpFunc { + Add = 'Add', + Sub = 'Subtract', + Mul = 'Multiply', + Div = 'Divide', + Power = 'Power', + Log = 'Logarithm', + Sqrt = 'Square Root', + Exp = 'Exponent', +} +export enum MathOpCmp { + Min = 'Minimum', + Max = 'Maximum', + Less = 'Less Than', + Greater = 'Greater Than', + Sign = 'Sign', +} +export enum MathOpRound { + Round = 'Round', + Floor = 'Floor', + Ceil = 'Ceiling', + Trunc = 'Truncate', + Frac = 'Fraction', + Mod = 'Modulo', + Snap = 'Snap', + Clamp = 'Clamp', +} +export enum MathOpTrig { + Sin = 'Sine', + Cos = 'Cosine', + Tan = 'Tangent', + Asin = 'Arcsine', + Acos = 'Arccosine', + Atan = 'Arctangent', + Atan2 = 'Arctan2', + + Sinh = 'Hyperbolic Sine', + Cosh = 'Hyperbolic Cosine', + Tanh = 'Hyperbolic Tangent', +} +export enum MathOpConv { + ToRad = 'To Radians', + ToDeg = 'To Degrees', +} + +export const MathOp = { ...MathOpFunc, ...MathOpCmp, ...MathOpRound, ...MathOpTrig, ...MathOpConv }; +export type MathOp = typeof MathOp; + +export interface MathInputs { + op: MathOp, + a: number | TypedArray, + b: number | TypedArray, +} + +export interface MathOutputs { + out: boolean | number | TypedArray, +} + +export const MathC = ({ id, x, y, inputs }: NodeComponentProps) => { + const options = { + 'Functions': Object.values(MathOpFunc), + 'Comparison': Object.values(MathOpCmp), + 'Rounding': Object.values(MathOpRound), + 'Trigonometric': Object.values(MathOpTrig), + 'Conversion': Object.values(MathOpConv), + }; + return ( + + + + + + + ); +}; + +const doMathOp = (op: MathOp, a: number, b: number): number => { + switch (op) { + case MathOp.Add: return a + b; + case MathOp.Sub: return a - b; + case MathOp.Mul: return a * b; + case MathOp.Div: return a / b; + case MathOp.Power: return a ** b; + case MathOp.Log: return Math.log(a) / Math.log(b); + case MathOp.Sqrt: return Math.sqrt(a); + case MathOp.Exp: return Math.exp(a); + + case MathOp.Min: return Math.min(a, b); + case MathOp.Max: return Math.max(a, b); + case MathOp.Less: return a < b; + case MathOp.Greater: return a > b; + case MathOp.Sign: return Math.sign(a); + + case MathOp.Round: return Math.round(a); + case MathOp.Floor: return Math.floor(a); + case MathOp.Ceil: return Math.ceil(a); + case MathOp.Trunc: return Math.trunc(a); + case MathOp.Frac: return a - Math.trunc(a); + case MathOp.Mod: return a % b; + case MathOp.Snap: return Math.round(a * b) / b; + case MathOp.Clamp: return Math.max(0, Math.min(a, 1)); + + case MathOp.Sin: return Math.sin(a); + case MathOp.Cos: return Math.cos(a); + case MathOp.Tan: return Math.tan(a); + case MathOp.Asin: return Math.asin(a); + case MathOp.Acos: return Math.acos(a); + case MathOp.Atan: return Math.atan(a); + case MathOp.Atan2: return Math.atan2(a, b); + + case MathOp.Sinh: return Math.sinh(a); + case MathOp.Cosh: return Math.cosh(a); + case MathOp.Tanh: return Math.tanh(a); + + case MathOp.ToRad: return a / 180 * Math.PI; + case MathOp.ToDeg: return a * 180 / Math.PI; + + default: throw new TypeError(); + } +}; + +const mathFunc = ({ op, a, b }: MathInputs): MathOutputs => { + const countScalar = (typeof a === 'number' ? 1 : 0) + (typeof b === 'number' ? 1 : 0); + if (typeof a === 'number') { + if (typeof b === 'number') { + return { out: doMathOp(op, a, b) }; + } else { + const out = new Float64Array(b.length); + for (let i = 0; i < out.length; i++) { + out[i] = doMathOp(op, a, b[i]); + } + return { out }; + } + } else { + if (typeof b === 'number') { + const out = new Float64Array(a.length); + for (let i = 0; i < out.length; i++) { + out[i] = doMathOp(op, a[i], b); + } + return { out }; + } else { + const out = new Float64Array(Math.min(a.length, b.length)); + for (let i = 0; i < out.length; i++) { + out[i] = doMathOp(op, a[i], b[i]); + } + return { out }; + } + } +}; + +export const MathNode: NodeInfo = { + component: MathC, + func: mathFunc, + inputs: { op: MathOp.Add, a: 0, b: 0 }, +}; diff --git a/src/nodes/Plot.tsx b/src/nodes/Plot.tsx new file mode 100644 index 0000000..dca3219 --- /dev/null +++ b/src/nodes/Plot.tsx @@ -0,0 +1,35 @@ +import { NodeShell, InputAny, NodeComponentProps, NodeInfo } from '../node.tsx'; + +export interface PlotInputs { + data: any; +} + +export interface PlotOutputs {} + +export const Plot = ({ id, x, y, inputs }: NodeComponentProps) => { + const data = inputs.data.value; + const scale = 100; + const dx = 0; + const dy = 0; + let path = ''; + if (data !== null && data.length > 3) { + for (let i = 0; i < data.length - data.length % 2; i += 2) { + path += (i ? 'L' : 'M') + (data[i] * scale + dx) + ' ' + (data[i+1] * scale + dy); + } + } + //alert(path); + return ( + + + + + + + ); +}; + +export const PlotNode: NodeInfo = { + component: Plot, + func: () => ({}), + inputs: { data: null }, +}; \ No newline at end of file diff --git a/src/nodes/SeparateXYZ.tsx b/src/nodes/SeparateXYZ.tsx new file mode 100644 index 0000000..bc14be1 --- /dev/null +++ b/src/nodes/SeparateXYZ.tsx @@ -0,0 +1,30 @@ +import { NodeShell, InputVector, OutputNumber, NodeComponentProps, NodeInfo } from '../node.tsx'; + +export interface SeparateXYZInputs { + vector: [number, number, number]; +} + +export interface SeparateXYZOutputs { + x: number; + y: number; + z: number; +} + +export type SeparateXYZProps = NodeComponentProps; + +export const SeparateXYZ = ({ id, x, y, inputs }: SeparateXYZProps) => { + return ( + + + + + + + ); +}; + +export const SeparateXYZNode: NodeInfo = { + component: SeparateXYZ, + func: ({ vector }) => ({ x: vector[0], y: vector[1], z: vector[2] }), + inputs: { vector: [0, 0, 0] }, +}; \ No newline at end of file diff --git a/src/nodes/Viewer.tsx b/src/nodes/Viewer.tsx new file mode 100644 index 0000000..addd0ab --- /dev/null +++ b/src/nodes/Viewer.tsx @@ -0,0 +1,28 @@ +import { NodeShell, InputAny, NodeComponentProps, NodeInfo } from '../node.tsx'; + +export interface ViewerInputs { + value: any; +} + +export interface ViewerOutputs {} + +export const Viewer = ({ id, x, y, inputs }: NodeComponentProps) => { + let data = inputs.value.value; + if (ArrayBuffer.isView(data)) { + data = Array.from(data); + } + return ( + + +
    +				{JSON.stringify(data)}
    +			
    +
    + ); +}; + +export const ViewerNode: NodeInfo = { + component: Viewer, + func: () => ({}), + inputs: { value: null }, +}; \ No newline at end of file diff --git a/src/nodes/index.ts b/src/nodes/index.ts new file mode 100644 index 0000000..0e63d3a --- /dev/null +++ b/src/nodes/index.ts @@ -0,0 +1,21 @@ +import { CombineXYZNode } from './CombineXYZ.tsx'; +import { SeparateXYZNode } from './SeparateXYZ.tsx'; +import { ViewerNode } from './Viewer.tsx'; +import { FourierNode } from './Fourier.tsx'; +import { LinspaceNode } from './Linspace.tsx'; +import { IntersperseNode } from './Intersperse.tsx'; +import { MathNode } from './Math.tsx'; +import { PlotNode } from './Plot.tsx'; + +const nodeRegistry: Record> = { + 'Combine XYZ': CombineXYZNode, + 'Separate XYZ': SeparateXYZNode, + 'Viewer': ViewerNode, + 'Fourier Transform': FourierNode, + 'Linspace': LinspaceNode, + 'Intersperse': IntersperseNode, + 'Math': MathNode, + 'Plot': PlotNode, +} + +export { nodeRegistry }; \ No newline at end of file diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..9f84ffe --- /dev/null +++ b/src/util.ts @@ -0,0 +1,2 @@ +export const cls = (...classes: Array) => + classes.filter(c => c).join(' '); \ No newline at end of file diff --git a/src/wasm.ts b/src/wasm.ts new file mode 100644 index 0000000..3a308b2 --- /dev/null +++ b/src/wasm.ts @@ -0,0 +1,9 @@ +import { instantiate } from '../build/out.js'; +import url from '../build/out.wasm'; +export const { + memory, + linspace, + intersperse, + dft, + fft, +} = await instantiate(await WebAssembly.compileStreaming(fetch(url))); \ No newline at end of file -- cgit v1.2.3