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