Transparent Blueprint Grid Implemented (But Broken)
This commit is contained in:
parent
39ede691d4
commit
faee07b720
114
Borealis.ui
114
Borealis.ui
@ -1,114 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>ProjectBorealis</class>
|
|
||||||
<widget class="QMainWindow" name="ProjectBorealis">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>925</width>
|
|
||||||
<height>698</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Project Borealis - Flyff Information Overlay</string>
|
|
||||||
</property>
|
|
||||||
<property name="autoFillBackground">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="centralwidget">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>10</x>
|
|
||||||
<y>10</y>
|
|
||||||
<width>260</width>
|
|
||||||
<height>41</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<family>Microsoft YaHei UI Light</family>
|
|
||||||
<pointsize>24</pointsize>
|
|
||||||
<weight>75</weight>
|
|
||||||
<bold>true</bold>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Project Borealis</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
<widget class="QTableView" name="tableView">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>391</y>
|
|
||||||
<width>921</width>
|
|
||||||
<height>271</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="frameShape">
|
|
||||||
<enum>QFrame::StyledPanel</enum>
|
|
||||||
</property>
|
|
||||||
<property name="frameShadow">
|
|
||||||
<enum>QFrame::Plain</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>10</x>
|
|
||||||
<y>50</y>
|
|
||||||
<width>211</width>
|
|
||||||
<height>31</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<family>Microsoft YaHei UI Light</family>
|
|
||||||
<pointsize>12</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Flyff Information Overlay</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
<widget class="QTabWidget" name="tabWidget">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>250</x>
|
|
||||||
<y>150</y>
|
|
||||||
<width>401</width>
|
|
||||||
<height>211</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="currentIndex">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="tab">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>Tab 1</string>
|
|
||||||
</attribute>
|
|
||||||
</widget>
|
|
||||||
<widget class="QWidget" name="tab_2">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>Tab 2</string>
|
|
||||||
</attribute>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
<widget class="QLineEdit" name="lineEdit">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>30</x>
|
|
||||||
<y>280</y>
|
|
||||||
<width>113</width>
|
|
||||||
<height>20</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
<widget class="QStatusBar" name="statusbar"/>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
@ -1,192 +0,0 @@
|
|||||||
import signal
|
|
||||||
|
|
||||||
from qtpy import QtWidgets
|
|
||||||
|
|
||||||
from OdenGraphQt import BaseNode, NodeGraph
|
|
||||||
from OdenGraphQt.constants import PortTypeEnum
|
|
||||||
from OdenGraphQt.qgraphics.node_base import NodeItem
|
|
||||||
|
|
||||||
|
|
||||||
class PublishWriteNodeItem(NodeItem):
|
|
||||||
def _align_widgets_horizontal(self, v_offset: int):
|
|
||||||
if not self._widgets:
|
|
||||||
return
|
|
||||||
|
|
||||||
rect = self.boundingRect()
|
|
||||||
y = rect.y() + v_offset
|
|
||||||
for widget in self._widgets.values():
|
|
||||||
if not widget.isVisible():
|
|
||||||
continue
|
|
||||||
|
|
||||||
widget_rect = widget.boundingRect()
|
|
||||||
x = rect.center().x() - (widget_rect.width() / 2)
|
|
||||||
widget.widget().setTitleAlign('center')
|
|
||||||
widget.setPos(x, y)
|
|
||||||
y += widget_rect.height()
|
|
||||||
|
|
||||||
|
|
||||||
class PrevNextNode(BaseNode):
|
|
||||||
__identifier__ = "action"
|
|
||||||
NODE_NAME = "Action Node"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# create an input port.
|
|
||||||
input_port = self.add_input("_prev", color=(180, 80, 0), multi_input=False)
|
|
||||||
# create an output port.
|
|
||||||
output_port = self.add_output("_next", multi_output=False)
|
|
||||||
|
|
||||||
input_port.port_item.set_allow_partial_match_constraint(True)
|
|
||||||
input_port.port_item.set_accept_constraint(
|
|
||||||
port_name=output_port.name(),
|
|
||||||
port_type=PortTypeEnum.OUT.value,
|
|
||||||
node_identifier=self.__identifier__,
|
|
||||||
)
|
|
||||||
|
|
||||||
output_port.port_item.set_allow_partial_match_constraint(True)
|
|
||||||
output_port.port_item.set_accept_constraint(
|
|
||||||
port_name=input_port.name(),
|
|
||||||
port_type=PortTypeEnum.IN.value,
|
|
||||||
node_identifier=self.__identifier__,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IngredientNode(BaseNode):
|
|
||||||
__identifier__ = "ingredient"
|
|
||||||
|
|
||||||
|
|
||||||
class SpamNode(IngredientNode):
|
|
||||||
__identifier__ = "spam"
|
|
||||||
NODE_NAME = "Spam"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
spam_port = self.add_output(
|
|
||||||
"spam",
|
|
||||||
color=(50, 150, 222),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EggNode(IngredientNode):
|
|
||||||
__identifier__ = "egg"
|
|
||||||
NODE_NAME = "Egg"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
egg_port = self.add_output(
|
|
||||||
"egg",
|
|
||||||
color=(50, 150, 222),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MealNode(BaseNode):
|
|
||||||
NODE_NAME = "Meal"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
spam_port = self.add_input("spam", color=(222, 15, 0), multi_input=False)
|
|
||||||
spam_port.port_item.set_reject_constraint(
|
|
||||||
port_name="egg",
|
|
||||||
port_type=PortTypeEnum.OUT.value,
|
|
||||||
node_identifier="egg",
|
|
||||||
)
|
|
||||||
egg_port = self.add_input("egg", color=(222, 15, 0), multi_input=False)
|
|
||||||
egg_port.port_item.set_reject_constraint(
|
|
||||||
port_name="spam",
|
|
||||||
port_type=PortTypeEnum.OUT.value,
|
|
||||||
node_identifier="spam",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BasePublishNode(PrevNextNode):
|
|
||||||
__identifier__ = "publish"
|
|
||||||
allow_multiple_write = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
port = self.add_output(
|
|
||||||
"write",
|
|
||||||
color=(184, 150, 0),
|
|
||||||
multi_output=self.allow_multiple_write,
|
|
||||||
)
|
|
||||||
port.port_item.set_accept_constraint(
|
|
||||||
port_name="src",
|
|
||||||
port_type=PortTypeEnum.IN.value,
|
|
||||||
node_identifier="publish",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PubNode(PrevNextNode):
|
|
||||||
__identifier__ = "pub"
|
|
||||||
NODE_NAME = "Not Tavern"
|
|
||||||
|
|
||||||
|
|
||||||
class PublishFileActionNode(BasePublishNode):
|
|
||||||
NODE_NAME = "Publish File"
|
|
||||||
allow_multiple_write = False
|
|
||||||
|
|
||||||
|
|
||||||
class PublishFileToManyActionNode(BasePublishNode):
|
|
||||||
NODE_NAME = "Publish File to Many"
|
|
||||||
allow_multiple_write = True
|
|
||||||
|
|
||||||
|
|
||||||
class PublishWriteNode(BaseNode):
|
|
||||||
__identifier__ = "publish"
|
|
||||||
NODE_NAME = "Publish Write"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(qgraphics_item=PublishWriteNodeItem)
|
|
||||||
self.set_color(164, 130, 0)
|
|
||||||
self.add_text_input("write", "Path:")
|
|
||||||
|
|
||||||
port = self.add_input("src", multi_input=False)
|
|
||||||
port.port_item.set_accept_constraint(
|
|
||||||
port_name="write",
|
|
||||||
port_type=PortTypeEnum.OUT.value,
|
|
||||||
node_identifier="publish",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
# handle SIGINT to make the app terminate on CTRL+C
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
||||||
|
|
||||||
app = QtWidgets.QApplication([])
|
|
||||||
|
|
||||||
# create graph controller.
|
|
||||||
graph = NodeGraph()
|
|
||||||
|
|
||||||
# set up context menu for the node graph.
|
|
||||||
graph.set_context_menu_from_file('../examples/hotkeys/hotkeys.json')
|
|
||||||
|
|
||||||
# registered example nodes.
|
|
||||||
graph.register_nodes([
|
|
||||||
SpamNode,
|
|
||||||
EggNode,
|
|
||||||
MealNode,
|
|
||||||
PubNode,
|
|
||||||
PublishFileActionNode,
|
|
||||||
PublishFileToManyActionNode,
|
|
||||||
PublishWriteNode,
|
|
||||||
])
|
|
||||||
|
|
||||||
# add nodes
|
|
||||||
graph.add_node(SpamNode())
|
|
||||||
graph.add_node(EggNode())
|
|
||||||
graph.add_node(MealNode())
|
|
||||||
graph.add_node(PubNode())
|
|
||||||
graph.add_node(PublishFileToManyActionNode())
|
|
||||||
graph.add_node(PublishFileActionNode())
|
|
||||||
graph.add_node(PublishWriteNode())
|
|
||||||
graph.auto_layout_nodes()
|
|
||||||
graph.clear_selection()
|
|
||||||
|
|
||||||
# show the node graph widget.
|
|
||||||
graph_widget = graph.widget
|
|
||||||
graph_widget.resize(1100, 800)
|
|
||||||
graph_widget.show()
|
|
||||||
|
|
||||||
app.exec_()
|
|
@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Standalone NodeGraphQT Math Node Example
|
|
||||||
|
|
||||||
This example defines a custom "Math Node" that:
|
|
||||||
- Uses two text inputs for numeric operands (via add_text_input)
|
|
||||||
- Provides a combo box for operator selection (via add_combo_menu)
|
|
||||||
- Offers a checkbox to enable/disable the operation (via add_checkbox)
|
|
||||||
- Computes a result and updates its title accordingly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from NodeGraphQt import NodeGraph, BaseNode
|
|
||||||
|
|
||||||
class MathNode(BaseNode):
|
|
||||||
"""
|
|
||||||
Math Node:
|
|
||||||
- Operands: Two text inputs (Operand 1 and Operand 2)
|
|
||||||
- Operator: Combo box to select 'Add', 'Subtract', 'Multiply', or 'Divide'
|
|
||||||
- Enable: Checkbox to enable/disable the math operation
|
|
||||||
- Output: Result of the math operation (if enabled)
|
|
||||||
"""
|
|
||||||
__identifier__ = 'example.math'
|
|
||||||
NODE_NAME = 'Math Node'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(MathNode, self).__init__()
|
|
||||||
|
|
||||||
# Add two text inputs for operands.
|
|
||||||
self.add_text_input('operand1', 'Operand 1', text='10')
|
|
||||||
self.add_text_input('operand2', 'Operand 2', text='5')
|
|
||||||
|
|
||||||
# Add a combo box for operator selection.
|
|
||||||
self.add_combo_menu('operator', 'Operator', items=['Add', 'Subtract', 'Multiply', 'Divide'])
|
|
||||||
|
|
||||||
# Add a checkbox to enable/disable the operation.
|
|
||||||
self.add_checkbox('enable', 'Enable Operation', state=True)
|
|
||||||
|
|
||||||
# Add an output port to transmit the result.
|
|
||||||
self.add_output('Result')
|
|
||||||
|
|
||||||
self.value = 0
|
|
||||||
self.set_name("Math Node")
|
|
||||||
self.process_input()
|
|
||||||
|
|
||||||
def process_input(self):
|
|
||||||
"""
|
|
||||||
Gather values from the widgets, perform the math operation if enabled,
|
|
||||||
update the node title, and send the result to connected nodes.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
op1 = float(self.get_property('operand1'))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
op1 = 0.0
|
|
||||||
try:
|
|
||||||
op2 = float(self.get_property('operand2'))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
op2 = 0.0
|
|
||||||
|
|
||||||
operator = self.get_property('operator')
|
|
||||||
enable = self.get_property('enable')
|
|
||||||
|
|
||||||
if enable:
|
|
||||||
if operator == 'Add':
|
|
||||||
result = op1 + op2
|
|
||||||
elif operator == 'Subtract':
|
|
||||||
result = op1 - op2
|
|
||||||
elif operator == 'Multiply':
|
|
||||||
result = op1 * op2
|
|
||||||
elif operator == 'Divide':
|
|
||||||
result = op1 / op2 if op2 != 0 else 0.0
|
|
||||||
else:
|
|
||||||
result = 0.0
|
|
||||||
else:
|
|
||||||
result = 0.0
|
|
||||||
|
|
||||||
self.value = result
|
|
||||||
self.set_name(f"Result: {result}")
|
|
||||||
|
|
||||||
output_port = self.output(0)
|
|
||||||
if output_port and output_port.connected_ports():
|
|
||||||
for connected_port in output_port.connected_ports():
|
|
||||||
connected_node = connected_port.node()
|
|
||||||
if hasattr(connected_node, 'receive_data'):
|
|
||||||
connected_node.receive_data(result, source_port_name='Result')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
from PySide2.QtWidgets import QApplication
|
|
||||||
except ImportError:
|
|
||||||
from PySide6.QtWidgets import QApplication
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
graph = NodeGraph()
|
|
||||||
graph.register_node(MathNode)
|
|
||||||
node = graph.create_node('example.math.MathNode', name='Math Node')
|
|
||||||
node.set_pos(100, 100)
|
|
||||||
graph.widget.resize(1200, 800)
|
|
||||||
graph.widget.setWindowTitle("NodeGraphQT Math Node Demo")
|
|
||||||
graph.widget.show()
|
|
||||||
sys.exit(app.exec_())
|
|
@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Collector Process:
|
|
||||||
- Runs the OCR engine.
|
|
||||||
- Updates OCR data every 0.5 seconds.
|
|
||||||
- Exposes the latest data via an HTTP API using Flask.
|
|
||||||
|
|
||||||
This version splits the HP, MP, and FP values into 'current' and 'total' before
|
|
||||||
sending them via the API, so the Character Status Node can ingest them directly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from flask import Flask, jsonify
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
# Global variable to hold the latest stats (HP, MP, FP, EXP)
|
|
||||||
latest_data = {
|
|
||||||
"hp_current": 0,
|
|
||||||
"hp_total": 0,
|
|
||||||
"mp_current": 0,
|
|
||||||
"mp_total": 0,
|
|
||||||
"fp_current": 0,
|
|
||||||
"fp_total": 0,
|
|
||||||
"exp": 0.0000
|
|
||||||
}
|
|
||||||
|
|
||||||
def ocr_collector():
|
|
||||||
"""
|
|
||||||
This function simulates the OCR process.
|
|
||||||
Replace the code below with your actual OCR logic.
|
|
||||||
"""
|
|
||||||
global latest_data
|
|
||||||
counter = 0
|
|
||||||
while True:
|
|
||||||
# Simulate updating stats:
|
|
||||||
hp_current = 50 + counter % 10
|
|
||||||
hp_total = 100
|
|
||||||
mp_current = 30 + counter % 5
|
|
||||||
mp_total = 50
|
|
||||||
fp_current = 20 # fixed, for example
|
|
||||||
fp_total = 20
|
|
||||||
exp_val = round(10.0 + (counter * 0.1), 4)
|
|
||||||
|
|
||||||
latest_data = {
|
|
||||||
"hp_current": hp_current,
|
|
||||||
"hp_total": hp_total,
|
|
||||||
"mp_current": mp_current,
|
|
||||||
"mp_total": mp_total,
|
|
||||||
"fp_current": fp_current,
|
|
||||||
"fp_total": fp_total,
|
|
||||||
"exp": exp_val
|
|
||||||
}
|
|
||||||
|
|
||||||
counter += 1
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
@app.route('/data')
|
|
||||||
def get_data():
|
|
||||||
"""Return the latest OCR data as JSON."""
|
|
||||||
return jsonify(latest_data)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Start the OCR collector in a background thread.
|
|
||||||
collector_thread = threading.Thread(target=ocr_collector)
|
|
||||||
collector_thread.daemon = True
|
|
||||||
collector_thread.start()
|
|
||||||
|
|
||||||
# Run the Flask app on localhost:5000.
|
|
||||||
app.run(host="127.0.0.1", port=5000)
|
|
@ -1,542 +0,0 @@
|
|||||||
#!/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()
|
|
Binary file not shown.
Binary file not shown.
@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
from qtpy import QtCore, QtGui
|
|
||||||
|
|
||||||
from OdenGraphQt import BaseNode
|
|
||||||
|
|
||||||
|
|
||||||
def draw_triangle_port(painter, rect, info):
|
|
||||||
"""
|
|
||||||
Custom paint function for drawing a Triangle shaped port.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
painter (QtGui.QPainter): painter object.
|
|
||||||
rect (QtCore.QRectF): port rect used to describe parameters
|
|
||||||
needed to draw.
|
|
||||||
info (dict): information describing the ports current state.
|
|
||||||
{
|
|
||||||
'port_type': 'in',
|
|
||||||
'color': (0, 0, 0),
|
|
||||||
'border_color': (255, 255, 255),
|
|
||||||
'multi_connection': False,
|
|
||||||
'connected': False,
|
|
||||||
'hovered': False,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
painter.save()
|
|
||||||
|
|
||||||
size = int(rect.height() / 2)
|
|
||||||
triangle = QtGui.QPolygonF()
|
|
||||||
triangle.append(QtCore.QPointF(-size, size))
|
|
||||||
triangle.append(QtCore.QPointF(0.0, -size))
|
|
||||||
triangle.append(QtCore.QPointF(size, size))
|
|
||||||
|
|
||||||
transform = QtGui.QTransform()
|
|
||||||
transform.translate(rect.center().x(), rect.center().y())
|
|
||||||
port_poly = transform.map(triangle)
|
|
||||||
|
|
||||||
# mouse over port color.
|
|
||||||
if info['hovered']:
|
|
||||||
color = QtGui.QColor(14, 45, 59)
|
|
||||||
border_color = QtGui.QColor(136, 255, 35)
|
|
||||||
# port connected color.
|
|
||||||
elif info['connected']:
|
|
||||||
color = QtGui.QColor(195, 60, 60)
|
|
||||||
border_color = QtGui.QColor(200, 130, 70)
|
|
||||||
# default port color
|
|
||||||
else:
|
|
||||||
color = QtGui.QColor(*info['color'])
|
|
||||||
border_color = QtGui.QColor(*info['border_color'])
|
|
||||||
|
|
||||||
pen = QtGui.QPen(border_color, 1.8)
|
|
||||||
pen.setJoinStyle(QtCore.Qt.PenJoinStyle.MiterJoin)
|
|
||||||
|
|
||||||
painter.setPen(pen)
|
|
||||||
painter.setBrush(color)
|
|
||||||
painter.drawPolygon(port_poly)
|
|
||||||
|
|
||||||
painter.restore()
|
|
||||||
|
|
||||||
|
|
||||||
def draw_square_port(painter, rect, info):
|
|
||||||
"""
|
|
||||||
Custom paint function for drawing a Square shaped port.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
painter (QtGui.QPainter): painter object.
|
|
||||||
rect (QtCore.QRectF): port rect used to describe parameters
|
|
||||||
needed to draw.
|
|
||||||
info (dict): information describing the ports current state.
|
|
||||||
{
|
|
||||||
'port_type': 'in',
|
|
||||||
'color': (0, 0, 0),
|
|
||||||
'border_color': (255, 255, 255),
|
|
||||||
'multi_connection': False,
|
|
||||||
'connected': False,
|
|
||||||
'hovered': False,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
painter.save()
|
|
||||||
|
|
||||||
# mouse over port color.
|
|
||||||
if info['hovered']:
|
|
||||||
color = QtGui.QColor(14, 45, 59)
|
|
||||||
border_color = QtGui.QColor(136, 255, 35, 255)
|
|
||||||
# port connected color.
|
|
||||||
elif info['connected']:
|
|
||||||
color = QtGui.QColor(195, 60, 60)
|
|
||||||
border_color = QtGui.QColor(200, 130, 70)
|
|
||||||
# default port color
|
|
||||||
else:
|
|
||||||
color = QtGui.QColor(*info['color'])
|
|
||||||
border_color = QtGui.QColor(*info['border_color'])
|
|
||||||
|
|
||||||
pen = QtGui.QPen(border_color, 1.8)
|
|
||||||
pen.setJoinStyle(QtCore.Qt.PenJoinStyle.MiterJoin)
|
|
||||||
|
|
||||||
painter.setPen(pen)
|
|
||||||
painter.setBrush(color)
|
|
||||||
painter.drawRect(rect)
|
|
||||||
|
|
||||||
painter.restore()
|
|
||||||
|
|
||||||
|
|
||||||
class CustomPortsNode(BaseNode):
|
|
||||||
"""
|
|
||||||
example test node with custom shaped ports.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# set a unique node identifier.
|
|
||||||
__identifier__ = 'nodes.custom.ports'
|
|
||||||
|
|
||||||
# set the initial default node name.
|
|
||||||
NODE_NAME = 'node'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(CustomPortsNode, self).__init__()
|
|
||||||
|
|
||||||
# create input and output port.
|
|
||||||
self.add_input('in', color=(200, 10, 0))
|
|
||||||
self.add_output('default')
|
|
||||||
self.add_output('square', painter_func=draw_square_port)
|
|
||||||
self.add_output('triangle', painter_func=draw_triangle_port)
|
|
BIN
Project_Borealis.zip
Normal file
BIN
Project_Borealis.zip
Normal file
Binary file not shown.
78
QML/background_grid.qml
Normal file
78
QML/background_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.
|
||||||
|
}
|
||||||
|
}
|
@ -1,104 +1,187 @@
|
|||||||
import os
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QMenu, QUndoStack
|
import types
|
||||||
from PyQt5.QtCore import Qt, QTimer, QRect
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget
|
||||||
from PyQt5.QtGui import QColor, QPainter, QPen, QBrush
|
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
|
from OdenGraphQt import NodeGraph, BaseNode
|
||||||
|
|
||||||
# Force qtpy to use PyQt5 explicitly
|
def import_nodes_from_folder(package_name):
|
||||||
os.environ["QT_API"] = "pyqt5"
|
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
|
||||||
|
|
||||||
# --- PATCH OdenGraphQt to use QtWidgets.QUndoStack instead of QtGui.QUndoStack ---
|
def make_node_command(graph, node_type):
|
||||||
import qtpy.QtGui
|
def command():
|
||||||
import qtpy.QtWidgets
|
try:
|
||||||
|
graph.create_node(node_type)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating node of type {node_type}: {e}")
|
||||||
|
return command
|
||||||
|
|
||||||
if not hasattr(qtpy.QtGui, "QUndoStack"): # Ensure patching only if necessary
|
# Edit Mode Button
|
||||||
qtpy.QtGui.QUndoStack = qtpy.QtWidgets.QUndoStack
|
class EditButton(QPushButton):
|
||||||
|
"""A small, frameless button to toggle edit mode."""
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__("Enter Edit Mode", parent)
|
||||||
|
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||||
|
self.setStyleSheet("background-color: rgba(255,255,255,200); border: 1px solid black;")
|
||||||
|
self.resize(140, 40)
|
||||||
|
|
||||||
# --- END PATCH ---
|
# Main Overlay Window
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
class TransparentGraphWindow(QMainWindow):
|
"""A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# Enable transparency & always-on-top behavior
|
# Full-screen overlay
|
||||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
|
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)
|
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
|
||||||
# Get full-screen size for overlay
|
# QML Background
|
||||||
screen_geometry = QApplication.primaryScreen().geometry()
|
self.qml_view = QQuickView()
|
||||||
self.setGeometry(screen_geometry)
|
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()
|
||||||
|
|
||||||
# Create Node Graph
|
# Save the QML root object for later property sync
|
||||||
self.graph = NodeGraph()
|
self.qml_root = self.qml_view.rootObject()
|
||||||
self.graph.widget.setParent(self)
|
|
||||||
self.graph.widget.setGeometry(self.rect())
|
|
||||||
|
|
||||||
# Make bottom-left corner interactive for context menu
|
# NodeGraph with TransparentViewer
|
||||||
self.context_menu_area = QRect(10, self.height() - 40, 50, 30)
|
self.graph = NodeGraph(viewer=TransparentViewer())
|
||||||
|
self.nodeGraphWidget = self.graph.widget
|
||||||
|
self.nodeGraphWidget.setStyleSheet("background: transparent; border: none;")
|
||||||
|
|
||||||
# Load nodes dynamically
|
# Transparent central widget
|
||||||
self.import_nodes()
|
central = QWidget(self)
|
||||||
|
central.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
|
||||||
# Global update timer for processing nodes
|
self.nodeGraphWidget.setParent(central)
|
||||||
self.timer = QTimer()
|
self.nodeGraphWidget.setGeometry(central.rect())
|
||||||
self.timer.timeout.connect(self.update_nodes)
|
|
||||||
|
# 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
|
||||||
|
self.editButton.setText("Exit Edit Mode") # Reflect that grid is active
|
||||||
|
|
||||||
|
# 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)
|
self.timer.start(500)
|
||||||
|
|
||||||
def import_nodes(self):
|
self.show()
|
||||||
"""Dynamically import all custom node classes from the 'Nodes' package."""
|
self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode)
|
||||||
try:
|
|
||||||
package_name = '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__:
|
|
||||||
self.graph.register_node(obj)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading nodes: {e}")
|
|
||||||
|
|
||||||
def update_nodes(self):
|
def toggleEditMode(self):
|
||||||
"""Calls process_input() on all nodes, if applicable."""
|
"""Toggle edit mode (pass-through clicks vs interactive)."""
|
||||||
|
self.isEditMode = not self.isEditMode
|
||||||
|
self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode)
|
||||||
|
self.editButton.setText("Exit Edit Mode" if self.isEditMode else "Enter 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():
|
for node in self.graph.all_nodes():
|
||||||
if hasattr(node, "process_input"):
|
if hasattr(node, "process_input"):
|
||||||
try:
|
node.process_input()
|
||||||
node.process_input()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing node {node}: {e}")
|
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
# Entry Point
|
||||||
"""Override mouse press to handle context menu in bottom-left area."""
|
if __name__ == '__main__':
|
||||||
if event.button() == Qt.RightButton and self.context_menu_area.contains(event.pos()):
|
|
||||||
self.show_context_menu(event.globalPos())
|
|
||||||
|
|
||||||
def show_context_menu(self, position):
|
|
||||||
"""Displays a right-click context menu."""
|
|
||||||
menu = QMenu(self)
|
|
||||||
quit_action = QAction("Quit", self)
|
|
||||||
quit_action.triggered.connect(self.close)
|
|
||||||
menu.addAction(quit_action)
|
|
||||||
menu.exec_(position)
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
"""Render transparent overlay and the small clickable menu area."""
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
|
|
||||||
# Draw semi-transparent context menu area
|
|
||||||
painter.setBrush(QBrush(QColor(50, 50, 50, 150))) # Dark semi-transparent box
|
|
||||||
painter.setPen(QPen(QColor(200, 200, 200, 200)))
|
|
||||||
painter.drawRect(self.context_menu_area)
|
|
||||||
|
|
||||||
painter.setFont(self.font())
|
|
||||||
painter.setPen(QColor(255, 255, 255))
|
|
||||||
painter.drawText(self.context_menu_area, Qt.AlignCenter, "Menu")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
window = TransparentGraphWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
Loading…
x
Reference in New Issue
Block a user