From dca79b8556f49d3d5a4711ab3e189c7ae659a926 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Tue, 1 Apr 2025 22:39:44 -0600 Subject: [PATCH] - Added Logic Comparison Node - Added Math Operation Node - Added Data Node - Added (Scaffold) of Backdrop Node - Added Internal Timer Logic for Node Updates - Added Borealis Value Bus to Transmit Data Between Nodes - Updated Powershell Deployment Script - Misc Fixes / Adjustments --- Data/WebUI/src/App.jsx | 45 +++-- .../nodes/Experimental/Experimental_Node.jsx | 20 -- .../src/nodes/Flyff Universe/Flyff_Node.jsx | 20 -- .../src/nodes/General Purpose/Data_Node.jsx | 4 +- .../General Purpose/Logical_Operators.jsx | 173 +++++++++++++++++ .../nodes/General Purpose/Math_Operation.jsx | 177 ++++++++++++++++++ .../nodes/Organization/Backdrop_Group_Box.jsx | 119 ++++++++++++ Launch-Borealis.ps1 | 14 +- 8 files changed, 503 insertions(+), 69 deletions(-) delete mode 100644 Data/WebUI/src/nodes/Experimental/Experimental_Node.jsx delete mode 100644 Data/WebUI/src/nodes/Flyff Universe/Flyff_Node.jsx create mode 100644 Data/WebUI/src/nodes/General Purpose/Logical_Operators.jsx create mode 100644 Data/WebUI/src/nodes/General Purpose/Math_Operation.jsx create mode 100644 Data/WebUI/src/nodes/Organization/Backdrop_Group_Box.jsx diff --git a/Data/WebUI/src/App.jsx b/Data/WebUI/src/App.jsx index e655fb9..6fe5ad7 100644 --- a/Data/WebUI/src/App.jsx +++ b/Data/WebUI/src/App.jsx @@ -1,4 +1,7 @@ +// Core React Imports import React, { useState, useEffect, useCallback, useRef } from "react"; + +// Material UI - Components import { AppBar, Toolbar, @@ -18,31 +21,25 @@ import { DialogContent, DialogContentText, DialogActions, - Divider + Divider, + Tooltip } from "@mui/material"; -import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; -import Tooltip from "@mui/material/Tooltip"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; - -// Icons for Sidebar Workflow Buttons -import SaveIcon from "@mui/icons-material/Save"; -import FileOpenIcon from "@mui/icons-material/FileOpen"; -import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; - -// Info Icon for About Menu -import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; - -// Node Icon (Left-Side of Each Node in Sidebar Menu) -import PolylineIcon from "@mui/icons-material/Polyline"; - -// Gitea Project Icon -import MergeTypeIcon from "@mui/icons-material/MergeType"; - -// Credits Icon -import PeopleIcon from "@mui/icons-material/People"; +// Material UI - Icons +import { + DragIndicator as DragIndicatorIcon, + KeyboardArrowDown as KeyboardArrowDownIcon, + ExpandMore as ExpandMoreIcon, + Save as SaveIcon, + FileOpen as FileOpenIcon, + DeleteForever as DeleteForeverIcon, + InfoOutlined as InfoOutlinedIcon, + Polyline as PolylineIcon, + MergeType as MergeTypeIcon, + People as PeopleIcon +} from "@mui/icons-material"; +// React Flow import ReactFlow, { Background, addEdge, @@ -51,12 +48,14 @@ import ReactFlow, { ReactFlowProvider, useReactFlow } from "reactflow"; + +// Styles import "reactflow/dist/style.css"; import "./Borealis.css"; // Global Node Update Timer Variable if (!window.BorealisUpdateRate) { - window.BorealisUpdateRate = 100; // Default Update Rate: 100ms + window.BorealisUpdateRate = 200; // Default Update Rate: 100ms } const nodeContext = require.context("./nodes", true, /\.jsx$/); diff --git a/Data/WebUI/src/nodes/Experimental/Experimental_Node.jsx b/Data/WebUI/src/nodes/Experimental/Experimental_Node.jsx deleted file mode 100644 index 41e8ba7..0000000 --- a/Data/WebUI/src/nodes/Experimental/Experimental_Node.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { Handle, Position } from "reactflow"; - -const experimentalNode = ({ data }) => { - return ( -
- - -
{data.label || "Experimental Node"}
-
{data.content || "Placeholder Experimental Content"}
-
- ); -}; - -export default { - type: "experimentalNode", - label: "Experimental Node", - defaultContent: "Placeholder Node", - component: experimentalNode -}; diff --git a/Data/WebUI/src/nodes/Flyff Universe/Flyff_Node.jsx b/Data/WebUI/src/nodes/Flyff Universe/Flyff_Node.jsx deleted file mode 100644 index 8580a9d..0000000 --- a/Data/WebUI/src/nodes/Flyff Universe/Flyff_Node.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { Handle, Position } from "reactflow"; - -const flyffNode = ({ data }) => { - return ( -
- - -
{data.label || "Flyff Node"}
-
{data.content || "Placeholder Flyff Content"}
-
- ); -}; - -export default { - type: "flyffNode", - label: "Flyff Node", - defaultContent: "Placeholder Node", - component: flyffNode -}; diff --git a/Data/WebUI/src/nodes/General Purpose/Data_Node.jsx b/Data/WebUI/src/nodes/General Purpose/Data_Node.jsx index 4ae4cc9..6720996 100644 --- a/Data/WebUI/src/nodes/General Purpose/Data_Node.jsx +++ b/Data/WebUI/src/nodes/General Purpose/Data_Node.jsx @@ -176,7 +176,7 @@ const DataNode = ({ id, data }) => { export default { type: "DataNode", // REQUIRED: unique identifier for the node type - label: "Data Node", + label: "String / Number Data", description: ` Foundational Data Node @@ -185,6 +185,6 @@ Foundational Data Node - Pushes value to downstream nodes every X ms - Uses BorealisValueBus to communicate with other nodes `.trim(), - content: "Store Strings, Ints, and Floats", + content: "Store a String or Number", component: DataNode }; diff --git a/Data/WebUI/src/nodes/General Purpose/Logical_Operators.jsx b/Data/WebUI/src/nodes/General Purpose/Logical_Operators.jsx new file mode 100644 index 0000000..3304603 --- /dev/null +++ b/Data/WebUI/src/nodes/General Purpose/Logical_Operators.jsx @@ -0,0 +1,173 @@ +/** + * ============================================== + * Borealis - Comparison Node (Logic Evaluation) + * ============================================== + * + * COMPONENT ROLE: + * This node takes two input values and evaluates them using a selected comparison operator. + * It returns 1 (true) or 0 (false) depending on the result of the comparison. + * + * FEATURES: + * - Dropdown to select input type: "Number" or "String" + * - Dropdown to select comparison operator: ==, !=, >, <, >=, <= + * - Dynamically disables numeric-only operators for string inputs + * - Automatically resets operator to == when switching to String + * - Supports summing multiple inputs per side (A, B) + * - For "String" mode: concatenates inputs in connection order + * - Uses BorealisValueBus for input/output + * - Controlled by global update timer + * + * STRUCTURE: + * - Label and Description + * - Input A (top-left) and Input B (middle-left) + * - Output (right edge) result: 1 (true) or 0 (false) + * - Operator dropdown and Input Type dropdown + */ + +import React, { useEffect, useRef, useState } from "react"; +import { Handle, Position, useReactFlow, useStore } from "reactflow"; + +if (!window.BorealisValueBus) window.BorealisValueBus = {}; +if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100; + +const ComparisonNode = ({ id, data }) => { + const { setNodes } = useReactFlow(); + const edges = useStore(state => state.edges); + + const [inputType, setInputType] = useState(data?.inputType || "Number"); + const [operator, setOperator] = useState(data?.operator || "Equal (==)"); + const [renderValue, setRenderValue] = useState("0"); + const valueRef = useRef("0"); + + useEffect(() => { + if (inputType === "String" && !["Equal (==)", "Not Equal (!=)"].includes(operator)) { + setOperator("Equal (==)"); + } + }, [inputType]); + + useEffect(() => { + let currentRate = window.BorealisUpdateRate; + let intervalId = null; + + const runNodeLogic = () => { + const edgeInputsA = edges.filter(e => e?.target === id && e.targetHandle === "a"); + const edgeInputsB = edges.filter(e => e?.target === id && e.targetHandle === "b"); + + const extractValues = (edgeList) => { + const values = edgeList.map(e => window.BorealisValueBus[e.source]).filter(v => v !== undefined); + if (inputType === "Number") { + return values.reduce((sum, v) => sum + (parseFloat(v) || 0), 0); + } + return values.join(""); + }; + + const a = extractValues(edgeInputsA); + const b = extractValues(edgeInputsB); + + const resultMap = { + "Equal (==)": a === b, + "Not Equal (!=)": a !== b, + "Greater Than (>)": a > b, + "Less Than (<)": a < b, + "Greater Than or Equal (>=)": a >= b, + "Less Than or Equal (<=)": a <= b + }; + + const result = resultMap[operator] ? 1 : 0; + + valueRef.current = result; + setRenderValue(result); + window.BorealisValueBus[id] = result; + + setNodes(nds => + nds.map(n => + n.id === id ? { ...n, data: { ...n.data, value: result, inputType, operator } } : n + ) + ); + }; + + intervalId = setInterval(runNodeLogic, currentRate); + + const monitor = setInterval(() => { + const newRate = window.BorealisUpdateRate; + if (newRate !== currentRate) { + clearInterval(intervalId); + currentRate = newRate; + intervalId = setInterval(runNodeLogic, currentRate); + } + }, 250); + + return () => { + clearInterval(intervalId); + clearInterval(monitor); + }; + }, [id, edges, inputType, operator, setNodes]); + + return ( +
+
A
+
B
+ + + +
+ {data?.label || "Comparison Node"} +
+ +
+
+ {data?.content || "Evaluates A vs B and outputs 1 (true) or 0 (false)."} +
+ + + + + + + +
Result: {renderValue}
+
+ + +
+ ); +}; + +const dropdownStyle = { + width: "100%", + fontSize: "9px", + padding: "4px", + background: "#1e1e1e", + color: "#ccc", + border: "1px solid #444", + borderRadius: "2px", + marginBottom: "4px" +}; + +export default { + type: "ComparisonNode", + label: "Logic Comparison", + description: ` +Compare Two Inputs (A vs B) + +- Uses configurable operator +- Supports numeric and string comparison +- Aggregates multiple inputs by summing (Number) or joining (String in connection order) +- Only == and != are valid for String mode +- Automatically resets operator when switching to String mode +- Outputs 1 (true) or 0 (false) into BorealisValueBus +- Live-updates based on global timer + `.trim(), + content: "Compare A and B using Logic", + component: ComparisonNode +}; diff --git a/Data/WebUI/src/nodes/General Purpose/Math_Operation.jsx b/Data/WebUI/src/nodes/General Purpose/Math_Operation.jsx new file mode 100644 index 0000000..d655852 --- /dev/null +++ b/Data/WebUI/src/nodes/General Purpose/Math_Operation.jsx @@ -0,0 +1,177 @@ +/** + * ============================================ + * Borealis - Math Operation Node (Multi-Input A/B) + * ============================================ + * + * COMPONENT ROLE: + * Performs live math operations on *two grouped input sets* (A and B). + * + * FUNCTIONALITY: + * - Inputs connected to Handle A are summed + * - Inputs connected to Handle B are summed + * - Math operation is applied as: A <operator> B + * - Result pushed via BorealisValueBus[id] + * + * SUPPORTED OPERATORS: + * - Add, Subtract, Multiply, Divide, Average + */ + +import React, { useEffect, useRef, useState } from "react"; +import { Handle, Position, useReactFlow, useStore } from "reactflow"; + +if (!window.BorealisValueBus) window.BorealisValueBus = {}; +if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100; + +const MathNode = ({ id, data }) => { + const { setNodes } = useReactFlow(); + const edges = useStore(state => state.edges); + + const [operator, setOperator] = useState(data?.operator || "Add"); + const [result, setResult] = useState("0"); + const resultRef = useRef(0); + + useEffect(() => { + let intervalId = null; + let currentRate = window.BorealisUpdateRate; + + const runLogic = () => { + const inputsA = edges.filter(e => e.target === id && e.targetHandle === "a"); + const inputsB = edges.filter(e => e.target === id && e.targetHandle === "b"); + + const sum = (list) => + list.map(e => parseFloat(window.BorealisValueBus[e.source]) || 0).reduce((a, b) => a + b, 0); + + const valA = sum(inputsA); + const valB = sum(inputsB); + + let value = 0; + switch (operator) { + case "Add": + value = valA + valB; + break; + case "Subtract": + value = valA - valB; + break; + case "Multiply": + value = valA * valB; + break; + case "Divide": + value = valB !== 0 ? valA / valB : 0; + break; + case "Average": + const totalInputs = inputsA.length + inputsB.length; + const totalSum = valA + valB; + value = totalInputs > 0 ? totalSum / totalInputs : 0; + break; + } + + resultRef.current = value; + setResult(value.toString()); + window.BorealisValueBus[id] = value.toString(); + + setNodes(nds => + nds.map(n => + n.id === id ? { ...n, data: { ...n.data, operator, value: value.toString() } } : n + ) + ); + }; + + intervalId = setInterval(runLogic, currentRate); + + const monitor = setInterval(() => { + const newRate = window.BorealisUpdateRate; + if (newRate !== currentRate) { + clearInterval(intervalId); + currentRate = newRate; + intervalId = setInterval(runLogic, currentRate); + } + }, 250); + + return () => { + clearInterval(intervalId); + clearInterval(monitor); + }; + }, [id, operator, edges, setNodes]); + + return ( +
+
A
+
B
+ + + +
+ {data?.label || "Math Operation"} +
+ +
+
+ Aggregates A and B inputs then performs operation. +
+ + + + + + +
+ + +
+ ); +}; + +const dropdownStyle = { + fontSize: "9px", + padding: "4px", + background: "#1e1e1e", + color: "#ccc", + border: "1px solid #444", + borderRadius: "2px", + width: "100%", + marginBottom: "8px" +}; + +const resultBoxStyle = { + fontSize: "9px", + padding: "4px", + background: "#2a2a2a", + color: "#ccc", + border: "1px solid #444", + borderRadius: "2px", + width: "100%" +}; + +export default { + type: "MathNode", + label: "Math Operation", + description: ` +Perform Math on Aggregated Inputs + +- A and B groups are independently summed +- Performs: Add, Subtract, Multiply, Divide, or Average +- Result = A B +- Emits result via BorealisValueBus every update tick + `.trim(), + content: "Perform Math Operations", + component: MathNode +}; diff --git a/Data/WebUI/src/nodes/Organization/Backdrop_Group_Box.jsx b/Data/WebUI/src/nodes/Organization/Backdrop_Group_Box.jsx new file mode 100644 index 0000000..b130685 --- /dev/null +++ b/Data/WebUI/src/nodes/Organization/Backdrop_Group_Box.jsx @@ -0,0 +1,119 @@ +/** + * =========================================== + * Borealis - Backdrop Group Box Node + * =========================================== + * + * COMPONENT ROLE: + * This node functions as a backdrop or grouping box. + * It's resizable and can be renamed by clicking its title. + * It doesn't connect to other nodes or pass data—it's purely visual. + * + * BEHAVIOR: + * - Allows renaming via single-click on the header text. + * - Can be resized by dragging from the bottom-right corner. + * + * NOTE: + * - No inputs/outputs: purely cosmetic for grouping and labeling. + */ + +import React, { useState, useEffect, useRef } from "react"; +import { Handle, Position } from "reactflow"; +import { ResizableBox } from "react-resizable"; +import "react-resizable/css/styles.css"; + +const BackdropGroupBoxNode = ({ id, data }) => { + const [title, setTitle] = useState(data?.label || "Backdrop Group Box"); + const [isEditing, setIsEditing] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditing]); + + const handleTitleClick = () => { + setIsEditing(true); + }; + + const handleTitleChange = (e) => { + const newTitle = e.target.value; + setTitle(newTitle); + window.BorealisValueBus[id] = newTitle; + }; + + const handleBlur = () => { + setIsEditing(false); + }; + + return ( +
{/* Prevent blocking other nodes */} + e.stopPropagation()} // prevent drag on resize + > +
+ {isEditing ? ( + + ) : ( + {title} + )} +
+
+ {/* Empty space for grouping */} +
+
+
+ ); +}; + +export default { + type: "BackdropGroupBoxNode", + label: "Backdrop Group Box", + description: ` +Resizable Grouping Node + +- Purely cosmetic, for grouping related nodes +- Resizable by dragging bottom-right corner +- Rename by clicking on title bar +`.trim(), + content: "Use as a visual group label", + component: BackdropGroupBoxNode +}; diff --git a/Launch-Borealis.ps1 b/Launch-Borealis.ps1 index 307736f..faf2038 100644 --- a/Launch-Borealis.ps1 +++ b/Launch-Borealis.ps1 @@ -110,7 +110,7 @@ Run-Step "Install Python Dependencies into Virtual Python Environment" { } # ---------------------- Build React App ---------------------- -Run-Step "Install NPM into ReactJS App" { +Run-Step "ReactJS App: Install NPM" { $packageJsonPath = Join-Path $webUIDestination "package.json" if (Test-Path $packageJsonPath) { Push-Location $webUIDestination @@ -120,20 +120,26 @@ Run-Step "Install NPM into ReactJS App" { } } -Run-Step "Install React Flow into ReactJS App" { +Run-Step "ReactJS App: Install React Resizable" { + Push-Location $webUIDestination + npm install react-resizable --no-fund --audit=false | Out-Null + Pop-Location +} + +Run-Step "ReactJS App: Install React Flow" { Push-Location $webUIDestination npm install reactflow --no-fund --audit=false | Out-Null Pop-Location } -Run-Step "Install Material UI Libraries into ReactJS App" { +Run-Step "ReactJS App: Install Material UI Libraries" { Push-Location $webUIDestination $env:npm_config_loglevel = "silent" # Force NPM to be completely silent npm install --silent @mui/material @mui/icons-material @emotion/react @emotion/styled --no-fund --audit=false 2>&1 | Out-Null Pop-Location } -Run-Step "Build ReactJS App" { +Run-Step "ReactJS App: Building App" { Push-Location $webUIDestination #npm run build | Out-Null npm run build