Implemented ReactJS Application Server with basic functionality.
This commit is contained in:
78
Data/Experiments/Transparent Nodes/QML/blueprint_grid.qml
Normal file
78
Data/Experiments/Transparent Nodes/QML/blueprint_grid.qml
Normal file
@ -0,0 +1,78 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Shapes 1.15
|
||||
import QtQuick.Window 2.15
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: Screen.width
|
||||
height: Screen.height
|
||||
|
||||
// Grid overlay is enabled at startup.
|
||||
property bool editMode: true
|
||||
|
||||
// Blue gradient background (edges fading inward) with stops shifted inward.
|
||||
Rectangle {
|
||||
id: gradientBackground
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: 0.5
|
||||
gradient: Gradient {
|
||||
// Shifted stops: outer stops moved to 0.1 and 0.9, inner stops to 0.4 and 0.6.
|
||||
GradientStop { position: 0.1; color: Qt.rgba(0, 100/255, 255/255, 0.5) }
|
||||
GradientStop { position: 0.4; color: Qt.rgba(0, 50/255, 180/255, 0.2) }
|
||||
GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) }
|
||||
GradientStop { position: 0.6; color: Qt.rgba(0, 50/255, 180/255, 0.2) }
|
||||
GradientStop { position: 0.9; color: Qt.rgba(0, 100/255, 255/255, 0.5) }
|
||||
}
|
||||
visible: editMode // Only show the gradient in edit mode
|
||||
}
|
||||
|
||||
// Top & Bottom fade remains unchanged.
|
||||
Rectangle {
|
||||
id: topBottomGradient
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: 0.3
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
GradientStop { position: 0.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) }
|
||||
GradientStop { position: 0.3; color: Qt.rgba(0, 50/255, 180/255, 0.1) }
|
||||
GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) }
|
||||
GradientStop { position: 0.7; color: Qt.rgba(0, 50/255, 180/255, 0.1) }
|
||||
GradientStop { position: 1.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) }
|
||||
}
|
||||
visible: editMode
|
||||
}
|
||||
|
||||
// Full-Screen Dynamic Grid with 10% increased transparency (grid lines at 0.3 opacity).
|
||||
Canvas {
|
||||
id: gridCanvas
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"; // Reduced opacity from 0.4 to 0.3.
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
var step = 120; // Grid spacing remains unchanged.
|
||||
|
||||
for (var x = 0; x < width; x += step) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (var y = 0; y < height; y += step) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
Component.onCompleted: requestPaint()
|
||||
onVisibleChanged: requestPaint()
|
||||
visible: editMode // Hide when edit mode is off.
|
||||
}
|
||||
}
|
193
Data/Experiments/Transparent Nodes/blueprint_grid.py
Normal file
193
Data/Experiments/Transparent Nodes/blueprint_grid.py
Normal file
@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import pkgutil
|
||||
import importlib
|
||||
import inspect
|
||||
import types
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget
|
||||
from PyQt5.QtCore import Qt, QUrl, QTimer
|
||||
from PyQt5.QtGui import QGuiApplication
|
||||
from PyQt5.QtQuick import QQuickView
|
||||
|
||||
# OdenGraphQt Fix: Monkey-patch QUndoStack
|
||||
import OdenGraphQt.base.graph as base_graph
|
||||
from PyQt5 import QtWidgets
|
||||
base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack
|
||||
|
||||
import OdenGraphQt.base.commands as base_commands
|
||||
_original_redo = base_commands.NodesRemovedCmd.redo
|
||||
_original_undo = base_commands.NodesRemovedCmd.undo
|
||||
|
||||
def _patched_redo(self):
|
||||
try:
|
||||
_original_redo(self)
|
||||
except TypeError as e:
|
||||
if "unexpected type" in str(e) and hasattr(self, 'node'):
|
||||
node_ids = []
|
||||
if isinstance(self.node, list):
|
||||
node_ids = [getattr(n, 'id', str(n)) for n in self.node]
|
||||
else:
|
||||
node_ids = [getattr(self.node, 'id', str(self.node))]
|
||||
self.graph.nodes_deleted.emit(node_ids)
|
||||
else:
|
||||
raise
|
||||
|
||||
def _patched_undo(self):
|
||||
try:
|
||||
_original_undo(self)
|
||||
except TypeError as e:
|
||||
if "unexpected type" in str(e) and hasattr(self, 'node'):
|
||||
node_ids = []
|
||||
if isinstance(self.node, list):
|
||||
node_ids = [getattr(n, 'id', str(n)) for n in self.node]
|
||||
else:
|
||||
node_ids = [getattr(self.node, 'id', str(self.node))]
|
||||
self.graph.nodes_deleted.emit(node_ids)
|
||||
else:
|
||||
raise
|
||||
|
||||
base_commands.NodesRemovedCmd.redo = _patched_redo
|
||||
base_commands.NodesRemovedCmd.undo = _patched_undo
|
||||
|
||||
# OdenGraphQt Transparent Viewer
|
||||
from OdenGraphQt.widgets.viewer import NodeViewer
|
||||
|
||||
class TransparentViewer(NodeViewer):
|
||||
"""A NodeViewer that does not paint anything in drawBackground() -> Fully transparent."""
|
||||
def drawBackground(self, painter, rect):
|
||||
pass # Do nothing, ensuring transparency.
|
||||
|
||||
# NodeGraph & Node Import Helpers
|
||||
from OdenGraphQt import NodeGraph, BaseNode
|
||||
|
||||
def import_nodes_from_folder(package_name):
|
||||
imported_nodes = []
|
||||
package = importlib.import_module(package_name)
|
||||
for loader, module_name, is_pkg in pkgutil.walk_packages(
|
||||
package.__path__, package.__name__ + "."):
|
||||
module = importlib.import_module(module_name)
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
|
||||
imported_nodes.append(obj)
|
||||
return imported_nodes
|
||||
|
||||
def make_node_command(graph, node_type):
|
||||
def command():
|
||||
try:
|
||||
graph.create_node(node_type)
|
||||
except Exception as e:
|
||||
print(f"Error creating node of type {node_type}: {e}")
|
||||
return command
|
||||
|
||||
# Edit Mode Button
|
||||
class EditButton(QPushButton):
|
||||
"""A small, frameless button to toggle edit mode."""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("Toggle Edit Mode", parent)
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
# Dark gray background with white text.
|
||||
self.setStyleSheet("background-color: #444444; border: 1px solid black; color: white;")
|
||||
self.resize(140, 40)
|
||||
|
||||
# Main Overlay Window
|
||||
class MainWindow(QMainWindow):
|
||||
"""A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Full-screen overlay
|
||||
app = QApplication.instance()
|
||||
screen_geo = app.primaryScreen().geometry()
|
||||
self.setGeometry(screen_geo)
|
||||
|
||||
# Frameless, top-most, fully transparent
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||
|
||||
# QML Background
|
||||
self.qml_view = QQuickView()
|
||||
self.qml_view.setSource(QUrl("qml/background_grid.qml"))
|
||||
self.qml_view.setFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.qml_view.setClearBeforeRendering(True)
|
||||
self.qml_view.setColor(Qt.transparent)
|
||||
self.qml_view.show()
|
||||
|
||||
# Save the QML root object for later property sync
|
||||
self.qml_root = self.qml_view.rootObject()
|
||||
|
||||
# NodeGraph with TransparentViewer
|
||||
self.graph = NodeGraph(viewer=TransparentViewer())
|
||||
self.nodeGraphWidget = self.graph.widget
|
||||
self.nodeGraphWidget.setStyleSheet("background: transparent; border: none;")
|
||||
|
||||
# Transparent central widget
|
||||
central = QWidget(self)
|
||||
central.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||
self.setCentralWidget(central)
|
||||
|
||||
self.nodeGraphWidget.setParent(central)
|
||||
self.nodeGraphWidget.setGeometry(central.rect())
|
||||
|
||||
# Edit Mode Button (Python controlled)
|
||||
self.editButton = EditButton(self)
|
||||
self.editButton.move(10, 10)
|
||||
self.editButton.clicked.connect(self.toggleEditMode)
|
||||
self.isEditMode = True # Set edit mode enabled by default
|
||||
|
||||
# Ensure QML grid overlay is enabled at startup
|
||||
if self.qml_root:
|
||||
self.qml_root.setProperty("editMode", self.isEditMode)
|
||||
|
||||
# Import custom nodes
|
||||
try:
|
||||
custom_nodes = import_nodes_from_folder('Nodes')
|
||||
for node_class in custom_nodes:
|
||||
self.graph.register_node(node_class)
|
||||
|
||||
graph_menu = self.graph.get_context_menu('graph')
|
||||
for node_class in custom_nodes:
|
||||
node_type = f"{node_class.__identifier__}.{node_class.__name__}"
|
||||
node_name = node_class.NODE_NAME
|
||||
graph_menu.add_command(
|
||||
f"Add {node_name}",
|
||||
make_node_command(self.graph, node_type)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error setting up custom nodes: {e}")
|
||||
|
||||
# Global update timer
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.global_update)
|
||||
self.timer.start(500)
|
||||
|
||||
# Timer to ensure the button stays on top (hacky, but effective)
|
||||
self.raiseTimer = QTimer(self)
|
||||
self.raiseTimer.timeout.connect(self.editButton.raise_)
|
||||
self.raiseTimer.start(1000) # Raise the button every 1 second
|
||||
|
||||
self.show()
|
||||
self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode)
|
||||
|
||||
def toggleEditMode(self):
|
||||
"""Toggle edit mode (pass-through clicks vs interactive)."""
|
||||
self.isEditMode = not self.isEditMode
|
||||
self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode)
|
||||
# Button text remains constant.
|
||||
self.editButton.setText("Toggle Edit Mode")
|
||||
if self.qml_root:
|
||||
self.qml_root.setProperty("editMode", self.isEditMode)
|
||||
|
||||
def global_update(self):
|
||||
"""Update all nodes periodically."""
|
||||
for node in self.graph.all_nodes():
|
||||
if hasattr(node, "process_input"):
|
||||
node.process_input()
|
||||
|
||||
# Entry Point
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
160
Data/Experiments/Transparent Nodes/borealis_transparent.py
Normal file
160
Data/Experiments/Transparent Nodes/borealis_transparent.py
Normal file
@ -0,0 +1,160 @@
|
||||
import sys
|
||||
import pkgutil
|
||||
import importlib
|
||||
import inspect
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu
|
||||
from PyQt5.QtCore import Qt, QTimer, QRectF, QPointF
|
||||
from PyQt5.QtGui import QColor, QPainter, QPen, QBrush, QGradient, QLinearGradient
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
from OdenGraphQt import NodeGraph, BaseNode
|
||||
|
||||
# --- Fix Missing QUndoStack in QtGui ---
|
||||
import OdenGraphQt.base.graph as base_graph
|
||||
base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack # Monkey-patch the missing QUndoStack
|
||||
|
||||
# --- Custom Graph Scene ---
|
||||
class CustomGraphScene(QGraphicsScene):
|
||||
"""
|
||||
Custom scene that draws a blueprint-style transparent grid with gradient shading.
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setBackgroundBrush(QtCore.Qt.transparent)
|
||||
self.grid_color = QtGui.QColor(100, 160, 160, 160) # Blueprint grid color (10% more transparent)
|
||||
self.grid_size = 115
|
||||
|
||||
def drawBackground(self, painter, rect):
|
||||
"""
|
||||
Custom draw function to render a blueprint-style grid with gradient shading.
|
||||
"""
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.Antialiasing, False)
|
||||
painter.setBrush(QtCore.Qt.NoBrush) # No background fill
|
||||
pen = QPen(self.grid_color, 0.5)
|
||||
|
||||
left = int(rect.left()) - (int(rect.left()) % self.grid_size)
|
||||
top = int(rect.top()) - (int(rect.top()) % self.grid_size)
|
||||
|
||||
# Draw vertical lines
|
||||
lines = []
|
||||
for x in range(left, int(rect.right()), self.grid_size):
|
||||
lines.append(QtCore.QLineF(x, rect.top(), x, rect.bottom()))
|
||||
|
||||
# Draw horizontal lines
|
||||
for y in range(top, int(rect.bottom()), self.grid_size):
|
||||
lines.append(QtCore.QLineF(rect.left(), y, rect.right(), y))
|
||||
|
||||
painter.setPen(pen)
|
||||
painter.drawLines(lines)
|
||||
|
||||
# Draw gradient shading (top and bottom)
|
||||
gradient = QLinearGradient(QPointF(rect.left(), rect.top()), QPointF(rect.left(), rect.bottom()))
|
||||
gradient.setColorAt(0.0, QColor(0, 40, 100, 220)) # Darker blue at the top
|
||||
gradient.setColorAt(0.5, QColor(0, 0, 0, 0)) # Transparent in the middle
|
||||
gradient.setColorAt(1.0, QColor(0, 40, 100, 220)) # Darker blue at the bottom
|
||||
painter.fillRect(rect, QBrush(gradient))
|
||||
|
||||
painter.restore()
|
||||
|
||||
# --- Node Management ---
|
||||
def import_nodes_from_folder(package_name):
|
||||
imported_nodes = []
|
||||
package = importlib.import_module(package_name)
|
||||
for loader, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
|
||||
module = importlib.import_module(module_name)
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
|
||||
imported_nodes.append(obj)
|
||||
return imported_nodes
|
||||
|
||||
# --- Custom Graph View ---
|
||||
class CustomGraphView(QGraphicsView):
|
||||
"""
|
||||
Custom view for the graph that applies full transparency and handles right-click context menu.
|
||||
"""
|
||||
def __init__(self, scene, graph, parent=None):
|
||||
super().__init__(scene, parent)
|
||||
self.graph = graph # Reference to NodeGraph
|
||||
self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
|
||||
self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setStyleSheet("background: transparent; border: none;")
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
|
||||
|
||||
# Enable context menu on right-click
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||
|
||||
def show_context_menu(self, position):
|
||||
"""
|
||||
Displays the node creation context menu with dynamically loaded nodes.
|
||||
"""
|
||||
menu = QMenu()
|
||||
for node_class in self.graph.registered_nodes():
|
||||
node_name = getattr(node_class, "NODE_NAME", node_class.__name__)
|
||||
menu.addAction(f"Create {node_name}", lambda nc=node_class: self.create_node(nc))
|
||||
menu.exec_(self.mapToGlobal(position))
|
||||
|
||||
def create_node(self, node_class):
|
||||
"""
|
||||
Creates a node instance of the given class in the NodeGraph.
|
||||
"""
|
||||
try:
|
||||
node = self.graph.create_node(f"{node_class.__identifier__}.{node_class.__name__}")
|
||||
print(f"Created node: {node_class.__name__}")
|
||||
except Exception as e:
|
||||
print(f"Error creating node: {e}")
|
||||
|
||||
# --- Main Window ---
|
||||
class MainWindow(QMainWindow):
|
||||
"""A frameless, transparent overlay with a custom graph."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Full-screen overlay
|
||||
app = QApplication.instance()
|
||||
screen_geo = app.primaryScreen().geometry()
|
||||
self.setGeometry(screen_geo)
|
||||
|
||||
# Frameless, top-most, fully transparent
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||
|
||||
# Transparent central widget
|
||||
central = QWidget(self)
|
||||
central.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||
layout = QVBoxLayout(central)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setCentralWidget(central)
|
||||
|
||||
# Initialize NodeGraph
|
||||
self.graph = NodeGraph()
|
||||
|
||||
# Load custom nodes
|
||||
custom_nodes = import_nodes_from_folder('Nodes')
|
||||
for node_class in custom_nodes:
|
||||
self.graph.register_node(node_class)
|
||||
|
||||
# Initialize Custom Graph Scene & View
|
||||
self.scene = CustomGraphScene()
|
||||
self.view = CustomGraphView(self.scene, self.graph, self)
|
||||
layout.addWidget(self.view)
|
||||
|
||||
# Global update timer
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.global_update)
|
||||
self.timer.start(500)
|
||||
|
||||
def global_update(self):
|
||||
"""Update all nodes periodically."""
|
||||
for node in self.graph.all_nodes():
|
||||
if hasattr(node, "process_input"):
|
||||
node.process_input()
|
||||
|
||||
# --- Entry Point ---
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
542
Data/Experiments/borealis_overlay.py
Normal file
542
Data/Experiments/borealis_overlay.py
Normal file
@ -0,0 +1,542 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
import numpy as np
|
||||
import cv2
|
||||
import pytesseract
|
||||
|
||||
try:
|
||||
import winsound
|
||||
HAS_WINSOUND = True
|
||||
except ImportError:
|
||||
HAS_WINSOUND = False
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QWidget
|
||||
from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
|
||||
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
|
||||
from PIL import Image, ImageGrab, ImageFilter
|
||||
|
||||
from rich.console import Console, Group
|
||||
from rich.table import Table
|
||||
from rich.progress import Progress, BarColumn, TextColumn
|
||||
from rich.text import Text
|
||||
from rich.live import Live
|
||||
|
||||
# =============================================================================
|
||||
# Global Config
|
||||
# =============================================================================
|
||||
|
||||
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||
|
||||
POLLING_RATE_MS = 500
|
||||
MAX_DATA_POINTS = 8
|
||||
|
||||
# We still use these defaults for Region size.
|
||||
DEFAULT_WIDTH = 180
|
||||
DEFAULT_HEIGHT = 130
|
||||
HANDLE_SIZE = 8
|
||||
LABEL_HEIGHT = 20
|
||||
|
||||
GREEN_HEADER_STYLE = "bold green"
|
||||
|
||||
BEEP_INTERVAL_SECONDS = 1.0 # Only beep once every 1 second
|
||||
|
||||
# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER)
|
||||
TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png" # Path to your bars template file
|
||||
MATCH_THRESHOLD = 0.4 # The correlation threshold to consider a "good" match
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
def beep_hp_warning():
|
||||
"""
|
||||
Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS).
|
||||
"""
|
||||
current_time = time.time()
|
||||
if (beep_hp_warning.last_beep_time is None or
|
||||
(current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)):
|
||||
|
||||
beep_hp_warning.last_beep_time = current_time
|
||||
if HAS_WINSOUND:
|
||||
# frequency=376 Hz, duration=100 ms
|
||||
winsound.Beep(376, 100)
|
||||
else:
|
||||
# Attempt terminal bell
|
||||
print('\a', end='')
|
||||
|
||||
beep_hp_warning.last_beep_time = None
|
||||
|
||||
|
||||
def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
|
||||
"""
|
||||
Attempt to locate the bars via OpenCV template matching:
|
||||
1) Grab the full screen using PIL.ImageGrab.
|
||||
2) Convert to NumPy array in BGR format for cv2.
|
||||
3) Load template from `template_path`.
|
||||
4) Use cv2.matchTemplate to find the best match location.
|
||||
5) If max correlation > threshold, return (x, y, w, h).
|
||||
6) Else return None.
|
||||
"""
|
||||
# 1) Capture full screen
|
||||
screenshot_pil = ImageGrab.grab()
|
||||
screenshot_np = np.array(screenshot_pil) # shape (H, W, 4) possibly
|
||||
# Convert RGBA or RGB to BGR
|
||||
screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
|
||||
|
||||
# 2) Load template from file
|
||||
template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
|
||||
if template_bgr is None:
|
||||
print(f"[WARN] Could not load template file: {template_path}")
|
||||
return None
|
||||
|
||||
# 3) Template matching
|
||||
result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)
|
||||
|
||||
# 4) Find best match
|
||||
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
|
||||
# template width/height
|
||||
th, tw, _ = template_bgr.shape
|
||||
|
||||
if max_val >= threshold:
|
||||
# max_loc is top-left corner of the best match
|
||||
found_x, found_y = max_loc
|
||||
return (found_x, found_y, tw, th)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def format_duration(seconds):
|
||||
if seconds is None:
|
||||
return "???"
|
||||
seconds = int(seconds)
|
||||
hours = seconds // 3600
|
||||
leftover = seconds % 3600
|
||||
mins = leftover // 60
|
||||
secs = leftover % 60
|
||||
if hours > 0:
|
||||
return f"{hours}h {mins}m {secs}s"
|
||||
else:
|
||||
return f"{mins}m {secs}s"
|
||||
|
||||
|
||||
def sanitize_experience_string(raw_text):
|
||||
text_no_percent = raw_text.replace('%', '')
|
||||
text_no_spaces = text_no_percent.replace(' ', '')
|
||||
cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces)
|
||||
match = re.search(r'\d+(?:\.\d+)?', cleaned)
|
||||
if not match:
|
||||
return None
|
||||
val = float(match.group(0))
|
||||
if val < 0:
|
||||
val = 0
|
||||
elif val > 100:
|
||||
val = 100
|
||||
return round(val, 4)
|
||||
|
||||
|
||||
def format_experience_value(value):
|
||||
if value < 0:
|
||||
value = 0
|
||||
elif value > 100:
|
||||
value = 100
|
||||
float_4 = round(value, 4)
|
||||
raw_str = f"{float_4:.4f}"
|
||||
int_part, dec_part = raw_str.split('.')
|
||||
if int_part == "100":
|
||||
pass
|
||||
elif len(int_part) == 1 and int_part != "0":
|
||||
int_part = "0" + int_part
|
||||
elif int_part == "0":
|
||||
int_part = "00"
|
||||
return f"{int_part}.{dec_part}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Region Class
|
||||
# -----------------------------------------------------------------------------
|
||||
class Region:
|
||||
"""
|
||||
Defines a draggable/resizable screen region for OCR capture.
|
||||
"""
|
||||
def __init__(self, x, y, label="Region", color=QColor(0,0,255)):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = DEFAULT_WIDTH
|
||||
self.h = DEFAULT_HEIGHT
|
||||
self.label = label
|
||||
self.color = color
|
||||
self.visible = True
|
||||
self.data = ""
|
||||
|
||||
def rect(self):
|
||||
return QRect(self.x, self.y, self.w, self.h)
|
||||
|
||||
def label_rect(self):
|
||||
return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT)
|
||||
|
||||
def resize_handles(self):
|
||||
return [
|
||||
QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
|
||||
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
|
||||
QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
|
||||
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# OverlayCanvas Class
|
||||
# -----------------------------------------------------------------------------
|
||||
class OverlayCanvas(QWidget):
|
||||
"""
|
||||
Renders the overlay & handles region dragging/resizing.
|
||||
"""
|
||||
def __init__(self, regions, parent=None):
|
||||
super().__init__(parent)
|
||||
self.regions = regions
|
||||
self.edit_mode = True
|
||||
self.selected_region = None
|
||||
self.selected_handle = None
|
||||
self.drag_offset = QPoint()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
for region in self.regions:
|
||||
if region.visible:
|
||||
pen = QPen(region.color)
|
||||
pen.setWidth(3)
|
||||
painter.setPen(pen)
|
||||
painter.drawRect(region.x, region.y, region.w, region.h)
|
||||
|
||||
painter.setFont(QFont("Arial", 12, QFont.Bold))
|
||||
painter.setPen(region.color)
|
||||
painter.drawText(region.x, region.y - 5, region.label)
|
||||
|
||||
if self.edit_mode:
|
||||
for handle in region.resize_handles():
|
||||
painter.fillRect(handle, region.color)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if not self.edit_mode:
|
||||
return
|
||||
if event.button() == Qt.LeftButton:
|
||||
for region in reversed(self.regions):
|
||||
for i, handle in enumerate(region.resize_handles()):
|
||||
if handle.contains(event.pos()):
|
||||
self.selected_region = region
|
||||
self.selected_handle = i
|
||||
return
|
||||
if region.label_rect().contains(event.pos()):
|
||||
self.selected_region = region
|
||||
self.selected_handle = None
|
||||
self.drag_offset = event.pos() - QPoint(region.x, region.y)
|
||||
return
|
||||
if region.rect().contains(event.pos()):
|
||||
self.selected_region = region
|
||||
self.selected_handle = None
|
||||
self.drag_offset = event.pos() - QPoint(region.x, region.y)
|
||||
return
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if not self.edit_mode or self.selected_region is None:
|
||||
return
|
||||
|
||||
if self.selected_handle is None:
|
||||
self.selected_region.x = event.x() - self.drag_offset.x()
|
||||
self.selected_region.y = event.y() - self.drag_offset.y()
|
||||
else:
|
||||
sr = self.selected_region
|
||||
if self.selected_handle == 0: # top-left
|
||||
sr.w += sr.x - event.x()
|
||||
sr.h += sr.y - event.y()
|
||||
sr.x = event.x()
|
||||
sr.y = event.y()
|
||||
elif self.selected_handle == 1: # top-right
|
||||
sr.w = event.x() - sr.x
|
||||
sr.h += sr.y - event.y()
|
||||
sr.y = event.y()
|
||||
elif self.selected_handle == 2: # bottom-left
|
||||
sr.w += sr.x - event.x()
|
||||
sr.h = event.y() - sr.y
|
||||
sr.x = event.x()
|
||||
elif self.selected_handle == 3: # bottom-right
|
||||
sr.w = event.x() - sr.x
|
||||
sr.h = event.y() - sr.y
|
||||
|
||||
sr.w = max(sr.w, 10)
|
||||
sr.h = max(sr.h, 10)
|
||||
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if not self.edit_mode:
|
||||
return
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.selected_region = None
|
||||
self.selected_handle = None
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# BorealisOverlay Class
|
||||
# -----------------------------------------------------------------------------
|
||||
class BorealisOverlay(QWidget):
|
||||
"""
|
||||
Single Region Overlay for Player Stats (HP/MP/FP/EXP) with:
|
||||
- Automatic location via OpenCV template matching at startup
|
||||
- OCR scanning
|
||||
- Low-HP beep
|
||||
- Rich Live updates in terminal
|
||||
"""
|
||||
def __init__(self, live=None):
|
||||
super().__init__()
|
||||
screen_geo = QApplication.primaryScreen().geometry()
|
||||
self.setGeometry(screen_geo)
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||
|
||||
# Try to find the bars automatically
|
||||
# If found => use that location, else default
|
||||
initial_x, initial_y = 250, 50
|
||||
region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT
|
||||
|
||||
match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD)
|
||||
if match_result is not None:
|
||||
found_x, found_y, w, h = match_result
|
||||
print(f"Character Status Located at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.")
|
||||
initial_x, initial_y = found_x, found_y
|
||||
# Optionally override region size with template size
|
||||
region_w, region_h = w, h
|
||||
else:
|
||||
print("Could not auto-locate the character status page. Set your theme to Masquerade and Interface Scale to 140%, and browser zoom level to 110%. Using default region.")
|
||||
|
||||
region = Region(initial_x, initial_y, label="Character Status")
|
||||
region.w = region_w
|
||||
region.h = region_h
|
||||
self.regions = [region]
|
||||
|
||||
self.canvas = OverlayCanvas(self.regions, self)
|
||||
self.canvas.setGeometry(self.rect())
|
||||
|
||||
# Tesseract
|
||||
self.engine = pytesseract
|
||||
|
||||
# Keep history of EXP data
|
||||
self.points = []
|
||||
|
||||
self.live = live
|
||||
|
||||
# Timer for periodic OCR scanning
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.collect_ocr_data)
|
||||
self.timer.start(POLLING_RATE_MS)
|
||||
|
||||
def set_live(self, live):
|
||||
self.live = live
|
||||
|
||||
def collect_ocr_data(self):
|
||||
for region in self.regions:
|
||||
if region.visible:
|
||||
screenshot = ImageGrab.grab(
|
||||
bbox=(region.x, region.y, region.x + region.w, region.y + region.h)
|
||||
)
|
||||
processed = self.preprocess_image(screenshot)
|
||||
text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
|
||||
region.data = text.strip()
|
||||
|
||||
if self.live is not None:
|
||||
renderable = self.build_renderable()
|
||||
self.live.update(renderable)
|
||||
|
||||
def preprocess_image(self, image):
|
||||
gray = image.convert("L")
|
||||
scaled = gray.resize((gray.width * 3, gray.height * 3))
|
||||
thresh = scaled.point(lambda p: p > 200 and 255)
|
||||
return thresh.filter(ImageFilter.MedianFilter(3))
|
||||
|
||||
def parse_all_stats(self, raw_text):
|
||||
raw_lines = raw_text.splitlines()
|
||||
lines = [l.strip() for l in raw_lines if l.strip()]
|
||||
stats_dict = {
|
||||
"hp": (0,1),
|
||||
"mp": (0,1),
|
||||
"fp": (0,1),
|
||||
"exp": None
|
||||
}
|
||||
if len(lines) < 4:
|
||||
return stats_dict
|
||||
|
||||
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
|
||||
if hp_match:
|
||||
stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2)))
|
||||
|
||||
mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1])
|
||||
if mp_match:
|
||||
stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2)))
|
||||
|
||||
fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2])
|
||||
if fp_match:
|
||||
stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2)))
|
||||
|
||||
exp_val = sanitize_experience_string(lines[3])
|
||||
stats_dict["exp"] = exp_val
|
||||
return stats_dict
|
||||
|
||||
def update_points(self, new_val):
|
||||
now = time.time()
|
||||
if self.points:
|
||||
_, last_v = self.points[-1]
|
||||
if abs(new_val - last_v) < 1e-6:
|
||||
return
|
||||
if new_val < last_v:
|
||||
self.points.clear()
|
||||
self.points.append((now, new_val))
|
||||
if len(self.points) > MAX_DATA_POINTS:
|
||||
self.points.pop(0)
|
||||
|
||||
def compute_time_to_100(self):
|
||||
n = len(self.points)
|
||||
if n < 2:
|
||||
return None
|
||||
first_t, first_v = self.points[0]
|
||||
last_t, last_v = self.points[-1]
|
||||
diff_v = last_v - first_v
|
||||
if diff_v <= 0:
|
||||
return None
|
||||
|
||||
steps = n - 1
|
||||
total_time = last_t - first_t
|
||||
if total_time <= 0:
|
||||
return None
|
||||
|
||||
avg_change = diff_v / steps
|
||||
remain = 100.0 - last_v
|
||||
if remain <= 0:
|
||||
return None
|
||||
|
||||
avg_time = total_time / steps
|
||||
rate_per_s = avg_change / avg_time if avg_time > 0 else 0
|
||||
if rate_per_s <= 0:
|
||||
return None
|
||||
|
||||
return int(remain / rate_per_s)
|
||||
|
||||
def build_renderable(self):
|
||||
raw_text = self.regions[0].data
|
||||
stats = self.parse_all_stats(raw_text)
|
||||
hp_cur, hp_max = stats["hp"]
|
||||
mp_cur, mp_max = stats["mp"]
|
||||
fp_cur, fp_max = stats["fp"]
|
||||
exp_val = stats["exp"]
|
||||
|
||||
# HP beep logic
|
||||
if hp_max > 0:
|
||||
hp_ratio = hp_cur / hp_max
|
||||
if 0 < hp_ratio <= 0.40:
|
||||
beep_hp_warning()
|
||||
|
||||
if exp_val is not None:
|
||||
self.update_points(exp_val)
|
||||
current_exp = self.points[-1][1] if self.points else 0.0
|
||||
|
||||
# Title
|
||||
title_text = Text("Project Borealis\n", style="bold white")
|
||||
subtitle_text = Text("Flyff Information Overlay\n\n", style="dim")
|
||||
|
||||
# HP / MP / FP bars
|
||||
bar_progress = Progress(
|
||||
"{task.description}",
|
||||
BarColumn(bar_width=30),
|
||||
TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"),
|
||||
transient=False,
|
||||
auto_refresh=False
|
||||
)
|
||||
bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur,
|
||||
style="red", complete_style="red")
|
||||
bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur,
|
||||
style="blue", complete_style="blue")
|
||||
bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur,
|
||||
style="green", complete_style="green")
|
||||
bar_progress.refresh()
|
||||
|
||||
# Historical EXP table
|
||||
table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
|
||||
table.add_column("Historical EXP", justify="center", style="green")
|
||||
table.add_column("Time Since Last Kill", justify="center", style="green")
|
||||
table.add_column("Average EXP Per Kill", justify="center", style="green")
|
||||
table.add_column("Average Time Between Kills", justify="center", style="green")
|
||||
|
||||
n = len(self.points)
|
||||
if n == 0:
|
||||
table.add_row("N/A", "N/A", "N/A", "N/A")
|
||||
elif n == 1:
|
||||
_, v0 = self.points[0]
|
||||
exp_str = f"[green]{format_experience_value(v0)}%[/green]"
|
||||
table.add_row(exp_str, "N/A", "N/A", "N/A")
|
||||
else:
|
||||
for i in range(1, n):
|
||||
t_cur, v_cur = self.points[i]
|
||||
t_prev, v_prev = self.points[i - 1]
|
||||
delta_v = v_cur - v_prev
|
||||
delta_str = f"{delta_v:+.4f}%"
|
||||
exp_main = format_experience_value(v_cur)
|
||||
exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]"
|
||||
|
||||
delta_t = t_cur - t_prev
|
||||
t_since_str = f"{delta_t:.1f}s"
|
||||
|
||||
diff_v = v_cur - self.points[0][1]
|
||||
steps = i
|
||||
avg_exp_str = f"{diff_v/steps:.4f}%"
|
||||
|
||||
total_time = t_cur - self.points[0][0]
|
||||
avg_kill_time = total_time / steps
|
||||
avg_time_str = f"{avg_kill_time:.1f}s"
|
||||
|
||||
table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str)
|
||||
|
||||
# Predicted Time to Level
|
||||
secs_left = self.compute_time_to_100()
|
||||
time_str = format_duration(secs_left)
|
||||
|
||||
time_bar = Progress(
|
||||
TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
|
||||
BarColumn(bar_width=30, complete_style="magenta"),
|
||||
TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
|
||||
TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"),
|
||||
transient=False,
|
||||
auto_refresh=False
|
||||
)
|
||||
time_bar.add_task("", total=100, completed=current_exp)
|
||||
time_bar.refresh()
|
||||
|
||||
return Group(
|
||||
title_text,
|
||||
subtitle_text,
|
||||
bar_progress,
|
||||
table,
|
||||
time_bar
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# main
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
"""
|
||||
1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching.
|
||||
2) Position overlay region accordingly if found, else default.
|
||||
3) Start PyQt, periodically OCR the region, update Rich Live in terminal.
|
||||
"""
|
||||
app = QApplication(sys.argv)
|
||||
window = BorealisOverlay()
|
||||
window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
|
||||
window.show()
|
||||
|
||||
console = Console()
|
||||
|
||||
with Live(console=console, refresh_per_second=4) as live:
|
||||
window.set_live(live)
|
||||
exit_code = app.exec_()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
80
Data/Experiments/flowpipe.py
Normal file
80
Data/Experiments/flowpipe.py
Normal file
@ -0,0 +1,80 @@
|
||||
from flask import Flask, jsonify
|
||||
from flowpipe.node import Node
|
||||
from flowpipe.graph import Graph
|
||||
from flowpipe.plug import InputPlug, OutputPlug
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# ===========================
|
||||
# Define Custom Nodes
|
||||
# ===========================
|
||||
|
||||
class MultiplyNode(Node):
|
||||
"""Multiplies an input value by a factor"""
|
||||
factor = InputPlug()
|
||||
value = InputPlug()
|
||||
result = OutputPlug()
|
||||
|
||||
def compute(self):
|
||||
self.result.value = self.value.value * self.factor.value
|
||||
|
||||
|
||||
class AddNode(Node):
|
||||
"""Adds two input values"""
|
||||
input1 = InputPlug()
|
||||
input2 = InputPlug()
|
||||
sum = OutputPlug()
|
||||
|
||||
def compute(self):
|
||||
self.sum.value = self.input1.value + self.input2.value
|
||||
|
||||
|
||||
class OutputNode(Node):
|
||||
"""Outputs the final result"""
|
||||
input_value = InputPlug()
|
||||
output_value = OutputPlug()
|
||||
|
||||
def compute(self):
|
||||
self.output_value.value = self.input_value.value
|
||||
|
||||
|
||||
# ===========================
|
||||
# Define Graph Workflow
|
||||
# ===========================
|
||||
|
||||
def create_workflow():
|
||||
"""Creates a sample workflow using nodes"""
|
||||
graph = Graph(name="Sample Workflow")
|
||||
|
||||
# Create nodes
|
||||
multiply = MultiplyNode(name="Multiplier", graph=graph)
|
||||
add = AddNode(name="Adder", graph=graph)
|
||||
output = OutputNode(name="Output", graph=graph)
|
||||
|
||||
# Connect nodes
|
||||
multiply.result.connect(add.input1) # Multiply output -> Add input1
|
||||
add.sum.connect(output.input_value) # Add output -> Output node
|
||||
|
||||
# Set static input values
|
||||
multiply.factor.value = 2
|
||||
multiply.value.value = 5 # 5 * 2 = 10
|
||||
add.input2.value = 3 # 10 + 3 = 13
|
||||
|
||||
return graph
|
||||
|
||||
|
||||
@app.route('/run-workflow', methods=['GET'])
|
||||
def run_workflow():
|
||||
"""Runs the defined node-based workflow"""
|
||||
graph = create_workflow()
|
||||
graph.evaluate() # Execute the graph
|
||||
|
||||
# Extract the final result from the output node
|
||||
output_node = graph.nodes["Output"]
|
||||
result = output_node.output_value.value
|
||||
|
||||
return jsonify({"workflow_result": result})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
98
Data/Experiments/gui_elements.py
Normal file
98
Data/Experiments/gui_elements.py
Normal file
@ -0,0 +1,98 @@
|
||||
# example_qt_interface.py
|
||||
import sys
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QAction, QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QLabel, QMenuBar, QToolBar, QSplitter, QListWidget,
|
||||
QTextEdit, QStatusBar, QFileDialog, QPushButton
|
||||
)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Example Qt Interface")
|
||||
|
||||
# Create and set up the menu bar.
|
||||
menu_bar = QMenuBar(self)
|
||||
self.setMenuBar(menu_bar)
|
||||
|
||||
# File menu.
|
||||
file_menu = menu_bar.addMenu("File")
|
||||
|
||||
# Create some actions to populate the File menu.
|
||||
open_action = QAction("Open", self)
|
||||
open_action.triggered.connect(self.open_file)
|
||||
file_menu.addAction(open_action)
|
||||
|
||||
save_action = QAction("Save", self)
|
||||
save_action.triggered.connect(self.save_file)
|
||||
file_menu.addAction(save_action)
|
||||
|
||||
exit_action = QAction("Exit", self)
|
||||
exit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(exit_action)
|
||||
|
||||
# Create a toolbar and add some actions.
|
||||
tool_bar = QToolBar("Main Toolbar", self)
|
||||
tool_bar.addAction(open_action)
|
||||
tool_bar.addAction(save_action)
|
||||
self.addToolBar(Qt.TopToolBarArea, tool_bar)
|
||||
|
||||
# Set up a status bar at the bottom.
|
||||
self.setStatusBar(QStatusBar(self))
|
||||
self.statusBar().showMessage("Ready")
|
||||
|
||||
# Create your central widget area.
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
layout = QVBoxLayout(central_widget)
|
||||
|
||||
# A splitter as an example container that can hold multiple widgets side-by-side.
|
||||
splitter = QSplitter()
|
||||
|
||||
# Left side: a simple list widget.
|
||||
self.list_widget = QListWidget()
|
||||
self.list_widget.addItem("Item A")
|
||||
self.list_widget.addItem("Item B")
|
||||
self.list_widget.addItem("Item C")
|
||||
splitter.addWidget(self.list_widget)
|
||||
|
||||
# Right side: a text edit widget.
|
||||
self.text_edit = QTextEdit()
|
||||
self.text_edit.setPlainText("Type here...")
|
||||
splitter.addWidget(self.text_edit)
|
||||
|
||||
layout.addWidget(splitter)
|
||||
|
||||
# Example button in the central widget area.
|
||||
example_button = QPushButton("Click Me")
|
||||
example_button.clicked.connect(self.on_button_clicked)
|
||||
layout.addWidget(example_button)
|
||||
|
||||
def open_file(self):
|
||||
file_name, _ = QFileDialog.getOpenFileName(self, "Open File", "", "All Files (*.*)")
|
||||
if file_name:
|
||||
self.statusBar().showMessage(f"Opened: {file_name}")
|
||||
|
||||
def save_file(self):
|
||||
file_name, _ = QFileDialog.getSaveFileName(self, "Save File", "", "All Files (*.*)")
|
||||
if file_name:
|
||||
self.statusBar().showMessage(f"Saved: {file_name}")
|
||||
|
||||
def on_button_clicked(self):
|
||||
self.statusBar().showMessage("Button clicked!")
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.resize(800, 600)
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
3
Data/WebUI/Canary.txt
Normal file
3
Data/WebUI/Canary.txt
Normal file
@ -0,0 +1,3 @@
|
||||
This file exists to show that the custom reactjs code is being copied into the reactJS app before building it.
|
||||
|
||||
Additional code to flesh out the application will be added in the future.
|
25
Data/WebUI/src/App.js
Normal file
25
Data/WebUI/src/App.js
Normal file
@ -0,0 +1,25 @@
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Borealis Workflow Automation Tool
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
117
Data/server.py
Normal file
117
Data/server.py
Normal file
@ -0,0 +1,117 @@
|
||||
from flask import Flask, send_from_directory, jsonify, request, abort
|
||||
import os
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
# Determine the absolute path for the React build folder
|
||||
build_folder = os.path.join(os.getcwd(), "web-interface", "build")
|
||||
if not os.path.exists(build_folder):
|
||||
print("WARNING: web-interface build folder not found. Please run your React build process to generate 'web-interface/build'.")
|
||||
|
||||
app = Flask(__name__, static_folder=build_folder, static_url_path="/")
|
||||
|
||||
# Directory where nodes are stored
|
||||
NODES_PACKAGE = "Nodes"
|
||||
|
||||
# In-memory workflow storage (a simple scaffold)
|
||||
workflow_data = {
|
||||
"nodes": [],
|
||||
"connections": []
|
||||
}
|
||||
|
||||
def import_nodes_from_folder(package_name):
|
||||
"""
|
||||
Recursively import all modules from the given package.
|
||||
Returns a dictionary where keys are subfolder names, and values are lists of node names.
|
||||
"""
|
||||
nodes_by_category = {}
|
||||
package = importlib.import_module(package_name)
|
||||
package_path = package.__path__[0]
|
||||
|
||||
for root, _, files in os.walk(package_path):
|
||||
rel_path = os.path.relpath(root, package_path).replace(os.sep, ".")
|
||||
module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name
|
||||
category_name = os.path.basename(root)
|
||||
|
||||
for file in files:
|
||||
if file.endswith(".py") and file != "__init__.py":
|
||||
module_name = f"{module_prefix}.{file[:-3]}"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if hasattr(obj, "NODE_NAME"):
|
||||
if category_name not in nodes_by_category:
|
||||
nodes_by_category[category_name] = []
|
||||
nodes_by_category[category_name].append(obj.NODE_NAME)
|
||||
except Exception as e:
|
||||
print(f"Failed to import {module_name}: {e}")
|
||||
|
||||
return nodes_by_category
|
||||
|
||||
@app.route("/")
|
||||
def serve_frontend():
|
||||
index_path = os.path.join(build_folder, "index.html")
|
||||
if os.path.exists(index_path):
|
||||
return send_from_directory(app.static_folder, "index.html")
|
||||
else:
|
||||
return "<h1>React App Not Found</h1><p>Please build the web-interface application.</p>", 404
|
||||
|
||||
@app.route("/api/nodes", methods=["GET"])
|
||||
def get_available_nodes():
|
||||
"""Returns a list of available node categories and node types."""
|
||||
nodes = import_nodes_from_folder(NODES_PACKAGE)
|
||||
return jsonify(nodes)
|
||||
|
||||
@app.route("/api/workflow", methods=["GET"])
|
||||
def get_workflow():
|
||||
"""Returns the current workflow data."""
|
||||
return jsonify(workflow_data)
|
||||
|
||||
@app.route("/api/workflow", methods=["POST"])
|
||||
def save_workflow():
|
||||
"""Saves the workflow data (nodes and connections) sent from the UI."""
|
||||
global workflow_data
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
abort(400, "Invalid workflow data")
|
||||
workflow_data = data
|
||||
return jsonify({"status": "success", "workflow": workflow_data})
|
||||
|
||||
@app.route("/api/node", methods=["POST"])
|
||||
def create_node():
|
||||
"""Creates a new node. Expects JSON with 'nodeType' and optionally 'position' and 'properties'."""
|
||||
data = request.get_json()
|
||||
if not data or "nodeType" not in data:
|
||||
abort(400, "Invalid node data")
|
||||
node = {
|
||||
"id": len(workflow_data["nodes"]) + 1, # simple incremental ID
|
||||
"type": data["nodeType"],
|
||||
"position": data.get("position", {"x": 0, "y": 0}),
|
||||
"properties": data.get("properties", {})
|
||||
}
|
||||
workflow_data["nodes"].append(node)
|
||||
return jsonify({"status": "success", "node": node})
|
||||
|
||||
@app.route("/api/node/<int:node_id>", methods=["DELETE"])
|
||||
def delete_node(node_id):
|
||||
"""Deletes the node with the given ID."""
|
||||
global workflow_data
|
||||
nodes = workflow_data["nodes"]
|
||||
workflow_data["nodes"] = [n for n in nodes if n["id"] != node_id]
|
||||
return jsonify({"status": "success", "deletedNode": node_id})
|
||||
|
||||
@app.route("/api/node/<int:node_id>", methods=["PUT"])
|
||||
def update_node(node_id):
|
||||
"""Updates an existing node's position or properties."""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
abort(400, "Invalid node data")
|
||||
for node in workflow_data["nodes"]:
|
||||
if node["id"] == node_id:
|
||||
node["position"] = data.get("position", node["position"])
|
||||
node["properties"] = data.get("properties", node["properties"])
|
||||
return jsonify({"status": "success", "node": node})
|
||||
abort(404, "Node not found")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
Reference in New Issue
Block a user