Merge remote-tracking branch 'origin/dev' into feat/show-connected-robots

This commit is contained in:
Björn Otgaar
2025-10-30 14:57:18 +01:00
16 changed files with 1105 additions and 5 deletions

View File

@@ -4,7 +4,11 @@ import './App.css'
import TemplatePage from './pages/TemplatePage/Template.tsx'
import Home from './pages/Home/Home.tsx'
import Robot from './pages/Robot/Robot.tsx';
<<<<<<< HEAD
import ConnectedRobots from './pages/ConnectedRobots/ConnectedRobots.tsx'
=======
import VisProg from "./pages/VisProgPage/VisProg.tsx";
>>>>>>> origin/dev
function App(){
@@ -27,6 +31,7 @@ function App(){
<Routes>
<Route path="/" element={<Home />} />
<Route path="/template" element={<TemplatePage />} />
<Route path="/editor" element={<VisProg />} />
<Route path="/robot" element={<Robot />} />
<Route
path="/ConnectedRobots"

View File

@@ -12,6 +12,7 @@ function Home() {
</div>
<div className={styles.links}>
<Link to={"/robot"}>Robot Interaction </Link>
<Link to={"/editor"}>Editor </Link>
<Link to={"/template"}>Template </Link>
<Link to={"/ConnectedRobots"}>Connected Robots </Link>
</div>

View File

@@ -0,0 +1,129 @@
/* editor UI */
.outer-editor-container {
margin-inline: auto;
display: flex;
justify-self: center;
padding: 10px;
align-items: center;
width: 80vw;
height: 80vh;
}
.inner-editor-container {
outline-style: solid;
border-radius: 10pt;
width: 90%;
height: 100%;
}
.dnd-panel {
margin-inline-start: auto;
margin-inline-end: auto;
background-color: canvas;
align-content: center;
margin-bottom: 0.5rem;
margin-top:auto;
width: 50%;
height:7%;
}
.inner-dnd-panel {
outline: 2.5pt solid black;
border-radius: 0 0 5pt 5pt;
border-color: dimgrey;
background-color: canvas;
align-items: center;
}
.dnd-node-container {
background-color: canvas;
justify-content: center;
}
/* Node Styles */
.default-node {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: black solid 2pt;
filter: drop-shadow(0 0 0.75rem black);
}
.default-node-norm {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: forestgreen solid 2pt;
filter: drop-shadow(0 0 0.25rem forestgreen);
}
.default-node-phase {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: dodgerblue solid 2pt;
filter: drop-shadow(0 0 0.25rem dodgerblue);
}
.default-node-start {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: orange solid 2pt;
filter: drop-shadow(0 0 0.25rem orange);
}
.default-node-end {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: red solid 2pt;
filter: drop-shadow(0 0 0.25rem red);
}
.draggable-node {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: black solid 2pt;
filter: drop-shadow(0 0 0.75rem black);
}
.draggable-node-norm {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: forestgreen solid 2pt;
filter: drop-shadow(0 0 0.25rem forestgreen);
}
.draggable-node-phase {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: dodgerblue solid 2pt;
filter: drop-shadow(0 0 0.25rem dodgerblue);
}
.draggable-node-start {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: orange solid 2pt;
filter: drop-shadow(0 0 0.25rem orange);
}
.draggable-node-end {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: red solid 2pt;
filter: drop-shadow(0 0 0.25rem red);
}

View File

@@ -1,3 +1,4 @@
<<<<<<< HEAD
import VisProgUI from "../../visualProgrammingUI/VisProgUI.tsx";
function VisProgPage() {
@@ -6,6 +7,144 @@ function VisProgPage() {
<VisProgUI />
</>
)
=======
import {
Background,
Controls,
Panel,
ReactFlow,
ReactFlowProvider,
MarkerType,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import {useShallow} from 'zustand/react/shallow';
import {
StartNode,
EndNode,
PhaseNode,
NormNode
} from './visualProgrammingUI/components/NodeDefinitions.tsx';
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
import styles from './VisProg.module.css'
// --| config starting params for flow |--
/**
* contains the types of all nodes that are available in the editor
*/
const NODE_TYPES = {
start: StartNode,
end: EndNode,
phase: PhaseNode,
norm: NormNode
};
/**
* defines how the default edge looks inside the editor
*/
const DEFAULT_EDGE_OPTIONS = {
type: 'default',
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#505050',
},
};
/**
* defines what functions in the FlowState store map to which names,
* @param state
*/
const selector = (state: FlowState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
onReconnectStart: state.onReconnectStart,
onReconnectEnd: state.onReconnectEnd,
onReconnect: state.onReconnect
});
// --| define ReactFlow editor |--
/**
* Defines the ReactFlow visual programming editor component
* any implementations of editor logic should be encapsulated where possible
* so the Component definition stays as readable as possible
* @constructor
*/
const VisProgUI = () => {
const {
nodes, edges,
onNodesChange,
onEdgesChange,
onConnect,
onReconnect,
onReconnectStart,
onReconnectEnd
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
return (
<div className={styles.outerEditorContainer}>
<div className={styles.innerEditorContainer}>
<ReactFlow
nodes={nodes}
edges={edges}
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
nodeTypes={NODE_TYPES}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onConnect={onConnect}
snapToGrid
fitView
proOptions={{hideAttribution: true}}
>
<Panel position="top-center" className={styles.dndPanel}>
<DndToolbar/> {/* contains the drag and drop panel for nodes */}
</Panel>
<Controls/>
<Background/>
</ReactFlow>
</div>
</div>
);
};
/**
* Places the VisProgUI component inside a ReactFlowProvider
*
* Wrapping the editor component inside a ReactFlowProvider
* allows us to access and interact with the components inside the editor, outside the editor definition,
* thus facilitating the addition of node specific functions inside their node definitions
*/
function VisualProgrammingUI() {
return (
<ReactFlowProvider>
<VisProgUI/>
</ReactFlowProvider>
);
}
/**
* houses the entire page, so also UI elements
* that are not a part of the Visual Programming UI
* @constructor
*/
function VisProgPage() {
return (
<>
<VisualProgrammingUI/>
</>
)
>>>>>>> origin/dev
}
export default VisProgPage

View File

@@ -0,0 +1,111 @@
import {create} from 'zustand';
import {
applyNodeChanges,
applyEdgeChanges,
addEdge,
reconnectEdge, type Edge, type Connection
} from '@xyflow/react';
import {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
*/
const initialNodes = [
{
id: 'start',
type: 'start',
position: {x: 0, y: 0},
data: {label: 'start'}
},
{
id: 'genericPhase',
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'}
}
];
/**
* contains the initial edges that are created when the editor is loaded
*/
const initialEdges = [
{
id: 'start-end',
source: 'start',
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<FlowState>((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
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges)
});
},
// handles attempted reconnections of a previously disconnected edge
onReconnect: (oldEdge: Edge, newConnection: Connection) => {
get().edgeReconnectSuccessful = true;
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; }) => {
if (!get().edgeReconnectSuccessful) {
set({
edges: get().edges.filter((e) => e.id !== edge.id),
});
}
set({
edgeReconnectSuccessful: true
});
},
deleteNode: (nodeId: string) => {
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});
},
}),
);
export default useFlowStore;

