diff --git a/src/pages/VisProgPage/VisProg.module.css b/src/pages/VisProgPage/VisProg.module.css index c58d0f3..8c6f70c 100644 --- a/src/pages/VisProgPage/VisProg.module.css +++ b/src/pages/VisProgPage/VisProg.module.css @@ -77,7 +77,7 @@ } .node-norm { - outline: forestgreen solid 2pt; + outline: rgb(0, 149, 25) solid 2pt; filter: drop-shadow(0 0 0.25rem forestgreen); } diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index 8208a70..17f4821 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -8,30 +8,16 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import {useShallow} from 'zustand/react/shallow'; - -import { - StartNodeComponent, - EndNodeComponent, - PhaseNodeComponent, - NormNodeComponent -} from './visualProgrammingUI/components/NodeDefinitions.tsx'; import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx'; -import graphReducer from "./visualProgrammingUI/GraphReducer.ts"; import useFlowStore from './visualProgrammingUI/VisProgStores.tsx'; import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx'; import styles from './VisProg.module.css' +import type { JSX } from 'react'; +import { NodeTypes } from './visualProgrammingUI/NodeRegistry.ts'; +import { graphReducer } from './visualProgrammingUI/GraphReducer.ts'; // --| config starting params for flow |-- -/** - * contains the types of all nodes that are available in the editor - */ -const NODE_TYPES = { - start: StartNodeComponent, - end: EndNodeComponent, - phase: PhaseNodeComponent, - norm: NormNodeComponent -}; /** * defines how the default edge looks inside the editor @@ -86,7 +72,7 @@ const VisProgUI = () => { nodes={nodes} edges={edges} defaultEdgeOptions={DEFAULT_EDGE_OPTIONS} - nodeTypes={NODE_TYPES} + nodeTypes={NodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onReconnect={onReconnect} diff --git a/src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts b/src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts index 138eb82..ae846d7 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts @@ -1,188 +1,16 @@ -import { - type Edge, - getIncomers, - getOutgoers -} from '@xyflow/react'; -import useFlowStore from "./VisProgStores.tsx"; -import type { - BehaviorProgram, - GoalData, - GoalReducer, - GraphPreprocessor, - NormData, - NormReducer, - OrderedPhases, - Phase, - PhaseReducer, - PreparedGraph, - PreparedPhase -} from "./GraphReducerTypes.ts"; -import type { - AppNode, - GoalNode, - NormNode, - PhaseNode -} from "./VisProgTypes.tsx"; +import useFlowStore from './VisProgStores'; +import { NodeReduces } from './NodeRegistry' /** - * Reduces the current graph inside the visual programming editor into a BehaviorProgram - * - * @param {GraphPreprocessor} graphPreprocessor - * @param {PhaseReducer} phaseReducer - * @param {NormReducer} normReducer - * @param {GoalReducer} goalReducer - * @returns {BehaviorProgram} + * Reduces a graph by reducing each of its phases down + * @returns an array of the reduced data types. */ -export default function graphReducer( - graphPreprocessor: GraphPreprocessor = defaultGraphPreprocessor, - phaseReducer: PhaseReducer = defaultPhaseReducer, - normReducer: NormReducer = defaultNormReducer, - goalReducer: GoalReducer = defaultGoalReducer -) : BehaviorProgram { - const nodes: AppNode[] = useFlowStore.getState().nodes; - const edges: Edge[] = useFlowStore.getState().edges; - const preparedGraph: PreparedGraph = graphPreprocessor(nodes, edges); - - return preparedGraph.map((preparedPhase: PreparedPhase) : Phase => - phaseReducer( - preparedPhase, - normReducer, - goalReducer - )); -}; - -/** - * reduces a single preparedPhase to a Phase object - * the Phase object describes a single phase in a BehaviorProgram - * - * @param {PreparedPhase} phase - * @param {NormReducer} normReducer - * @param {GoalReducer} goalReducer - * @returns {Phase} - */ -export function defaultPhaseReducer( - phase: PreparedPhase, - normReducer: NormReducer = defaultNormReducer, - goalReducer: GoalReducer = defaultGoalReducer -) : Phase { - return { - id: phase.phaseNode.id, - name: phase.phaseNode.data.label, - nextPhaseId: phase.nextPhaseId, - phaseData: { - norms: phase.connectedNorms.map(normReducer), - goals: phase.connectedGoals.map(goalReducer) - } - } -} - -/** - * the default implementation of the goalNode reducer function - * - * @param {GoalNode} node - * @returns {GoalData} - */ -function defaultGoalReducer(node: GoalNode) : GoalData { - return { - id: node.id, - name: node.data.label, - value: node.data.value - } -} - -/** - * the default implementation of the normNode reducer function - * - * @param {NormNode} node - * @returns {NormData} - */ -function defaultNormReducer(node: NormNode) :NormData { - return { - id: node.id, - name: node.data.label, - value: node.data.value - } -} - -// Graph preprocessing functions: - -/** - * Preprocesses the provide state of the behavior editor graph, preparing it for further processing in - * the graphReducer function - * - * @param {AppNode[]} nodes - * @param {Edge[]} edges - * @returns {PreparedGraph} - */ -export function defaultGraphPreprocessor(nodes: AppNode[], edges: Edge[]) : PreparedGraph { - const norms : NormNode[] = nodes.filter((node) => node.type === 'norm') as NormNode[]; - const goals : GoalNode[] = nodes.filter((node) => node.type === 'goal') as GoalNode[]; - const orderedPhases : OrderedPhases = orderPhases(nodes, edges); - - return orderedPhases.phaseNodes.map((phase: PhaseNode) : PreparedPhase => { - const nextPhase = orderedPhases.connections.get(phase.id); - return { - phaseNode: phase, - nextPhaseId: nextPhase as string, - connectedNorms: getIncomers({id: phase.id}, norms,edges), - connectedGoals: getIncomers({id: phase.id}, goals,edges) - }; +export function graphReducer() { + const { nodes } = useFlowStore.getState(); + return nodes + .filter((n) => n.type == 'phase') + .map((n) => { + const reducer = NodeReduces['phase']; + return reducer(n, nodes) }); -} - -/** - * orderPhases takes the state of the graph created by the editor and turns it into an OrderedPhases object. - * - * @param {AppNode[]} nodes - * @param {Edge[]} edges - * @returns {OrderedPhases} - */ -export function orderPhases(nodes: AppNode[],edges: Edge[]) : OrderedPhases { - // find the first Phase node - const phaseNodes : PhaseNode[] = nodes.filter((node) => node.type === 'phase') as PhaseNode[]; - const startNodeIndex = nodes.findIndex((node : AppNode):boolean => {return (node.type === 'start');}); - const firstPhaseNode = getOutgoers({ id: nodes[startNodeIndex].id },phaseNodes,edges); - - // recursively adds the phase nodes to a list in the order they are connected in the graph - const nextPhase = ( - currentIndex: number, - { phaseNodes: phases, connections: connections} : OrderedPhases - ) : OrderedPhases => { - // get the current phase and the next phases; - const currentPhase = phases[currentIndex]; - const nextPhaseNodes = getOutgoers(currentPhase,phaseNodes,edges); - const nextNodes = getOutgoers(currentPhase,nodes, edges); - - // handles adding of the next phase to the chain, and error handle if an invalid state is received - if (nextPhaseNodes.length === 1 && nextNodes.length === 1) { - connections.set(currentPhase.id, nextPhaseNodes[0].id); - return nextPhase(phases.push(nextPhaseNodes[0] as PhaseNode) - 1, {phaseNodes: phases, connections: connections}); - } else { - // handle erroneous states - if (nextNodes.length === 0){ - throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" doesn't have any outgoing connections`); - } else { - if (nextNodes.length > 1) { - throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" connects to too many targets`); - } else { - if (nextNodes[0].type === "end"){ - connections.set(currentPhase.id, "end"); - // returns the final output of the function - return { phaseNodes: phases, connections: connections}; - } else { - throw new Error(`| INVALID PROGRAM | the node "${nextNodes[0].id}" that "${currentPhase.id}" connects to is not a phase or end node`); - } - } - } - } - } - // initializes the Map describing the connections between phase nodes - // we need this Map to make sure we preserve this information, - // so we don't need to do checks on the entire set of edges in further stages of the reduction algorithm - const connections : Map = new Map(); - - // returns an empty list if no phase nodes are present, otherwise returns an ordered list of phaseNodes - if (firstPhaseNode.length > 0) { - return nextPhase(0, {phaseNodes: [firstPhaseNode[0] as PhaseNode], connections: connections}) - } else { return {phaseNodes: [], connections: connections} } } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/GraphReducerTypes.ts b/src/pages/VisProgPage/visualProgrammingUI/GraphReducerTypes.ts deleted file mode 100644 index 9151b56..0000000 --- a/src/pages/VisProgPage/visualProgrammingUI/GraphReducerTypes.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type {Edge} from "@xyflow/react"; -import type {AppNode, GoalNode, NormNode, PhaseNode} from "./VisProgTypes.tsx"; - - -/** - * defines how a norm is represented in the simplified behavior program - */ -export type NormData = { - id: string; - name: string; - value: string; -}; - -/** - * defines how a goal is represented in the simplified behavior program - */ -export type GoalData = { - id: string; - name: string; - value: string; -}; - -/** - * definition of a PhaseData object, it contains all phaseData that is relevant - * for further processing and execution of a phase. - */ -export type PhaseData = { - norms: NormData[]; - goals: GoalData[]; -}; - -/** - * Describes a single phase within the simplified representation of a behavior program, - * - * Contains: - * - the id of the described phase, - * - the name of the described phase, - * - the id of the next phase in the user defined behavior program - * - the data property of the described phase node - * - * @NOTE at the moment the type definitions do not support branching programs, - * if branching of phases is to be supported in the future, the type definition for Phase has to be updated - */ -export type Phase = { - id: string; - name: string; - nextPhaseId: string; - phaseData: PhaseData; -}; - -/** - * Describes a simplified behavior program as a list of Phase objects - */ -export type BehaviorProgram = Phase[]; - - - -export type NormReducer = (node: NormNode) => NormData; -export type GoalReducer = (node: GoalNode) => GoalData; -export type PhaseReducer = ( - preparedPhase: PreparedPhase, - normReducer: NormReducer, - goalReducer: GoalReducer -) => Phase; - -/** - * contains: - * - * - list of phases, sorted based on position in chain between the start and end node - * - a dictionary containing all outgoing connections, - * to other phase or end nodes, for each phase node uses the id of the source node as key - * and the id of the target node as value - * - */ -export type OrderedPhases = { - phaseNodes: PhaseNode[]; - connections: Map; -}; - -/** - * A single prepared phase, - * contains: - * - the described phaseNode, - * - the id of the next phaseNode or "end" for the end node - * - a list of the normNodes that are connected to the described phase - * - a list of the goalNodes that are connected to the described phase - */ -export type PreparedPhase = { - phaseNode: PhaseNode; - nextPhaseId: string; - connectedNorms: NormNode[]; - connectedGoals: GoalNode[]; -}; - -/** - * a list of PreparedPhase objects, - * describes the preprocessed state of a program, - * before the contents of the node - */ -export type PreparedGraph = PreparedPhase[]; - -export type GraphPreprocessor = (nodes: AppNode[], edges: Edge[]) => PreparedGraph; - - - - diff --git a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts new file mode 100644 index 0000000..12202f1 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts @@ -0,0 +1,33 @@ +import StartNode, { StartConnects, StartNodeDefaults, StartReduce } from "./nodes/StartNode"; +import EndNode, { EndConnects, EndNodeDefaults, EndReduce } from "./nodes/EndNode"; +import PhaseNode, { PhaseConnects, PhaseNodeDefaults, PhaseReduce } from "./nodes/PhaseNode"; +import NormNode, { NormConnects, NormNodeDefaults, NormReduce } from "./nodes/NormNode"; + +export const NodeTypes = { + start: StartNode, + end: EndNode, + phase: PhaseNode, + norm: NormNode, +}; + +// Default node data for creation +export const NodeDefaults = { + start: StartNodeDefaults, + end: EndNodeDefaults, + phase: PhaseNodeDefaults, + norm: NormNodeDefaults, +}; + +export const NodeReduces = { + start: StartReduce, + end: EndReduce, + phase: PhaseReduce, + norm: NormReduce, +} + +export const NodeConnects = { + start: StartConnects, + end: EndConnects, + phase: PhaseConnects, + norm: NormConnects, +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index 300c14b..1368d5d 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx @@ -1,142 +1,127 @@ -import {create} from 'zustand'; +import { create } from 'zustand'; import { applyNodeChanges, applyEdgeChanges, addEdge, - reconnectEdge, type Edge, type Connection + reconnectEdge, + type Node, + type Edge, + type NodeChange, + type XYPosition, } from '@xyflow/react'; +import type { FlowState, AppNode } from './VisProgTypes'; +import { NodeDefaults, NodeConnects } from './NodeRegistry'; -import {type AppNode, type FlowState} from './VisProgTypes.tsx'; /** - * contains the nodes that are created when the editor is loaded, - * should contain at least a start and an end node + * Create a node given the correct data + * @param type + * @param id + * @param position + * @param data + * @constructor */ -const initialNodes = [ - { - id: 'start', - type: 'start', - position: {x: 0, y: 0}, - data: {label: 'start'} - }, - { - id: 'phase-1', - type: 'phase', - position: {x: 0, y: 150}, - data: {label: 'Generic Phase', number: 1}, - }, - { - id: 'end', - type: 'end', - position: {x: 0, y: 300}, - data: {label: 'End'} +function createNode(id: string, type: string, position: XYPosition, data: any) { + + const defaultData = Object.entries(NodeDefaults).find(([t, _]) => t == type)?.[1] + const newData = { + id: id, + type: type, + position: position, + data: data, } + + return (defaultData == undefined) ? newData : ({...defaultData, ...newData}) +} + +//* Initial nodes, created by using createNodeInstance. */ +const initialNodes : Node[] = [ + createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}), + createNode('end', 'end', {x: 370, y: 100}, {label: "End"}), + createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children: ['end', 'start']}), + createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"]}), ]; -/** - * contains the initial edges that are created when the editor is loaded - */ -const initialEdges = [ - { - id: 'start-phase-1', - source: 'start', - target: 'phase-1', - }, - { - id: 'phase-1-end', - source: 'phase-1', - target: 'end', - } +// * Initial edges * / +const initialEdges: Edge[] = [ + { id: 'start-phase-1', source: 'start', target: 'phase-1' }, + { id: 'phase-1-end', source: 'phase-1', target: 'end' }, ]; -/** - * The useFlowStore hook contains the implementation for editor functionality and state - * we can use this inside our editor component to access the current state - * and use any implemented functionality - */ const useFlowStore = create((set, get) => ({ nodes: initialNodes, edges: initialEdges, edgeReconnectSuccessful: true, - onNodesChange: (changes) => { - set({ - nodes: applyNodeChanges(changes, get().nodes) - }); - }, - onEdgesChange: (changes) => { - set({ - edges: applyEdgeChanges(changes, get().edges) - }); - }, - // handles connection of newly created edges + + onNodesChange: (changes) => + set({nodes: applyNodeChanges(changes, get().nodes)}), + onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }), + + // Let's make sure we tell the nodes when they're connected, and how it matters. onConnect: (connection) => { - set({ - edges: addEdge(connection, get().edges) - }); - }, - // handles attempted reconnections of a previously disconnected edge - onReconnect: (oldEdge: Edge, newConnection: Connection) => { + const edges = addEdge(connection, get().edges); + const nodes = get().nodes; + // connection has: { source, sourceHandle, target, targetHandle } + // Let's find the source and target ID's. + let sourceNode = nodes.find((n) => n.id == connection.source); + let targetNode = nodes.find((n) => n.id == connection.target); + + // In case the nodes weren't found, return basic functionality. + if (sourceNode == undefined || targetNode == undefined || sourceNode.type == undefined || targetNode.type == undefined) { + set({ nodes, edges }); + return; + } + + // We should find out how their data changes by calling their respective functions. + let sourceConnectFunction = Object.entries(NodeConnects).find(([t, _]) => t == sourceNode.type)?.[1] + let targetConnectFunction = Object.entries(NodeConnects).find(([t, _]) => t == targetNode.type)?.[1] + if (sourceConnectFunction == undefined || targetConnectFunction == undefined) { + set({ nodes, edges }); + return; + } + + // We're going to have to update their data based on how they want to update it. + sourceConnectFunction(sourceNode, targetNode, true) + targetConnectFunction(targetNode, sourceNode, false) + set({ nodes, edges }); +}, + + onReconnect: (oldEdge, newConnection) => { get().edgeReconnectSuccessful = true; - set({ - edges: reconnectEdge(oldEdge, newConnection, get().edges) - }); + set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) }); }, - // Handles initiation of reconnection of edges that are manually disconnected from a node - onReconnectStart: () => { - set({ - edgeReconnectSuccessful: false - }); - }, - // Drops the edge from the set of edges, removing it from the flow, if no successful reconnection occurred - onReconnectEnd: (_: unknown, edge: { id: string; }) => { + + onReconnectStart: () => set({ edgeReconnectSuccessful: false }), + onReconnectEnd: (_evt, edge) => { if (!get().edgeReconnectSuccessful) { - set({ - edges: get().edges.filter((e) => e.id !== edge.id), - }); + set({ edges: get().edges.filter((e) => e.id !== edge.id) }); } - set({ - edgeReconnectSuccessful: true - }); + set({ edgeReconnectSuccessful: true }); }, - deleteNode: (nodeId: string) => { + + deleteNode: (nodeId) => set({ nodes: get().nodes.filter((n) => n.id !== nodeId), - edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId) - }); - }, - setNodes: (nodes) => { - set({nodes}); - }, - setEdges: (edges) => { - set({edges}); - }, -/** - * handles updating the data component of a node, - * if the provided data object contains entries that aren't present in the updated node's data component - * those entries are added to the data component, - * entries that do exist within the node's data component, - * are simply updated to contain the new value - * - * the data object - * @param {string} nodeId - * @param {object} data - */ - updateNodeData: (nodeId: string, data) => { - set({ - nodes: get().nodes.map((node) : AppNode => { - if (node.id === nodeId) { - return { - ...node, - data: { - ...node.data, - ...data - } - }; - } else { return node; } - }) - }); - } - }), -); + edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId), + }), -export default useFlowStore; \ No newline at end of file + setNodes: (nodes) => set({ nodes }), + setEdges: (edges) => set({ edges }), + + updateNodeData: (nodeId, data) => { + set({ + nodes: get().nodes.map((node) => { + if (node.id === nodeId) { + node.data = { ...node.data, ...data }; + } + return node; + }), + }); + }, + + addNode: (node: Node) => { + set({ nodes: [...get().nodes, node] }); + }, +})); + +export default useFlowStore; diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx index bb7c28c..6b98d6b 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx @@ -1,47 +1,24 @@ -import { - type Edge, - type Node, - type OnNodesChange, - type OnEdgesChange, - type OnConnect, - type OnReconnect, -} from '@xyflow/react'; +// VisProgTypes.ts +import type { Edge, OnNodesChange, OnEdgesChange, OnConnect, OnReconnect, Node } from '@xyflow/react'; +import type { NodeTypes } from './NodeRegistry'; +export type AppNode = typeof NodeTypes -type defaultNodeData = { - label: string; -}; - -export type StartNode = Node; -export type EndNode = Node; -export type GoalNode = Node; -export type NormNode = Node; -export type PhaseNode = Node; - - -/** - * a type meant to house different node types, currently not used - * but will allow us to more clearly define nodeTypes when we implement - * computation of the Graph inside the ReactFlow editor - */ -export type AppNode = Node | StartNode | EndNode | NormNode | GoalNode | PhaseNode; - - -/** - * The type for the Zustand store object used to manage the state of the ReactFlow editor - */ export type FlowState = { - nodes: AppNode[]; + nodes: Node[]; edges: Edge[]; edgeReconnectSuccessful: boolean; + onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onConnect: OnConnect; onReconnect: OnReconnect; onReconnectStart: () => void; onReconnectEnd: (_: unknown, edge: { id: string }) => void; + deleteNode: (nodeId: string) => void; - setNodes: (nodes: AppNode[]) => void; + setNodes: (nodes: Node[]) => void; setEdges: (edges: Edge[]) => void; updateNodeData: (nodeId: string, data: object) => void; + addNode: (node: Node) => void; }; \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx index c9e1496..f34bd00 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx @@ -1,19 +1,10 @@ -import {useDraggable} from '@neodrag/react'; -import { - useReactFlow, - type XYPosition -} from '@xyflow/react'; -import { - type ReactNode, - useCallback, - useRef, - useState -} from 'react'; -import useFlowStore from "../VisProgStores.tsx"; -import styles from "../../VisProg.module.css" -import type {AppNode, PhaseNode, NormNode} from "../VisProgTypes.tsx"; - - +import { useDraggable } from '@neodrag/react'; +import { useReactFlow, type XYPosition } from '@xyflow/react'; +import { type ReactNode, useCallback, useRef, useState } from 'react'; +import useFlowStore from '../VisProgStores'; +import styles from '../../VisProg.module.css'; +import type { AppNode } from '../VisProgTypes'; +import { NodeDefaults, type NodeTypes } from '../NodeRegistry' /** * DraggableNodeProps dictates the type properties of a DraggableNode @@ -21,41 +12,28 @@ import type {AppNode, PhaseNode, NormNode} from "../VisProgTypes.tsx"; interface DraggableNodeProps { className?: string; children: ReactNode; - nodeType: string; - onDrop: (nodeType: string, position: XYPosition) => void; + nodeType: keyof typeof NodeTypes; + onDrop: (nodeType: keyof typeof NodeTypes, position: XYPosition) => void; } /** - * Definition of a node inside the drag and drop toolbar, - * these nodes require an onDrop function to be supplied - * that dictates how the node is created in the graph. - * - * @param className - * @param children - * @param nodeType - * @param onDrop - * @constructor + * Definition of a node inside the drag and drop toolbar. + * These nodes require an onDrop function that dictates + * how the node is created in the graph. */ -function DraggableNode({className, children, nodeType, onDrop}: DraggableNodeProps) { +function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) { const draggableRef = useRef(null); - const [position, setPosition] = useState({x: 0, y: 0}); + const [position, setPosition] = useState({ x: 0, y: 0 }); - // @ts-expect-error comes from a package and doesn't appear to play nicely with strict typescript typing + // @ts-expect-error from the neodrag package — safe to ignore useDraggable(draggableRef, { - position: position, - onDrag: ({offsetX, offsetY}) => { - // Calculate position relative to the viewport - setPosition({ - x: offsetX, - y: offsetY, - }); + position, + onDrag: ({ offsetX, offsetY }) => { + setPosition({ x: offsetX, y: offsetY }); }, - onDragEnd: ({event}) => { - setPosition({x: 0, y: 0}); - onDrop(nodeType, { - x: event.clientX, - y: event.clientY, - }); + onDragEnd: ({ event }) => { + setPosition({ x: 0, y: 0 }); + onDrop(nodeType, { x: event.clientX, y: event.clientY }); }, }); @@ -66,71 +44,49 @@ function DraggableNode({className, children, nodeType, onDrop}: DraggableNodePro ); } +/** + * addNode — adds a new node to the flow using the unified class-based system. + * Keeps numbering logic for phase/norm nodes. + */ +export function addNode(nodeType: keyof typeof NodeTypes, position: XYPosition) { + const { nodes, setNodes } = useFlowStore.getState(); + const defaultData = NodeDefaults[nodeType] -// eslint-disable-next-line react-refresh/only-export-components -export function addNode(nodeType: string, position: XYPosition) { - const {setNodes} = useFlowStore.getState(); - const nds : AppNode[] = useFlowStore.getState().nodes; - const newNode = () => { - switch (nodeType) { - case "phase": - { - const phaseNodes= nds.filter((node) => node.type === 'phase'); - let phaseNumber; - if (phaseNodes.length > 0) { - const finalPhaseId : number = +(phaseNodes[phaseNodes.length - 1].id.split('-')[1]); - phaseNumber = finalPhaseId + 1; - } else { - phaseNumber = 1; - } - const phaseNode : PhaseNode = { - id: `phase-${phaseNumber}`, - type: nodeType, - position, - data: {label: 'new', number: phaseNumber}, - } - return phaseNode; - } - case "norm": - { - const normNodes= nds.filter((node) => node.type === 'norm'); - let normNumber - if (normNodes.length > 0) { - const finalNormId : number = +(normNodes[normNodes.length - 1].id.split('-')[1]); - normNumber = finalNormId + 1; - } else { - normNumber = 1; - } + if (!defaultData) throw new Error(`Node type '${nodeType}' not found in registry`); - const normNode : NormNode = { - id: `norm-${normNumber}`, - type: nodeType, - position, - data: {label: `new norm node`, value: "Pepper should be formal"}, - } - return normNode; - } - default: { - throw new Error(`Node ${nodeType} not found`); - } - } + const sameTypeNodes = nodes.filter((node) => node.type === nodeType); + const nextNumber = + sameTypeNodes.length > 0 + ? (() => { + const lastNode = sameTypeNodes[sameTypeNodes.length - 1]; + const parts = lastNode.id.split('-'); + const lastNum = Number(parts[1]); + return Number.isNaN(lastNum) ? sameTypeNodes.length + 1 : lastNum + 1; + })() + : 1; + + const id = `${nodeType}-${nextNumber}`; + + let newNode = { + id: id, + type: nodeType, + position, + data: {...defaultData} } - - setNodes(nds.concat(newNode())); + + console.log("Tried to add node"); + setNodes([...nodes, newNode]); } /** - * the DndToolbar defines how the drag and drop toolbar component works - * and includes the default onDrop behavior through handleNodeDrop - * @constructor + * DndToolbar defines how the drag and drop toolbar component works + * and includes the default onDrop behavior. */ export function DndToolbar() { - const {screenToFlowPosition} = useReactFlow(); - /** - * handleNodeDrop implements the default onDrop behavior - */ + const { screenToFlowPosition } = useReactFlow(); + const handleNodeDrop = useCallback( - (nodeType: string, screenPosition: XYPosition) => { + (nodeType: keyof typeof NodeTypes, screenPosition: XYPosition) => { const flow = document.querySelector('.react-flow'); const flowRect = flow?.getBoundingClientRect(); const isInFlow = @@ -140,7 +96,6 @@ export function DndToolbar() { screenPosition.y >= flowRect.top && screenPosition.y <= flowRect.bottom; - // Create a new node and add it to the flow if (isInFlow) { const position = screenToFlowPosition(screenPosition); addNode(nodeType, position); @@ -149,19 +104,35 @@ export function DndToolbar() { [screenToFlowPosition], ); + + const droppableNodes = Object.entries(NodeDefaults) + .filter(([_, data]) => data.droppable) + .map(([type, data]) => ({ + type: type as DraggableNodeProps['nodeType'], + data + })); + + + return (
You can drag these nodes to the pane to create new nodes.
- - phase Node - - - norm Node - + { + // Maps over all the nodes that are droppable, and puts them in the panel + } + {droppableNodes.map(({type, data}) => ( + + {data.label} + + ))}
); -} \ No newline at end of file +} diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx new file mode 100644 index 0000000..a00ad4e --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx @@ -0,0 +1,73 @@ +import { + Handle, + type NodeProps, + Position, + type Connection, + type Edge, + useReactFlow, + type Node, +} from '@xyflow/react'; +import { Toolbar } from './NodeDefinitions'; +import styles from '../../VisProg.module.css'; + +export type EndNodeData = { + label: string; + droppable: Boolean; + hasReduce: Boolean; +}; + + +export const EndNodeDefaults: EndNodeData = { + label: "End Node", + droppable: false, + hasReduce: true +}; + +export type EndNode = Node + +export function EndNodeCanConnect(connection: Connection | Edge): boolean { + // connection has: { source, sourceHandle, target, targetHandle } + + // Example rules: + if (connection.source === connection.target) return false; + + + if (connection.targetHandle && !["a", "b"].includes(connection.targetHandle)) { + return false; + } + + if (connection.sourceHandle && connection.sourceHandle !== "result") { + return false; + } + + // If all rules pass + return true; +} + +export default function EndNode(props: NodeProps) { + const reactFlow = useReactFlow(); + const label_input_id = `phase_${props.id}_label_input`; + return ( + <> + +
+
+ End +
+ + + +
+ + ); +} + +export function EndReduce(node: Node, nodes: Node[]) { + return { + id: node.id + } +} + +export function EndConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { + +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/NodeDefinitions.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/NodeDefinitions.tsx similarity index 53% rename from src/pages/VisProgPage/visualProgrammingUI/components/NodeDefinitions.tsx rename to src/pages/VisProgPage/visualProgrammingUI/nodes/NodeDefinitions.tsx index 19f56dd..5367dff 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/NodeDefinitions.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/NodeDefinitions.tsx @@ -1,21 +1,10 @@ import { - Handle, - type NodeProps, - NodeToolbar, - Position -} from '@xyflow/react'; + NodeToolbar} from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import styles from '../../VisProg.module.css'; import useFlowStore from "../VisProgStores.tsx"; -import type { - StartNode, - EndNode, - PhaseNode, - NormNode -} from "../VisProgTypes.tsx"; //Toolbar definitions - type ToolbarProps = { nodeId: string; allowDelete: boolean; @@ -45,7 +34,6 @@ export function Toolbar({nodeId, allowDelete}: ToolbarProps) { } // Renaming component - /** * Adds a component that can be used to edit a node's label entry inside its Data * can be added to any custom node that has a label inside its Data @@ -94,90 +82,3 @@ export function EditableName({nodeLabel = "new node", nodeId} : { nodeLabel : st ) } - -// Definitions of Nodes - -/** - * Start Node definition: - * - * @param {string} id - * @param {defaultNodeData} data - * @returns {React.JSX.Element} - * @constructor - */ -export const StartNodeComponent = ({id, data}: NodeProps) => { - return ( - <> - -
-
data test {data.label}
- -
- - ); -}; - - -/** - * End node definition: - * - * @param {string} id - * @param {defaultNodeData} data - * @returns {React.JSX.Element} - * @constructor - */ -export const EndNodeComponent = ({id, data}: NodeProps) => { - return ( - <> - -
-
{data.label}
- -
- - ); -}; - - -/** - * Phase node definition: - * - * @param {string} id - * @param {defaultNodeData & {number: number}} data - * @returns {React.JSX.Element} - * @constructor - */ -export const PhaseNodeComponent = ({id, data}: NodeProps) => { - return ( - <> - -
- - - - -
- - ); -}; - - -/** - * Norm node definition: - * - * @param {string} id - * @param {defaultNodeData & {value: string}} data - * @returns {React.JSX.Element} - * @constructor - */ -export const NormNodeComponent = ({id, data}: NodeProps) => { - return ( - <> - -
- - -
- - ); -}; \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx new file mode 100644 index 0000000..eefbfe6 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.tsx @@ -0,0 +1,86 @@ +import { + Handle, + type NodeProps, + Position, + type Connection, + type Edge, + useReactFlow, + type Node, +} from '@xyflow/react'; +import { Toolbar } from './NodeDefinitions'; +import styles from '../../VisProg.module.css'; +import { NodeDefaults, NodeReduces } from '../NodeRegistry'; +import type { FlowState } from '../VisProgTypes'; + +/** + * The default data dot a Norm node + * @param label: the label of this Norm + * @param droppable: whether this node is droppable from the drop bar (initialized as true) + * @param children: ID's of children of this node + */ +export type NormNodeData = { + label: string; + droppable: boolean; + normList: string[]; + hasReduce: boolean; +}; + +/** + * Default data for this node + */ +export const NormNodeDefaults: NormNodeData = { + label: "Norm Node", + droppable: true, + normList: [], + hasReduce: true, +}; + +export type NormNode = Node + +/** + * + * @param connection + * @returns + */ +export function NormNodeCanConnect(connection: Connection | Edge): boolean { + return true; +} + +/** + * Defines how a Norm node should be rendered + * @param props NodeProps, like id, label, children + * @returns React.JSX.Element + */ +export default function NormNode(props: NodeProps) { + const reactFlow = useReactFlow(); + const label_input_id = `Norm_${props.id}_label_input`; + const data = props.data as NormNodeData; + return ( + <> + +
+
+ + {props.data.label as string} +
+ {data.normList.map((norm) => (
{norm}
))} + +
+ + ); +} + +/** + * Reduces each Norm, including its children down into its relevant data. + * @param props: The Node Properties of this node. + */ +export function NormReduce(node: Node, nodes: Node[]) { + const data = node.data as NormNodeData; + return { + label: data.label, + list: data.normList, + } +} + +export function NormConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx new file mode 100644 index 0000000..6ed9218 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx @@ -0,0 +1,116 @@ +import { + Handle, + type NodeProps, + Position, + type Connection, + type Edge, + useReactFlow, + type Node, +} from '@xyflow/react'; +import { Toolbar } from './NodeDefinitions'; +import styles from '../../VisProg.module.css'; +import { NodeDefaults, NodeReduces } from '../NodeRegistry'; +import type { FlowState } from '../VisProgTypes'; + +/** + * The default data dot a phase node + * @param label: the label of this phase + * @param droppable: whether this node is droppable from the drop bar (initialized as true) + * @param children: ID's of children of this node + */ +export type PhaseNodeData = { + label: string; + droppable: boolean; + children: string[]; + hasReduce: boolean; +}; + +/** + * Default data for this node + */ +export const PhaseNodeDefaults: PhaseNodeData = { + label: "Phase Node", + droppable: true, + children: [], + hasReduce: true, +}; + +export type PhaseNode = Node + +/** + * + * @param connection + * @returns + */ +export function PhaseNodeCanConnect(connection: Connection | Edge): boolean { + return true; +} + +/** + * Defines how a phase node should be rendered + * @param props NodeProps, like id, label, children + * @returns React.JSX.Element + */ +export default function PhaseNode(props: NodeProps) { + const reactFlow = useReactFlow(); + const label_input_id = `phase_${props.id}_label_input`; + return ( + <> + +
+
+ + {props.data.label as string} +
+ + + +
+ + ); +} + +/** + * Reduces each phase, including its children down into its relevant data. + * @param props: The Node Properties of this node. + */ +export function PhaseReduce(node: Node, nodes: Node[]) { + const thisnode = node as PhaseNode; + const data = thisnode.data as PhaseNodeData; + const reducableChildren = Object.entries(NodeDefaults) + .filter(([_, data]) => data.hasReduce) + .map(([type, _]) => ( + type + )); + + let childrenData: any = "" + if (data.children != undefined) { + childrenData = data.children.map((childId) => { + // Reduce each of this phases' children. + let child = nodes.find((node) => node.id == childId); + + // Make sure that we reduce only valid children nodes. + if (child == undefined || child.type == undefined || !reducableChildren.includes(child.type)) return '' + const reducer = NodeReduces[child.type as keyof typeof NodeReduces] + + if (!reducer) { + console.warn(`No reducer found for node type ${child.type}`); + return null; + } + + return reducer(child, nodes); + })} + return { + id: thisnode.id, + name: data.label as string, + children: childrenData, + } +} + +export function PhaseConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { + console.log("Connect functionality called.") + let node = thisNode as PhaseNode + let data = node.data as PhaseNodeData + if (isThisSource) + data.children.push(otherNode.id) +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx new file mode 100644 index 0000000..51d0096 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx @@ -0,0 +1,93 @@ +import { + Handle, + type NodeProps, + Position, + type Connection, + type Edge, + useReactFlow, + type Node, +} from '@xyflow/react'; +import { Toolbar } from './NodeDefinitions'; +import styles from '../../VisProg.module.css'; + +/* --------------------------------------------------------- + * 1. THE DATA SHAPE FOR THIS NODE TYPE + * -------------------------------------------------------*/ +export type StartNodeData = { + label: string; + droppable: boolean; + hasReduce: boolean; +}; + +/* --------------------------------------------------------- + * 2. DEFAULT DATA FOR NEW INSTANCES OF THIS NODE + * -------------------------------------------------------*/ +export const StartNodeDefaults: StartNodeData = { + label: "Start Node", + droppable: false, + hasReduce: true, +}; + +export type StartNode = Node + +/* --------------------------------------------------------- + * 3. CUSTOM CONNECTION LOGIC FOR THIS NODE + * -------------------------------------------------------*/ +export function startNodeCanConnect(connection: Connection | Edge): boolean { + // connection has: { source, sourceHandle, target, targetHandle } + + // Example rules: + + // ❌ Cannot connect to itself + if (connection.source === connection.target) return false; + + // ❌ Only allow incoming connections on input slots "a" or "b" + if (connection.targetHandle && !["a", "b"].includes(connection.targetHandle)) { + return false; + } + + // ❌ Only allow outgoing connections from "result" + if (connection.sourceHandle && connection.sourceHandle !== "result") { + return false; + } + + // If all rules pass + return true; +} + +/* --------------------------------------------------------- + * 4. OPTIONAL: Node execution logic + * If your system evaluates nodes, this is where that lives. + * -------------------------------------------------------*/ + + +/* --------------------------------------------------------- + * 5. THE NODE COMPONENT (UI) + * -------------------------------------------------------*/ +export default function StartNode(props: NodeProps) { + const reactFlow = useReactFlow(); + const label_input_id = `phase_${props.id}_label_input`; + return ( + <> + +
+
+ Start +
+ + + +
+ + ); +} + +export function StartReduce(node: Node, nodes: Node[]) { + return { + id: node.id + } +} + +export function StartConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { + +} \ No newline at end of file