diff options
| author | Sam Nystrom <sam@samnystrom.dev> | 2024-03-16 21:09:02 +0000 |
|---|---|---|
| committer | Sam Nystrom <sam@samnystrom.dev> | 2024-03-16 17:16:58 -0400 |
| commit | 3b8c7e391e1f370af74df30a14a699a49543f918 (patch) | |
| tree | c2f0514911963843def2267329e14554a4d21fe9 /src/NodeEditor.tsx | |
| parent | 13b10dd150f9c9cf382fae9802a704ba9c582a62 (diff) | |
refactor: extract NodeEditor into a component
Diffstat (limited to 'src/NodeEditor.tsx')
| -rw-r--r-- | src/NodeEditor.tsx | 286 |
1 files changed, 0 insertions, 286 deletions
diff --git a/src/NodeEditor.tsx b/src/NodeEditor.tsx deleted file mode 100644 index 781e64b..0000000 --- a/src/NodeEditor.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { useContext, useEffect, useMemo, useCallback, useRef } from 'preact/hooks'; -import { signal, computed, batch, useSignal, useComputed, Signal } from '@preact/signals'; -import { Pb } from './context.ts'; -import { NodeComponent, SocketHandlers } from './node.ts'; -import { nodeRegistry } from './nodes'; -import type { SocketHandler, NodeInfo } from './node.ts'; -import { InputSocket } from './dataflow.ts'; -import { Toolbar, ButtonMenu, MenuItem } from './components'; -import './NodeEditor.css'; - -interface NodeInstance { - id: number; - component: NodeComponent<any>; - x: Signal<number>; - y: Signal<number>; - inputs: Record<string, InputSocket<any>>; - outputs: Record<string, Signal<any>>; -} - -const nodeFactory = () => { - let nextNodeId = 0; - return (x: number, y: number, { component, func, inputs }: NodeInfo<any, any>): NodeInstance => { - 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<number>; - fromY: Signal<number>; - toX: Signal<number>; - toY: Signal<number>; -} - -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 ( - <path class="link" d={`M ${fromX} ${fromY} C ${c1x} ${fromY} ${c2x} ${toY} ${toX} ${toY}`} /> - ); -}; - -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 interface NodeEditorProps { - user: string; - project: string; -} - -const NodeEditor = ({ user, project }: NodeEditorProps) => { - const pb = useContext(Pb)!; - - const offsetX = useSignal(0); - const offsetY = useSignal(0); - const scale = useSignal(1); - - const instantiateNode = useMemo(nodeFactory, []); - const svgRef = useRef<SVGSVGElement | null>(null); - - const nodes = useSignal<NodeInstance[]>([]); - - const currentLink = useSignal<null | Omit<LinkData, 'to'>>(null); - const links = useSignal<LinkData[]>([]); - const allLinks = useComputed(() => (links.value as LinkProps[]).concat(currentLink.value as LinkProps ?? [])); - - useEffect(async () => { - const projectData = await pb.collection('projects') - .getFirstListItem(pb.filter('name = {:project} && owner.username = {:user}', { project, user })); - const filter = pb.filter('project.id = {:id}', { id: projectData.id }); - const projectNodes = await pb.collection('nodes').getFullList({ filter }); - const projectLinks = await pb.collection('links').getFullList({ filter }); - const instances = projectNodes.map(node => instantiateNode(node.x, node.y, node.name)); - nodes.value = nodes.value.concat(instances); - }, []); - - const onOutMouseDown: SocketHandler = useCallback((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('no node for mousedown id'); - - 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 = useCallback((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('no node for inmousedown id'); - - 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 = useCallback((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('no nodes for inmouseup ids'); - - 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 onKeyDown = useCallback((event: KeyboardEvent) => { - if (event.code === 'KeyX') { - alert('X'); - } - }, []); - - useEffect(() => { - document.addEventListener('keydown', onKeyDown); - return () => document.removeEventListener('keydown', onKeyDown); - }, []); - - const onBgMouseDown = useCallback(() => { - 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 = useCallback((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; - }), []); - - const addNode = useCallback((node: NodeInfo<any, any>) => { - nodes.value = nodes.value.concat(instantiateNode(100, 100, node)); - }, []); - - return ( - <div class="__NodeEditor"> - <Toolbar title={project}> - <ButtonMenu label="Add"> - {Object.entries(nodeRegistry).map(([name, node]) => ( - <MenuItem label={name} onClick={() => addNode(node)} /> - ))} - </ButtonMenu> - </Toolbar> - <svg width="100vw" height="100vh" ref={svgRef} onMouseDown={onBgMouseDown} onWheel={onBgWheel}> - <pattern - id="bg-grid-major" - patternUnits="userSpaceOnUse" - x={offsetX} y={offsetY} - width={120 * scale.value} height={120 * scale.value} - > - <pattern id="bg-grid-minor" patternUnits="userSpaceOnUse" x="0" y="0" width="24" height="24"> - <circle cx="2" cy="2" r="1" fill="var(--surface)" /> - </pattern> - <g transform={`scale(${scale})`}> - <rect fill="url(#bg-grid-minor)" x="0" y="0" width="100%" height="100%" /> - <circle cx="2" cy="2" r="2" fill="var(--surface)" /> - </g> - </pattern> - <rect fill="url(#bg-grid-major)" x="0" y="0" width="100%" height="100%" /> - <g transform={`translate(${offsetX},${offsetY}) scale(${scale})`}> - {allLinks.value.map(({fromX, fromY, toX, toY}) => ( - <Link fromX={fromX} fromY={fromY} toX={toX} toY={toY} /> - ))} - <SocketHandlers.Provider value={socketHandlers}> - {nodes.value.map(node => ( - <node.component id={node.id} x={node.x} y={node.y} inputs={node.inputs} /> - ))} - </SocketHandlers.Provider> - </g> - </svg> - </div> - ); -}; - -export default NodeEditor;
\ No newline at end of file |
