refactor nodes

This commit is contained in:
2026-03-09 14:04:13 +01:00
parent 76039bdd0e
commit 24efc597b2
19 changed files with 572 additions and 288 deletions

View File

@@ -15,11 +15,7 @@ import {
type NodeChange,
type EdgeChange,
} from '@xyflow/react'
import ConfigNode from './components/graph/ConfigNode'
import FunctionNode from './components/graph/FunctionNode'
import RenderingNode from './components/graph/RenderingNode'
import VariableNode from './components/graph/VariableNode'
import { AnimatedEdge } from './components/graph/AnimatedEdge'
import { AnimatedEdge } from './components/base/AnimatedEdge'
import FlowContext from './lib/flowContext'
import { useTheme } from './lib/themeContext'
import { useGraphStateWithHistory } from './hooks/useGraphStateWithHistory'
@@ -35,8 +31,14 @@ import {
ContextMenuGroup
} from "@/components/ui/context-menu"
import { AppMenubar } from '@/components/AppMenubar'
import { ClipboardPaste, Code2, ScrollText, Sparkles, Variable } from 'lucide-react'
import { ClipboardPaste } from 'lucide-react'
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from './lib/flowUtils'
import {
getRegisteredNodeTypes,
getRegisteredNodeTypeIds,
getDefaultStyle,
isConnectionAllowed,
} from './lib/nodeRegistry'
const SNAP_GRID: [number, number] = [15, 15]
const snapToGrid = (x: number, y: number): { x: number; y: number } => ({
@@ -125,7 +127,7 @@ export default function App() {
)
const nodeTypes = React.useMemo(
() => ({ config: ConfigNode, render: RenderingNode, variable: VariableNode, function: FunctionNode }),
() => Object.fromEntries(getRegisteredNodeTypes().map((r) => [r.id, r.component])),
[]
)
@@ -153,11 +155,12 @@ export default function App() {
const sourceType = sourceNode?.type
const targetType = targetNode?.type
if (!sourceType || !targetType) return false
if (connection.source === connection.target) return false
if (targetType === 'variable') return false
if (targetType === 'render' && (sourceType === 'variable' || sourceType === 'function')) return false
if (sourceType === 'render') return false
return true
return isConnectionAllowed(
sourceType,
targetType,
connection.source,
connection.target
)
},
[nodes]
)
@@ -294,22 +297,17 @@ export default function App() {
(type: string) => {
const position = getMenuPosition()
if (position == null) return
const typeMap: Record<string, Node['type']> = {
config: 'config',
render: 'render',
variable: 'variable',
function: 'function',
}
const nodeType = typeMap[type] ?? 'config'
const nodeType = type as Node['type']
setNodes((nds) => {
const newId = getNextNodeId(nodeType, nds.map((n) => n.id))
const dataMap = getDefaultDataForType(nodeType, newId)
const style = getDefaultStyle(nodeType)
const newNode: Node = {
id: newId,
type: nodeType,
position: { x: position.x, y: position.y },
data: dataMap,
style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config,
style,
}
return nds.concat(newNode)
})
@@ -319,8 +317,6 @@ export default function App() {
[getMenuPosition, setNodes]
)
const VALID_NODE_TYPES = ['config', 'render', 'variable', 'function'] as const
const pasteNode = React.useCallback(async () => {
const position = getMenuPosition()
if (position == null) return
@@ -328,17 +324,19 @@ export default function App() {
const text = await navigator.clipboard?.readText()
if (!text) return
const raw = JSON.parse(text) as { id?: string; type?: string; data?: any; position?: { x: number; y: number }; style?: any }
if (!raw || typeof raw.type !== 'string' || !VALID_NODE_TYPES.includes(raw.type as any)) return
const validIds = getRegisteredNodeTypeIds()
if (!raw || typeof raw.type !== 'string' || !validIds.includes(raw.type)) return
setNodes((nds) => {
const newId = getNextNodeId(raw.type as Node['type'], nds.map((n) => n.id))
const data = raw.data != null && typeof raw.data === 'object' ? { ...raw.data } : {}
if (raw.type === 'config' && data.title != null) data.title = `config-${newId}`
const style = getDefaultStyle(raw.type)
const newNode: Node = {
id: newId,
type: raw.type as Node['type'],
position: { x: position.x, y: position.y },
data,
style: DEFAULT_NODE_STYLE[raw.type] ?? DEFAULT_NODE_STYLE.config,
style,
}
return nds.concat(newNode)
})
@@ -387,75 +385,67 @@ export default function App() {
/>
<div className="flex-1 min-h-0 relative flex flex-col">
<div className="flex-1 min-h-0 flex flex-col">
<FlowContext.Provider value={flowContextValue}>
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="flex-1 min-h-0 w-full" onWheelCapture={onWheelCapture}>
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
isValidConnection={isValidConnection}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
defaultEdgeOptions={defaultEdgeOptions}
colorMode={theme as ColorMode}
snapToGrid
snapGrid={SNAP_GRID}
fitView
onInit={onInit}
nodeDragThreshold={1}
>
<Background variant="dots" gap={20} />
<Controls />
<MiniMap />
</ReactFlow>
</ReactFlowProvider>
</div>
</ContextMenuTrigger>
<FlowContext.Provider value={flowContextValue}>
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="flex-1 min-h-0 w-full" onWheelCapture={onWheelCapture}>
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
isValidConnection={isValidConnection}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
defaultEdgeOptions={defaultEdgeOptions}
colorMode={theme as ColorMode}
snapToGrid
snapGrid={SNAP_GRID}
fitView
onInit={onInit}
nodeDragThreshold={1}
>
<Background variant="dots" gap={20} />
<Controls />
<MiniMap />
</ReactFlow>
</ReactFlowProvider>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuGroup>
<ContextMenuSub>
<ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-44">
<ContextMenuGroup>
<ContextMenuItem onSelect={() => createNode('config')}>
<ScrollText className="mr-2 h-4 w-4" />
Config
</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('render')}>
<Sparkles className="mr-2 h-4 w-4" />
Renderer
</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
<ContextMenuItem onSelect={() => createNode('variable')}>
<Variable className="mr-2 h-4 w-4" />
Variable
</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('function')}>
<Code2 className="mr-2 h-4 w-4" />
Function
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem onSelect={() => pasteNode()}>
<ClipboardPaste className="mr-2 h-4 w-4" />
Paste
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
</FlowContext.Provider>
<ContextMenuContent className="w-48">
<ContextMenuGroup>
<ContextMenuSub>
<ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-44">
<ContextMenuGroup>
{getRegisteredNodeTypes().map((desc, index) => (
<React.Fragment key={desc.id}>
{index === 2 ? <ContextMenuSeparator /> : null}
<ContextMenuItem onSelect={() => createNode(desc.id)}>
{desc.menuIcon}
{desc.menuLabel}
</ContextMenuItem>
</React.Fragment>
))}
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem onSelect={() => pasteNode()}>
<ClipboardPaste className="mr-2 h-4 w-4" />
Paste
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuContent>
</ContextMenu>
</FlowContext.Provider>
</div>
</div>
</div >