View File

@@ -0,0 +1,34 @@
import {
type Edge,
type Node,
type OnNodesChange,
type OnEdgesChange,
type OnConnect,
type OnReconnect,
} from '@xyflow/react';
/**
* 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;
/**
* The type for the Zustand store object used to manage the state of the ReactFlow editor
*/
export type FlowState = {
nodes: AppNode[];
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;
setEdges: (edges: Edge[]) => void;
};

View File

@@ -0,0 +1,148 @@
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"
/**
* DraggableNodeProps dictates the type properties of a DraggableNode
*/
interface DraggableNodeProps {
className?: string;
children: ReactNode;
nodeType: string;
onDrop: (nodeType: string, 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
*/
function DraggableNode({className, children, nodeType, onDrop}: DraggableNodeProps) {
const draggableRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<XYPosition>({x: 0, y: 0});
// @ts-expect-error comes from a package and doesn't appear to play nicely with strict typescript typing
useDraggable(draggableRef, {
position: position,
onDrag: ({offsetX, offsetY}) => {
// Calculate position relative to the viewport
setPosition({
x: offsetX,
y: offsetY,
});
},
onDragEnd: ({event}) => {
setPosition({x: 0, y: 0});
onDrop(nodeType, {
x: event.clientX,
y: event.clientY,
});
},
});
return (
<div className={className} ref={draggableRef}>
{children}
</div>
);
}
// eslint-disable-next-line react-refresh/only-export-components
export function addNode(nodeType: string, position: XYPosition) {
const {setNodes} = useFlowStore.getState();
const nds = useFlowStore.getState().nodes;
const newNode = () => {
switch (nodeType) {
case "phase":
{
const phaseNumber = nds.filter((node) => node.type === 'phase').length;
return {
id: `phase-${phaseNumber}`,
type: nodeType,
position,
data: {label: 'new', number: phaseNumber},
};
}
case "norm":
{
const normNumber = nds.filter((node) => node.type === 'norm').length;
return {
id: `norm-${normNumber}`,
type: nodeType,
position,
data: {label: `new norm node`},
};
}
default: {
throw new Error(`Node ${nodeType} not found`);
}
}
}
setNodes(nds.concat(newNode()));
}
/**
* the DndToolbar defines how the drag and drop toolbar component works
* and includes the default onDrop behavior through handleNodeDrop
* @constructor
*/
export function DndToolbar() {
const {screenToFlowPosition} = useReactFlow();
/**
* handleNodeDrop implements the default onDrop behavior
*/
const handleNodeDrop = useCallback(
(nodeType: string, screenPosition: XYPosition) => {
const flow = document.querySelector('.react-flow');
const flowRect = flow?.getBoundingClientRect();
const isInFlow =
flowRect &&
screenPosition.x >= flowRect.left &&
screenPosition.x <= flowRect.right &&
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);
}
},
[screenToFlowPosition],
);
return (
<div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`}>
<div className="description">
You can drag these nodes to the pane to create new nodes.
</div>
<div className={`flex-row gap-lg ${styles.dndNodeContainer}`}>
<DraggableNode className={styles.draggableNodePhase} nodeType="phase" onDrop={handleNodeDrop}>
phase Node
</DraggableNode>
<DraggableNode className={styles.draggableNodeNorm} nodeType="norm" onDrop={handleNodeDrop}>
norm Node
</DraggableNode>
</div>
</div>
);
}

View File

@@ -0,0 +1,124 @@
import {Handle, NodeToolbar, Position} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import styles from '../../VisProg.module.css';
import useFlowStore from "../VisProgStores.tsx";
// Contains the datatypes for the data inside our NodeTypes
// this has to be improved or adapted to suit our implementation for computing the graph
// into a format that is useful for the Control Backend
type defaultNodeData = {
label: string;
};
type startNodeData = defaultNodeData;
type endNodeData = defaultNodeData;
type normNodeData = defaultNodeData;
type phaseNodeData = defaultNodeData & {
number: number;
};
export type nodeData = defaultNodeData | startNodeData | phaseNodeData | endNodeData;
// Node Toolbar definition, contains node delete functionality
type ToolbarProps = {
nodeId: string;
allowDelete: boolean;
};
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
const {deleteNode} = useFlowStore();
const deleteParentNode = ()=> {
deleteNode(nodeId);
}
return (
<NodeToolbar>
<button className="Node-toolbar__deletebutton" onClick={deleteParentNode} disabled={!allowDelete}>delete</button>
</NodeToolbar>);
}
// Definitions of Nodes
// Start Node definition:
type StartNodeProps = {
id: string;
data: startNodeData;
};
export const StartNode = ({id, data}: StartNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={false}/>
<div className={styles.defaultNodeStart}>
<div> data test {data.label} </div>
<Handle type="source" position={Position.Right} id="start"/>
</div>
</>
);
};
// End node definition:
type EndNodeProps = {
id: string;
data: endNodeData;
};
export const EndNode = ({id, data}: EndNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={false}/>
<div className={styles.defaultNodeEnd}>
<div> {data.label} </div>
<Handle type="target" position={Position.Left} id="end"/>
</div>
</>
);
};
// Phase node definition:
type PhaseNodeProps = {
id: string;
data: phaseNodeData;
};
export const PhaseNode = ({id, data}: PhaseNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={true}/>
<div className={styles.defaultNodePhase}>
<div> phase {data.number} {data.label} </div>
<Handle type="target" position={Position.Left} id="target"/>
<Handle type="target" position={Position.Bottom} id="norms"/>
<Handle type="source" position={Position.Right} id="source"/>
</div>
</>
);
};
// Norm node definition:
type NormNodeProps = {
id: string;
data: normNodeData;
};
export const NormNode = ({id, data}: NormNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={true}/>
<div className={styles.defaultNodeNorm}>
<div> Norm {data.label} </div>
<Handle type="source" position={Position.Right} id="NormSource"/>
</div>
</>
);
};