// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import { type NodeProps, Position, type Node, useNodeConnections } from '@xyflow/react'; import {useEffect, useRef} from "react"; import {type EditorWarning} from "../components/EditorWarnings.tsx"; import { Toolbar } from '../components/NodeComponents'; import styles from '../../VisProg.module.css'; import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx"; import {allowOnlyConnectionsFromType, noSelfConnections} from "../HandleRules.ts"; import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry'; import useFlowStore from '../VisProgStores'; import { TextField } from '../../../../components/TextField'; /** * 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 * @param hasReduce: whether this node has reducing functionality (true by default) * @param nextPhaseId: */ export type PhaseNodeData = { label: string; droppable: boolean; children: string[]; hasReduce: boolean; nextPhaseId: string | "end" | null; isFirstPhase: boolean; }; export type PhaseNode = Node /** * 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 data = props.data; const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore(); const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value}); const label_input_id = `phase_${props.id}_label_input`; const connections = useNodeConnections({ id: props.id, handleType: "target", handleId: 'data' }) const phaseOutCons = useNodeConnections({ id: props.id, handleType: "source", handleId: 'source', }) const phaseInCons = useNodeConnections({ id: props.id, handleType: "target", handleId: 'target', }) useEffect(() => { const noConnectionWarning : EditorWarning = { scope: { id: props.id, handleId: 'data' }, type: 'MISSING_INPUT', severity: "WARNING", description: "the phaseNode has no incoming goals, norms, and/or triggers" } if (connections.length === 0) { registerWarning(noConnectionWarning); return; } unregisterWarning(props.id, `${noConnectionWarning.type}:data`); }, [connections.length, props.id, registerWarning, unregisterWarning]); useEffect(() => { const notConnectedInfo : EditorWarning = { scope: { id: props.id, handleId: undefined, }, type: 'NOT_CONNECTED_TO_PROGRAM', severity: "INFO", description: "The PhaseNode is not connected to other nodes" }; const noIncomingPhaseWarning : EditorWarning = { scope: { id: props.id, handleId: 'target' }, type: 'MISSING_INPUT', severity: "WARNING", description: "the phaseNode has no incoming connection from a phase or the startNode" } const noOutgoingPhaseWarning : EditorWarning = { scope: { id: props.id, handleId: 'source' }, type: 'MISSING_OUTPUT', severity: "WARNING", description: "the phaseNode has no outgoing connection to a phase or the endNode" } // register relevant warning and unregister others if (phaseInCons.length === 0 && phaseOutCons.length === 0) { registerWarning(notConnectedInfo); unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`); unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`); return; } if (phaseOutCons.length === 0) { registerWarning(noOutgoingPhaseWarning); unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`); unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type); return; } if (phaseInCons.length === 0) { registerWarning(noIncomingPhaseWarning); unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`); unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type); return; } // unregister all warnings if none should be present unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type); unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`); unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`); }, [phaseInCons.length, phaseOutCons.length, props.id, registerWarning, unregisterWarning]); const ref = useRef(null); useEffect(() => { if (ref.current) { const { width, height } = ref.current.getBoundingClientRect(); console.log('Node width:', width, 'height:', height); } }, []); return ( <>
); }; /** * Reduces each phase, including its children down into its relevant data. * @param node the node which is being reduced * @param nodes all the nodes currently in the flow. * @returns A collection of all reduced nodes in this phase, starting with this phases' reduced data. */ export function PhaseReduce(node: Node, nodes: Node[]) { const thisNode = node as PhaseNode; const data = thisNode.data as PhaseNodeData; // node typings that are not in phase const nodesNotInPhase: string[] = Object.entries(NodesInPhase) .filter(([, f]) => !f()) .map(([t]) => t); // node typings that then are in phase const nodesInPhase: string[] = Object.entries(NodeTypes) .filter(([t]) => !nodesNotInPhase.includes(t)) .map(([t]) => t); // children nodes - make sure to check for empty arrays let childrenNodes: Node[] = []; if (data.children) childrenNodes = nodes.filter((node) => data.children.includes(node.id)); // Build the result object const result: Record = { id: thisNode.id, name: data.label, }; nodesInPhase.forEach((type) => { const typedChildren = childrenNodes.filter((child) => child.type == type); const reducer = NodeReduces[type as keyof typeof NodeReduces]; if (!reducer) { console.warn(`No reducer found for node type ${type}`); result[type + "s"] = []; } else { result[type + "s"] = []; for (const typedChild of typedChildren) { (result[type + "s"] as object[]).push(reducer(typedChild, nodes)) } } }); return result; } export const PhaseTooltip = ` A phase is a single stage of the program, during a phase Pepper will behave in accordance with any connected norms, goals and triggers`; /** * This function is called whenever a connection is made with this node type as the target (phase) * @param _thisNode the node of this node type which function is called * @param _sourceNodeId the source of the received connection */ export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; const sourceNode = nodes.find((node) => node.id === _sourceNodeId)! switch (sourceNode.type) { case "phase": break; case "start": data.isFirstPhase = true; break; // we only add none phase or start nodes to the children // endNodes cannot be the source of an outgoing connection // so we don't need to cover them with a special case // before handling the default behavior default: data.children.push(_sourceNodeId); break; } } /** * This function is called whenever a connection is made with this node type as the source (phase) * @param _thisNode the node of this node type which function is called * @param _targetNodeId the target of the created connection */ export function PhaseConnectionSource(_thisNode: Node, _targetNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; const targetNode = nodes.find((node) => node.id === _targetNodeId) if (!targetNode) {throw new Error("Source node not found")} // we set the nextPhaseId to the next target's id if the target is a phaseNode, // or "end" if the target node is the end node switch (targetNode.type) { case 'phase': data.nextPhaseId = _targetNodeId; break; case 'end': data.nextPhaseId = "end"; break; default: break; } } /** * This function is called whenever a connection is disconnected with this node type as the target (phase) * @param _thisNode the node of this node type which function is called * @param _sourceNodeId the source of the disconnected connection */ export function PhaseDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; const sourceNode = nodes.find((node) => node.id === _sourceNodeId) const sourceType = sourceNode ? sourceNode.type : "deleted"; switch (sourceType) { case "phase": break; case "start": data.isFirstPhase = false; break; // we only add none phase or start nodes to the children // endNodes cannot be the source of an outgoing connection // so we don't need to cover them with a special case // before handling the default behavior default: data.children = data.children.filter((child) => { if (child != _sourceNodeId) return child; }); break; } } /** * This function is called whenever a connection is disconnected with this node type as the source (phase) * @param _thisNode the node of this node type which function is called * @param _targetNodeId the target of the diconnected connection */ export function PhaseDisconnectionSource(_thisNode: Node, _targetNodeId: string) { const data = _thisNode.data as PhaseNodeData const nodes = useFlowStore.getState().nodes; // if the target is a phase or end node set the nextPhaseId to null, // as we are no longer connected to a subsequent phaseNode or to the endNode if (nodes.some((node) => node.id === _targetNodeId && ['phase', 'end'].includes(node.type!))){ data.nextPhaseId = null; } }