Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | import {
type NodeProps,
Position,
type Node
} from '@xyflow/react';
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<PhaseNodeData>
/**
* 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<PhaseNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
const label_input_id = `phase_${props.id}_label_input`;
return (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodePhase}`}>
<div className={"flex-row gap-sm"}>
<label htmlFor={label_input_id}>Name:</label>
<TextField
id={label_input_id}
value={data.label}
setValue={updateLabel}
placeholder={"Phase ..."}
/>
</div>
<SingleConnectionHandle type="target" position={Position.Left} id="target" rules={[
noSelfConnections,
allowOnlyConnectionsFromType(["phase", "start"]),
]}/>
<MultiConnectionHandle type="target" position={Position.Bottom} id="data" rules={[
allowOnlyConnectionsFromType(["norm", "goal", "trigger"])
]}/>
<SingleConnectionHandle type="source" position={Position.Right} id="source" rules={[
noSelfConnections,
allowOnlyConnectionsFromType(["phase", "end"]),
]}/>
</div>
</>
);
};
/**
* 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<string, unknown> = {
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;
}
} |