From b4d7b00dd1add8e1cffb7771539fa6c419b64846 Mon Sep 17 00:00:00 2001 From: Sam Nystrom Date: Wed, 13 Mar 2024 22:37:58 +0000 Subject: refactor: extract nodes/sockets to components --- src/components/nodes/Input.css | 103 +++++++++++++++++++++++++++++++++++++ src/components/nodes/Input.tsx | 97 ++++++++++++++++++++++++++++++++++ src/components/nodes/NodeShell.css | 47 +++++++++++++++++ src/components/nodes/NodeShell.tsx | 46 +++++++++++++++++ src/components/nodes/Output.css | 20 +++++++ src/components/nodes/Output.tsx | 19 +++++++ src/components/nodes/Socket.css | 20 +++++++ src/components/nodes/Socket.tsx | 41 +++++++++++++++ src/components/nodes/index.ts | 3 ++ 9 files changed, 396 insertions(+) create mode 100644 src/components/nodes/Input.css create mode 100644 src/components/nodes/Input.tsx create mode 100644 src/components/nodes/NodeShell.css create mode 100644 src/components/nodes/NodeShell.tsx create mode 100644 src/components/nodes/Output.css create mode 100644 src/components/nodes/Output.tsx create mode 100644 src/components/nodes/Socket.css create mode 100644 src/components/nodes/Socket.tsx create mode 100644 src/components/nodes/index.ts (limited to 'src/components') diff --git a/src/components/nodes/Input.css b/src/components/nodes/Input.css new file mode 100644 index 0000000..aa01558 --- /dev/null +++ b/src/components/nodes/Input.css @@ -0,0 +1,103 @@ +@scope (.__Input) { + :scope { + display: flex; + justify-content: flex-start; + align-items: center; + padding: 4px 0; + + .__Output + & { + padding-top: 8px; + } + + input { + background-color: var(--overlay); + border: none; + border-radius: 4px; + outline: none; + color: var(--text); + font-size: 1em; + text-align: right; + width: 100%; + max-width: 200px; + margin-right: 12px; + padding-top: 3px; + padding-bottom: 2px; + + .linked & { + display: none; + } + } + } +} + +@scope (.__InputNum) { + & + & { + padding-top: 0; + } + + :scope { + --socket-color: var(--data-float); + + span { + position: relative; + left: 8px; + width: 0; + white-space: nowrap; + padding-bottom: 2px; + } + } +} + +@scope (.__InputVector) { + :scope { + flex-direction: column; + align-items: flex-start; + gap: 1px; + --socket-color: var(--data-vector); + + & :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; + } + } + } +} + +@scope (.__InputSelect) { + :scope { + --socket-color: var(--data-float); + + 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/components/nodes/Input.tsx b/src/components/nodes/Input.tsx new file mode 100644 index 0000000..1160f3f --- /dev/null +++ b/src/components/nodes/Input.tsx @@ -0,0 +1,97 @@ +import { InputSocket } from '../../dataflow.ts'; +import { InSocket } from './Socket.tsx'; +import './Input.css'; + +interface InProps { + children: ComponentChildren; + linked?: boolean; + class: string; +} + +const Input = ({ children, linked = false, ...props }: InProps) => ( +
  • {children}
  • +); + +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 InputInteger = InputNum(parseInt); +export const InputNumber = InputNum(parseFloat); + +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 ( + + + + + ); +}; diff --git a/src/components/nodes/NodeShell.css b/src/components/nodes/NodeShell.css new file mode 100644 index 0000000..5e4fb2a --- /dev/null +++ b/src/components/nodes/NodeShell.css @@ -0,0 +1,47 @@ +@scope (.__NodeShell) { + :scope { + width: 0; + height: 0; + overflow: visible; + + > details { + background-color: var(--surface); + min-width: 180px; + width: fit-content; + 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; + } + + summary { + background-color: var(--primary); + padding: 3px; + padding-left: 6px; + margin-bottom: 4px; + border-radius: 6px 6px 0 0; + list-style-image: url('../../icons/chevron-down.svg'); + } + &:not([open]) summary { + list-style-image: url('../../icons/chevron-right.svg'); + } + + ul { + list-style: none; + padding: 0; + margin: 0; + } + } + } +} diff --git a/src/components/nodes/NodeShell.tsx b/src/components/nodes/NodeShell.tsx new file mode 100644 index 0000000..0de1a6d --- /dev/null +++ b/src/components/nodes/NodeShell.tsx @@ -0,0 +1,46 @@ +import { ComponentChildren } from 'preact'; +import { batch, Signal } from '@preact/signals'; +import { NodeId } from '../../node.ts'; +import './NodeShell.css'; + +export interface NodeShellProps { + children: ComponentChildren; + id: number; + name: string; + x: Signal; + y: Signal; +} + +const NodeShell = ({ children, id, name, x, y }: 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} + +
    +
    +
    + ); +}; + +export default NodeShell; \ No newline at end of file diff --git a/src/components/nodes/Output.css b/src/components/nodes/Output.css new file mode 100644 index 0000000..2c752a1 --- /dev/null +++ b/src/components/nodes/Output.css @@ -0,0 +1,20 @@ +@scope (.__Output) { + :scope { + display: flex; + justify-content: flex-end; + align-items: center; + padding: 4px 0; + } +} + +@scope (.__OutputNumber) { + :scope { + --socket-color: var(--data-float); + } +} + +@scope (.__OutputVector) { + :scope { + --socket-color: var(--data-vector); + } +} \ No newline at end of file diff --git a/src/components/nodes/Output.tsx b/src/components/nodes/Output.tsx new file mode 100644 index 0000000..8cd9c98 --- /dev/null +++ b/src/components/nodes/Output.tsx @@ -0,0 +1,19 @@ +import { OutSocket } from './Socket.tsx'; +import './Output.css'; + +export interface OutputProps { + name: string; + label: string; +} + +const Output = (type: string, cls: string) => ({ name, label }: OutputProps) => { + return ( +
  • + {label} + +
  • + ); +}; + +export const OutputNumber = Output('number', '__OutputNumber'); +export const OutputVector = Output('vector', '__OutputVector'); diff --git a/src/components/nodes/Socket.css b/src/components/nodes/Socket.css new file mode 100644 index 0000000..5879b32 --- /dev/null +++ b/src/components/nodes/Socket.css @@ -0,0 +1,20 @@ +@scope (.__Socket) { + :scope { + position: relative; + circle { + fill: var(--socket-color, var(--data-float)); + } + } +} + +@scope (.__InSocket) { + :scope { + right: 4px; + } +} + +@scope (.__OutSocket) { + :scope { + left: 4px; + } +} diff --git a/src/components/nodes/Socket.tsx b/src/components/nodes/Socket.tsx new file mode 100644 index 0000000..a4f38e1 --- /dev/null +++ b/src/components/nodes/Socket.tsx @@ -0,0 +1,41 @@ +import { useContext } from 'preact/hooks'; +import { NodeId, SocketHandlers } from '../../node.ts'; +import './Socket.css'; + +interface SocketProps { + name: string; + class?: string; + onMouseDown?: SocketHandler; + onMouseUp?: SocketHandler; +} + +const Socket = ({ name, onMouseDown, onMouseUp, ...props }: SocketProps) => { + const nodeId = useContext(NodeId); + const wrap = (func?: SocketHandler) => (event: MouseEvent) => func && func(nodeId, name, event); + return ( + + + + ); +}; + +export const InSocket = ({ name }: { name: string }) => { + const handlers = useContext(SocketHandlers); + return ( + + ); +}; + +export const OutSocket = ({ name }: { name: string }) => { + const handlers = useContext(SocketHandlers); + return ( + + ); +}; diff --git a/src/components/nodes/index.ts b/src/components/nodes/index.ts new file mode 100644 index 0000000..d29f39f --- /dev/null +++ b/src/components/nodes/index.ts @@ -0,0 +1,3 @@ +export { default as NodeShell, NodeShellProps } from './NodeShell.tsx'; +export { OutputNumber, OutputVector, OutputProps } from './Output.tsx'; +export { InputAny, InputArray, InputInteger, InputNumber, InputVector, InputSelect, InputProps, InputSelectProps } from './Input.tsx'; -- cgit v1.2.3