summaryrefslogtreecommitdiff
path: root/src/node.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/node.tsx')
-rw-r--r--src/node.tsx208
1 files changed, 208 insertions, 0 deletions
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<SocketHandlers>({});
+export const NodeId = createContext<number>(0);
+
+export interface NodeComponentProps<I extends {}> {
+ id: number;
+ x: Signal<number>;
+ y: Signal<number>;
+ inputs: { [Property in keyof I]: InputSocket<I[Property]> };
+}
+export type NodeComponent<I extends {}> = FunctionComponent<NodeComponentProps<I>>;
+
+export interface NodeInfo<I extends {}, O extends {}> {
+ component: NodeComponent<I>;
+ 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 (
+ <svg
+ width="10"
+ height="10"
+ viewBox="0 0 16 16"
+ onMouseDown={wrap(onMouseDown)}
+ onMouseUp={wrap(onMouseUp)}
+ >
+ <circle cx="8" cy="8" r="7" stroke="black" stroke-width="1" />
+ </svg>
+ );
+};
+
+const InSocket = ({ name }: { name: string }) => {
+ const handlers = useContext(SocketHandlers);
+ return (
+ <Socket name={name} onMouseDown={handlers.onInMouseDown} onMouseUp={handlers.onInMouseUp} />
+ );
+};
+
+const OutSocket = ({ name }: { name: string }) => {
+ const handlers = useContext(SocketHandlers);
+ return (
+ <Socket name={name} onMouseDown={handlers.onOutMouseDown} onMouseUp={handlers.onOutMouseUp} />
+ );
+};
+
+export interface InputProps<T> {
+ name: string;
+ label: string;
+ value: InputSocket<T>;
+}
+
+export const InputAny = ({ name, label }: Omit<InputProps<any>, 'value'>) => {
+ return (
+ <li class={cls(styles.in)}>
+ <InSocket name={name} />
+ {label}
+ </li>
+ );
+};
+
+export const InputArray = ({ name, label }: Omit<InputProps<any>, 'value'>) => {
+ return (
+ <li class={cls(styles.in, styles.number)}>
+ <InSocket name={name} />
+ {label}
+ </li>
+ );
+};
+
+const InputNum = (parseFunc: (string) => number) => ({ name, label, value }: InputProps<number>) => {
+ const onInput = (event: InputEvent) => {
+ value.value = parseFunc((event.target as HTMLInputElement).value);
+ }
+ return (
+ <li class={cls(styles.in, styles.number, value.link.value && styles.linked)}>
+ <InSocket name={name} />
+ <span>{label}</span>
+ <input type="number" value={value.manual.value} onInput={onInput} />
+ </li>
+ );
+};
+
+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 (
+ <li class={cls(styles.in, styles.vector, value.link.value && styles.linked)}>
+ <div>
+ <InSocket name={name} />
+ {label}
+ </div>
+ <input type="number" value={value.value[0]} onInput={onInput(0)} />
+ <input type="number" value={value.value[1]} onInput={onInput(1)} />
+ <input type="number" value={value.value[2]} onInput={onInput(2)} />
+ </li>
+ );
+};
+
+export interface InputSelectProps extends InputProps<string> {
+ options: string[] | Record<string, string[]>;
+}
+
+export const InputSelect = ({ name, label, value, options }: InputSelectProps) => {
+ const onChange = (event: InputEvent) => {
+ value.value = (event.target as HTMLSelectElement).value;
+ }
+ return (
+ <li class={cls(styles.in, styles.select, value.link.value && styles.linked)}>
+ <InSocket name={name} />
+ <select aria-label={label} onChange={onChange}>
+ {Array.isArray(options)
+ ? options.map(opt => <option value={opt}>{opt}</option>)
+ : Object.entries(options).map(([label, group]) => (
+ <optgroup label={label}>
+ {group.map(opt => <option value={opt}>{opt}</option>)}
+ </optgroup>
+ ))
+ }
+ </select>
+ </li>
+ );
+};
+
+export interface OutputProps {
+ name: string;
+ label: string;
+}
+
+const Output = ({ name, label, type }: OutputProps & { type: string }) => {
+ return (
+ <li class={cls(styles.out, styles[type])}>
+ {label}
+ <OutSocket name={name} />
+ </li>
+ );
+};
+
+export const OutputNumber = (props: OutputProps) => <Output {...props} type="number" />;
+export const OutputVector = (props: OutputProps) => <Output {...props} type="vector" />;
+
+export interface NodeShellProps {
+ id: number;
+ name: string;
+ x: Signal<number>;
+ y: Signal<number>;
+ 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 (
+ <foreignObject x={x} y={y} width="0" height="0" class={styles.foreignObject}>
+ <details open tabindex={0} class={styles.node} onMouseDown={onMouseDown}>
+ <summary><span onClick={e => e.stopPropagation()}>{name}</span></summary>
+ <ul>
+ <NodeId.Provider value={id}>
+ {children}
+ </NodeId.Provider>
+ </ul>
+ </details>
+ </foreignObject>
+ );
+}; \ No newline at end of file