- 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
This commit is contained in:
Nicole Rappe 2025-04-01 22:39:44 -06:00
parent d46c6ecc3c
commit dca79b8556
8 changed files with 503 additions and 69 deletions

View File

@ -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$/);

View File

@ -1,20 +0,0 @@
import React from "react";
import { Handle, Position } from "reactflow";
const experimentalNode = ({ data }) => {
return (
<div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" />
<Handle type="source" position={Position.Right} className="borealis-handle" />
<div className="borealis-node-header">{data.label || "Experimental Node"}</div>
<div className="borealis-node-content">{data.content || "Placeholder Experimental Content"}</div>
</div>
);
};
export default {
type: "experimentalNode",
label: "Experimental Node",
defaultContent: "Placeholder Node",
component: experimentalNode
};

View File

@ -1,20 +0,0 @@
import React from "react";
import { Handle, Position } from "reactflow";
const flyffNode = ({ data }) => {
return (
<div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" />
<Handle type="source" position={Position.Right} className="borealis-handle" />
<div className="borealis-node-header">{data.label || "Flyff Node"}</div>
<div className="borealis-node-content">{data.content || "Placeholder Flyff Content"}</div>
</div>
);
};
export default {
type: "flyffNode",
label: "Flyff Node",
defaultContent: "Placeholder Node",
component: flyffNode
};

View File

@ -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
};

View File

@ -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 (
<div className="borealis-node">
<div style={{ position: "absolute", left: -16, top: 12, fontSize: "8px", color: "#ccc" }}>A</div>
<div style={{ position: "absolute", left: -16, top: 50, fontSize: "8px", color: "#ccc" }}>B</div>
<Handle type="target" position={Position.Left} id="a" style={{ top: 12 }} className="borealis-handle" />
<Handle type="target" position={Position.Left} id="b" style={{ top: 50 }} className="borealis-handle" />
<div className="borealis-node-header">
{data?.label || "Comparison Node"}
</div>
<div className="borealis-node-content">
<div style={{ marginBottom: "6px", fontSize: "9px", color: "#ccc" }}>
{data?.content || "Evaluates A vs B and outputs 1 (true) or 0 (false)."}
</div>
<label style={{ fontSize: "9px" }}>Input Type:</label>
<select value={inputType} onChange={(e) => setInputType(e.target.value)} style={dropdownStyle}>
<option value="Number">Number</option>
<option value="String">String</option>
</select>
<label style={{ fontSize: "9px", marginTop: "6px" }}>Operator:</label>
<select value={operator} onChange={(e) => setOperator(e.target.value)} style={dropdownStyle}>
<option>Equal (==)</option>
<option>Not Equal (!=)</option>
<option disabled={inputType === "String"}>Greater Than (&gt;)</option>
<option disabled={inputType === "String"}>Less Than (&lt;)</option>
<option disabled={inputType === "String"}>Greater Than or Equal (&gt;=)</option>
<option disabled={inputType === "String"}>Less Than or Equal (&lt;=)</option>
</select>
<div style={{ marginTop: "8px", fontSize: "9px" }}>Result: {renderValue}</div>
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
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
};

View File

@ -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 &lt;operator&gt; 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 (
<div className="borealis-node">
<div style={{ position: "absolute", left: -16, top: 12, fontSize: "8px", color: "#ccc" }}>A</div>
<div style={{ position: "absolute", left: -16, top: 50, fontSize: "8px", color: "#ccc" }}>B</div>
<Handle type="target" position={Position.Left} id="a" style={{ top: 12 }} className="borealis-handle" />
<Handle type="target" position={Position.Left} id="b" style={{ top: 50 }} className="borealis-handle" />
<div className="borealis-node-header">
{data?.label || "Math Operation"}
</div>
<div className="borealis-node-content">
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>
Aggregates A and B inputs then performs operation.
</div>
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
Operator:
</label>
<select
value={operator}
onChange={(e) => setOperator(e.target.value)}
style={dropdownStyle}
>
<option value="Add">Add</option>
<option value="Subtract">Subtract</option>
<option value="Multiply">Multiply</option>
<option value="Divide">Divide</option>
<option value="Average">Average</option>
</select>
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
Result:
</label>
<input
type="text"
value={result}
disabled
style={resultBoxStyle}
/>
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
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 <op> B
- Emits result via BorealisValueBus every update tick
`.trim(),
content: "Perform Math Operations",
component: MathNode
};

View File

@ -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 (
<div style={{ pointerEvents: "auto", zIndex: -1 }}> {/* Prevent blocking other nodes */}
<ResizableBox
width={200}
height={120}
minConstraints={[120, 80]}
maxConstraints={[600, 600]}
resizeHandles={["se"]}
style={{
backgroundColor: "rgba(44, 44, 44, 0.5)",
border: "1px solid #3a3a3a",
borderRadius: "4px",
boxShadow: "0 0 5px rgba(88, 166, 255, 0.15)",
overflow: "hidden",
position: "relative"
}}
onClick={(e) => e.stopPropagation()} // prevent drag on resize
>
<div
onClick={handleTitleClick}
style={{
backgroundColor: "rgba(35, 35, 35, 0.5)",
padding: "6px 10px",
fontWeight: "bold",
fontSize: "10px",
cursor: "pointer",
userSelect: "none"
}}
>
{isEditing ? (
<input
ref={inputRef}
type="text"
value={title}
onChange={handleTitleChange}
onBlur={handleBlur}
style={{
fontSize: "10px",
padding: "2px",
background: "#1e1e1e",
color: "#ccc",
border: "1px solid #444",
borderRadius: "2px",
width: "100%"
}}
/>
) : (
<span>{title}</span>
)}
</div>
<div style={{ padding: "10px", fontSize: "9px", height: "100%" }}>
{/* Empty space for grouping */}
</div>
</ResizableBox>
</div>
);
};
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
};

View File

@ -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