diff --git a/Nodes/__init__.py b/Nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Nodes/__pycache__/__init__.cpython-312.pyc b/Nodes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d824339 Binary files /dev/null and b/Nodes/__pycache__/__init__.cpython-312.pyc differ diff --git a/Nodes/__pycache__/array_node.cpython-312.pyc b/Nodes/__pycache__/array_node.cpython-312.pyc new file mode 100644 index 0000000..5a2ef80 Binary files /dev/null and b/Nodes/__pycache__/array_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/average_node.cpython-312.pyc b/Nodes/__pycache__/average_node.cpython-312.pyc new file mode 100644 index 0000000..3227399 Binary files /dev/null and b/Nodes/__pycache__/average_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/character_status_node.cpython-312.pyc b/Nodes/__pycache__/character_status_node.cpython-312.pyc new file mode 100644 index 0000000..eebb7f8 Binary files /dev/null and b/Nodes/__pycache__/character_status_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/convert_to_percent_node.cpython-312.pyc b/Nodes/__pycache__/convert_to_percent_node.cpython-312.pyc new file mode 100644 index 0000000..35b30eb Binary files /dev/null and b/Nodes/__pycache__/convert_to_percent_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/data_node.cpython-312.pyc b/Nodes/__pycache__/data_node.cpython-312.pyc new file mode 100644 index 0000000..f9bfc00 Binary files /dev/null and b/Nodes/__pycache__/data_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/display_node.cpython-312.pyc b/Nodes/__pycache__/display_node.cpython-312.pyc new file mode 100644 index 0000000..ff29655 Binary files /dev/null and b/Nodes/__pycache__/display_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/display_string_node.cpython-312.pyc b/Nodes/__pycache__/display_string_node.cpython-312.pyc new file mode 100644 index 0000000..425735b Binary files /dev/null and b/Nodes/__pycache__/display_string_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/display_text_node.cpython-312.pyc b/Nodes/__pycache__/display_text_node.cpython-312.pyc new file mode 100644 index 0000000..edc29d3 Binary files /dev/null and b/Nodes/__pycache__/display_text_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/display_value_node.cpython-312.pyc b/Nodes/__pycache__/display_value_node.cpython-312.pyc new file mode 100644 index 0000000..7ff124e Binary files /dev/null and b/Nodes/__pycache__/display_value_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/multiply_node.cpython-312.pyc b/Nodes/__pycache__/multiply_node.cpython-312.pyc new file mode 100644 index 0000000..3540a7c Binary files /dev/null and b/Nodes/__pycache__/multiply_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/number_input_node.cpython-312.pyc b/Nodes/__pycache__/number_input_node.cpython-312.pyc new file mode 100644 index 0000000..1e03916 Binary files /dev/null and b/Nodes/__pycache__/number_input_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/subtract_node.cpython-312.pyc b/Nodes/__pycache__/subtract_node.cpython-312.pyc new file mode 100644 index 0000000..6705f01 Binary files /dev/null and b/Nodes/__pycache__/subtract_node.cpython-312.pyc differ diff --git a/Nodes/array_node.py b/Nodes/array_node.py new file mode 100644 index 0000000..b069fe2 --- /dev/null +++ b/Nodes/array_node.py @@ -0,0 +1,49 @@ +from NodeGraphQt import BaseNode + +class ArrayNode(BaseNode): + """ + Array Node: + - Inputs: 'in' (value to store), 'ArraySize' (defines maximum length) + - Output: 'Array' (the current array as a string) + - Stores incoming values in an array with size defined by ArraySize. + When full, it removes the oldest value. + """ + __identifier__ = 'io.github.nicole.array' + NODE_NAME = 'Array' + + def __init__(self): + super(ArrayNode, self).__init__() + self.values = {} # Ensure values is a dictionary. + self.add_input('in') + self.add_input('ArraySize') + self.add_output('Array') + self.array = [] + self.value = "[]" # Output as a string. + self.array_size = 10 # Default array size. + self.set_name("Array: []") + + def process_input(self): + # Get array size from 'ArraySize' input if available. + size_port = self.input('ArraySize') + connected_size = size_port.connected_ports() if size_port is not None else [] + if connected_size: + connected_port = connected_size[0] + parent_node = connected_port.node() + try: + self.array_size = int(float(getattr(parent_node, 'value', 10))) + except (ValueError, TypeError): + self.array_size = 10 + + # Get new value from 'in' input if available. + in_port = self.input('in') + connected_in = in_port.connected_ports() if in_port is not None else [] + if connected_in: + connected_port = connected_in[0] + parent_node = connected_port.node() + new_value = getattr(parent_node, 'value', None) + if new_value is not None: + self.array.append(new_value) + while len(self.array) > self.array_size: + self.array.pop(0) + self.value = str(self.array) + self.set_name(f"Array: {self.value}") diff --git a/Nodes/average_node.py b/Nodes/average_node.py new file mode 100644 index 0000000..2f8be35 --- /dev/null +++ b/Nodes/average_node.py @@ -0,0 +1,36 @@ +from NodeGraphQt import BaseNode + +class AverageNode(BaseNode): + """ + Average Node: + - Inputs: A, B, C (adjustable as needed) + - Output: Result (the average of the inputs) + """ + __identifier__ = 'io.github.nicole.average' + NODE_NAME = 'Average' + + def __init__(self): + super(AverageNode, self).__init__() + self.values = {} # Ensure values is a dictionary. + self.add_input('A') + self.add_input('B') + self.add_input('C') + self.add_output('Result') + self.value = 0 + self.set_name("Average: 0") + + def process_input(self): + values = [] + for port_name in ['A', 'B', 'C']: + port = self.input(port_name) + connected = port.connected_ports() if port is not None else [] + if connected: + connected_port = connected[0] + parent_node = connected_port.node() + try: + values.append(float(getattr(parent_node, 'value', 0))) + except (ValueError, TypeError): + pass + avg = sum(values) / len(values) if values else 0 + self.value = avg + self.set_name(f"Average: {avg}") diff --git a/Nodes/character_status_node.py b/Nodes/character_status_node.py new file mode 100644 index 0000000..f48e256 --- /dev/null +++ b/Nodes/character_status_node.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Character Status Node + +This node represents the character's status. It has no input ports and four output ports: + - HP, MP, FP, EXP. +It polls an API endpoint (http://127.0.0.1:5000/data) every 500 ms to update its values. +If the API call is successful, the node's title is set to "Character Status (API Connected)". +If the API is down or returns an error, the title is set to "Character Status (API Disconnected)". +""" + +from NodeGraphQt import BaseNode +from Qt import QtCore, QtGui +import requests + +def get_draw_stat_port(color, border_color=None, alpha=127): + """ + Returns a custom port painter function that draws a circular port with a + semi-transparent fill and then draws text (port label and current value) + next to it. + """ + if border_color is None: + border_color = color + + def painter_func(painter, rect, info): + painter.save() + # Draw the port circle. + pen = QtGui.QPen(QtGui.QColor(*border_color)) + pen.setWidth(1.8) + painter.setPen(pen) + semi_transparent_color = QtGui.QColor(color[0], color[1], color[2], alpha) + painter.setBrush(semi_transparent_color) + painter.drawEllipse(rect) + # Draw the label and current value. + port = info.get('port') + if port is not None: + node = port.node() + stat = port.name() + # Use the node's 'values' dictionary if available. + value = node.values.get(stat, "N/A") if hasattr(node, 'values') else "N/A" + text_rect = rect.adjusted(rect.width() + 4, 0, rect.width() + 70, 0) + painter.setPen(QtGui.QColor(0, 0, 0)) + painter.drawText(text_rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, + f"{stat}: {value}") + painter.restore() + return painter_func + +class CharacterStatusNode(BaseNode): + __identifier__ = 'io.github.nicole.status' + NODE_NAME = 'Character Status' + + def __init__(self): + super(CharacterStatusNode, self).__init__() + # Initialize the output values as a dictionary. + self.values = {"HP": "N/A", "MP": "N/A", "FP": "N/A", "EXP": "N/A"} + # Add output ports for each stat with custom painters. + self.add_output("HP", painter_func=get_draw_stat_port((255, 0, 0))) # Red for HP + self.add_output("MP", painter_func=get_draw_stat_port((0, 0, 255))) # Blue for MP + self.add_output("FP", painter_func=get_draw_stat_port((0, 255, 0))) # Green for FP + self.add_output("EXP", painter_func=get_draw_stat_port((127, 255, 212))) # Aquamarine for EXP + # Set an initial title. + self.set_name("Character Status (API Disconnected)") + # Create a QTimer that polls the API every 500ms. + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.poll_api) + self.timer.start(500) + + def poll_api(self): + """ + Polls the API endpoint to retrieve the latest character stats and updates + the node's internal values. Expects a JSON response with keys: + - "hp", "mp", "fp", "exp" + """ + try: + response = requests.get("http://127.0.0.1:5000/data", timeout=1) + if response.status_code == 200: + data = response.json() + # Update the values dictionary. + self.values["HP"] = data.get("hp", "0/0") + self.values["MP"] = data.get("mp", "0/0") + self.values["FP"] = data.get("fp", "0/0") + self.values["EXP"] = data.get("exp", "0.0000") + self.set_name("Character Status (API Connected)") + self.update() + else: + self.set_name("Character Status (API Disconnected)") + except Exception as e: + self.set_name("Character Status (API Disconnected)") + print("Error polling API in CharacterStatusNode:", e) diff --git a/Nodes/convert_to_percent_node.py b/Nodes/convert_to_percent_node.py new file mode 100644 index 0000000..7d309ef --- /dev/null +++ b/Nodes/convert_to_percent_node.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Convert to Percent Node + +This node takes an input string formatted as "numerator/denom" (e.g., "50/50"), +splits it into two numbers, computes (numerator / denom) * 100, and outputs the result +as a float formatted to 4 decimal places. If an error occurs, an error message is stored. +The node's title is always "Convert to Percent". +""" + +from NodeGraphQt import BaseNode +from Qt import QtCore + +class ConvertToPercentNode(BaseNode): + __identifier__ = 'io.github.nicole.convert' + NODE_NAME = 'Convert to Percent' + + def __init__(self): + super(ConvertToPercentNode, self).__init__() + # Add one input port (expects a string in "numerator/denom" format). + self.add_input("in") + # Add one output port. + self.add_output("Percent") + # Initialize internal value. + self.value = "No Input" + # Set the node title to a static string. + self.set_name(self.NODE_NAME) + # Initialize a values dictionary so that connected Display nodes can read output. + self.values = {} + + def process_input(self): + input_port = self.input(0) + connected_ports = input_port.connected_ports() if input_port is not None else [] + if connected_ports: + connected_output = connected_ports[0] + parent_node = connected_output.node() + port_name = connected_output.name() + # Use parent's values dictionary if available, else use its value attribute. + if hasattr(parent_node, 'values') and isinstance(parent_node.values, dict): + input_value = parent_node.values.get(port_name, "") + else: + input_value = getattr(parent_node, 'value', "") + input_str = str(input_value).strip() + try: + parts = input_str.split('/') + if len(parts) != 2: + raise ValueError("Input must be in the format 'num/denom'") + numerator = float(parts[0].strip()) + denominator = float(parts[1].strip()) + if denominator == 0: + raise ZeroDivisionError("Division by zero") + percent = (numerator / denominator) * 100 + formatted_percent = f"{percent:.4f}" + self.value = formatted_percent + except Exception as e: + self.value = f"Error: {e}" + else: + self.value = "No Input" + # Always keep the title static. + self.set_name(self.NODE_NAME) + # Store the computed value in the values dictionary under the output port key. + self.values["Percent"] = self.value diff --git a/Nodes/data_node.py b/Nodes/data_node.py new file mode 100644 index 0000000..5828d5b --- /dev/null +++ b/Nodes/data_node.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +""" +Data Node: + - Input: Accepts a value (string, integer, or float) from an input port. + - Output: Outputs the current value, either from the input port or set manually via a text box. + +Behavior: +- If both input and output are connected: + - Acts as a passthrough, displaying the input value and transmitting it to the output. + - Manual input is disabled. +- If only the output is connected: + - Allows manual value entry, which is sent to the output. +- If only the input is connected: + - Displays the input value but does not transmit it further. +""" + +from NodeGraphQt import BaseNode + +class DataNode(BaseNode): + __identifier__ = 'io.github.nicole.data' + NODE_NAME = 'Data Node' + + def __init__(self): + super(DataNode, self).__init__() + # Add input and output ports. + self.add_input('Input') + self.add_output('Output') + # Add a text input widget for manual entry. + self.add_text_input('value', 'Value', text='') + # Initialize the value from the widget property. + self.process_widget_event() + self.set_name(f"Data Node: {self.value}") + + def post_create(self): + """ + Called after the node's widget is fully created. + Connect the text input widget's textChanged signal to process_widget_event. + """ + text_widget = self.get_widget('value') + if text_widget is not None: + try: + # Connect the textChanged signal if available. + text_widget.textChanged.connect(self.process_widget_event) + except Exception as e: + print("Error connecting textChanged signal:", e) + + def process_widget_event(self, event=None): + """ + Reads the current text from the node's property and updates the node's internal value. + """ + current_text = self.get_property('value') + self.value = current_text + self.set_name(f"Data Node: {self.value}") + + def property_changed(self, property_name): + """ + Called when a node property changes. If the 'value' property changes, + update the internal value. + """ + if property_name == 'value': + self.process_widget_event() + + def update_stream(self): + """ + Updates the node's behavior based on the connection states. + """ + input_port = self.input(0) + output_port = self.output(0) + + if input_port.connected_ports() and output_port.connected_ports(): + # Both input and output are connected; act as passthrough. + self.set_property('value', '') + self.get_widget('value').setEnabled(False) + input_value = input_port.connected_ports()[0].node().get_property('value') + self.set_property('value', input_value) + output_port.send_data(input_value) + elif output_port.connected_ports(): + # Only output is connected; allow manual input. + self.get_widget('value').setEnabled(True) + output_port.send_data(self.get_property('value')) + elif input_port.connected_ports(): + # Only input is connected; display input value. + self.get_widget('value').setEnabled(False) + input_value = input_port.connected_ports()[0].node().get_property('value') + self.set_property('value', input_value) + else: + # Neither input nor output is connected; allow manual input. + self.get_widget('value').setEnabled(True) + + def on_input_connected(self, input_port, output_port): + """ + Called when an input port is connected. + """ + self.update_stream() + + def on_input_disconnected(self, input_port, output_port): + """ + Called when an input port is disconnected. + """ + self.update_stream() + + def on_output_connected(self, output_port, input_port): + """ + Called when an output port is connected. + """ + self.update_stream() + + def on_output_disconnected(self, output_port, input_port): + """ + Called when an output port is disconnected. + """ + self.update_stream() diff --git a/Nodes/multiply_node.py b/Nodes/multiply_node.py new file mode 100644 index 0000000..c5211c9 --- /dev/null +++ b/Nodes/multiply_node.py @@ -0,0 +1,36 @@ +from NodeGraphQt import BaseNode + +class MultiplyNode(BaseNode): + """ + Multiply Node: + - Inputs: A, B + - Output: Result (A * B) + """ + __identifier__ = 'io.github.nicole.multiply' + NODE_NAME = 'Multiply' + + def __init__(self): + super(MultiplyNode, self).__init__() + self.values = {} # Ensure values is a dictionary. + self.add_input('A') + self.add_input('B') + self.add_output('Result') + self.value = 0 + + def process_input(self): + inputs = {} + for port_name in ['A', 'B']: + port = self.input(port_name) + connected = port.connected_ports() if port is not None else [] + if connected: + connected_port = connected[0] + parent_node = connected_port.node() + try: + inputs[port_name] = float(getattr(parent_node, 'value', 0)) + except (ValueError, TypeError): + inputs[port_name] = 0.0 + else: + inputs[port_name] = 0.0 + result = inputs['A'] * inputs['B'] + self.value = result + self.set_name(f"Multiply: {result}") diff --git a/Nodes/subtract_node.py b/Nodes/subtract_node.py new file mode 100644 index 0000000..08d37af --- /dev/null +++ b/Nodes/subtract_node.py @@ -0,0 +1,37 @@ +# Nodes/subtract_node.py + +from NodeGraphQt import BaseNode + +class SubtractNode(BaseNode): + """ + Subtract Node: + - Inputs: A, B + - Output: Result (A - B) + """ + __identifier__ = 'io.github.nicole.subtract' + NODE_NAME = 'Subtract' + + def __init__(self): + super(SubtractNode, self).__init__() + self.add_input('A') + self.add_input('B') + self.add_output('Result') + self.value = 0 + + def process_input(self): + inputs = {} + for port_name in ['A', 'B']: + port = self.input(port_name) + connected = port.connected_ports() if port is not None else [] + if connected: + connected_port = connected[0] + parent_node = connected_port.node() + try: + inputs[port_name] = float(getattr(parent_node, 'value', 0)) + except (ValueError, TypeError): + inputs[port_name] = 0.0 + else: + inputs[port_name] = 0.0 + result = inputs['A'] - inputs['B'] + self.value = result + self.set_name(f"Subtract: {result}") diff --git a/flow_UI.py b/flow_UI.py index 557ce77..ea6f2a9 100644 --- a/flow_UI.py +++ b/flow_UI.py @@ -1,239 +1,117 @@ #!/usr/bin/env python3 """ -Modified Flow UI Application: -- The character node is now named "Character Status" (display title). -- DisplayValueNodes dynamically update their title to show the value from the connected output port. -- Newly spawned DisplayValueNodes (via the context menu) follow the same dynamic behavior. -- Node creation strings now include the class name to match NodeGraphQt requirements. +Main Application (flow_UI.py) + +This file dynamically imports custom node classes from the 'Nodes' package, +registers them with NodeGraphQt, and sets up an empty graph. +Nodes can be added dynamically via the graph’s right-click context menu, +and a "Remove Selected Node" option is provided. +A global update timer periodically calls process_input() on nodes. +Additionally, this file patches QGraphicsScene.setSelectionArea to handle +selection behavior properly (so that multiple nodes can be selected). """ -import sys -import requests +# --- Patch QGraphicsScene.setSelectionArea to handle selection arguments --- from Qt import QtWidgets, QtCore, QtGui + +_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea + +def _patched_setSelectionArea(self, painterPath, second_arg, *args, **kwargs): + try: + # Try calling the original method with the provided arguments. + return _original_setSelectionArea(self, painterPath, second_arg, *args, **kwargs) + except TypeError as e: + # If a TypeError is raised, assume the call was made with only a QPainterPath + # and an ItemSelectionMode, and patch it by supplying defaults. + # Default operation: ReplaceSelection, default transform: QTransform() + return _original_setSelectionArea(self, painterPath, + QtCore.Qt.ReplaceSelection, + second_arg, + QtGui.QTransform()) + +# Monkey-patch the setSelectionArea method. +QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea + +# --- End of patch section --- + +import sys +import pkgutil +import importlib +import inspect +from Qt import QtWidgets, QtCore from NodeGraphQt import NodeGraph, BaseNode -# ----------------------------------------------------------------------------- -# Custom Port Painter Function for Character Status Node Ports (Semi-Transparent) -# ----------------------------------------------------------------------------- -def get_draw_stat_port(color, border_color=None, alpha=127): +def import_nodes_from_folder(package_name): """ - Returns a custom port painter function that draws a circular port with a semi-transparent fill, - then draws text (port label and current value) next to it. - - Args: - color (tuple): RGB tuple for the fill color. - border_color (tuple, optional): RGB tuple for the border color. - Defaults to the same as color if not provided. - alpha (int, optional): Alpha value (0-255) for transparency. Default is 127. + Dynamically import all modules from the given package and return a list of + classes that subclass BaseNode. """ - if border_color is None: - border_color = color + 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 painter_func(painter, rect, info): - painter.save() - # --- Section: Port Drawing --- - pen = QtGui.QPen(QtGui.QColor(*border_color)) - pen.setWidth(1.8) - painter.setPen(pen) - # Create semi-transparent fill using the provided alpha value. - semi_transparent_color = QtGui.QColor(color[0], color[1], color[2], alpha) - painter.setBrush(semi_transparent_color) - painter.drawEllipse(rect) - # --- Section: Port Label and Value Drawing --- - port = info.get('port') - if port is not None: - node = port.node() # Parent node (e.g. Character Status) - stat = port.name() # Port label (e.g. "HP") - # Retrieve the current value from the node's values dict (if available) - value = node.values.get(stat, "N/A") if hasattr(node, 'values') else "N/A" - # Create a text rectangle to the right of the port. - text_rect = rect.adjusted(rect.width() + 4, 0, rect.width() + 70, 0) - painter.setPen(QtGui.QColor(0, 0, 0)) - painter.drawText(text_rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, - f"{stat}: {value}") - painter.restore() - return painter_func - -# ----------------------------------------------------------------------------- -# Custom Node Classes -# ----------------------------------------------------------------------------- - -class CharacterStatusNode(BaseNode): +def make_node_command(graph, nt): """ - Character Status Node - - This node represents the character's current status. - It has no input ports and four output ports: "HP", "MP", "FP", and "EXP". - The current values are stored in self.values and are drawn using a custom painter. + Given a NodeGraph instance and a node type string nt, return a command + function that creates a node of that type. """ - __identifier__ = 'io.github.nicole.status' - NODE_NAME = 'Character Status' + def command(): + try: + node = graph.create_node(nt) + # (No need to force top-level if nodes are created as top-level by default.) + except Exception as e: + print(f"Error creating node of type {nt}: {e}") + return command - def __init__(self): - super(CharacterStatusNode, self).__init__() - # --- Section: Initialization of Stat Values and Ports --- - self.values = {"HP": "N/A", "MP": "N/A", "FP": "N/A", "EXP": "N/A"} - self.add_output("HP", painter_func=get_draw_stat_port((255, 0, 0))) # Red for HP - self.add_output("MP", painter_func=get_draw_stat_port((0, 0, 255))) # Blue for MP - self.add_output("FP", painter_func=get_draw_stat_port((0, 255, 0))) # Green for FP - self.add_output("EXP", painter_func=get_draw_stat_port((127, 255, 212))) # Aquamarine for EXP - # Set the node title to "Character Status" - self.set_name("Character Status") - - def update_value(self, stat, value): - """ - Update the value for a given stat. - - Args: - stat (str): Stat name (e.g., "HP", "MP", etc.). - value (str): New value for the stat. - """ - self.values[stat] = value - -class DisplayValueNode(BaseNode): - """ - Display Value Node - - This node displays a single input value. - Its title is updated to show the value from the connected output port. - If no port is connected, it displays "Display Value: No Input Configured". - """ - __identifier__ = 'io.github.nicole.display' - NODE_NAME = 'Display Value Node' - - def __init__(self): - super(DisplayValueNode, self).__init__() - # --- Section: Initialization and Default State --- - self.add_input("in") - self.add_output("out") - self.value = "No Input Configured" - self.update_display() - - def update_display(self): - """ - Update the node's title to reflect the current displayed value. - """ - self.set_name(f"Display Value: {self.value}") - - def process_input(self): - """ - Processes the input connection: - - If connected, retrieve the value from the connected node's output port. - - If not, set the value to "No Input Configured". - """ - input_port = self.input(0) - connections = input_port.connections() if input_port is not None else [] - if connections: - # --- Section: Processing Connected Input --- - connection = connections[0] - out_port = connection.out_port() - if out_port: - parent_node = out_port.node() - port_name = out_port.name() - self.value = parent_node.values.get(port_name, "N/A") - else: - self.value = "No Input Configured" - else: - # --- Section: No Connection Found --- - self.value = "No Input Configured" - self.update_display() - -# ----------------------------------------------------------------------------- -# API Polling Functions -# ----------------------------------------------------------------------------- -def poll_api(): - """ - Polls the Flask API for JSON data. - Expected JSON format: {"hp": "150/150", "mp": "75/100", "fp": "20/20", "exp": "10.0000"} - - Returns: - dict: The JSON data, or None on error. - """ - api_url = "http://127.0.0.1:5000/data" - try: - response = requests.get(api_url, timeout=1) - if response.status_code == 200: - return response.json() - else: - print("API error: status code", response.status_code) - except Exception as e: - print("Error polling API:", e) - return None - -def update_nodes(graph, character_status_node): - """ - Polls the API, updates the Character Status node's values, - and then updates all DisplayValueNodes in the graph. - - Args: - graph (NodeGraph): The NodeGraphQt graph instance. - character_status_node (CharacterStatusNode): The character status node. - """ - data = poll_api() - if data is None: - return - # --- Section: Update Character Status Node with Latest Data --- - character_status_node.update_value("HP", data.get('hp', "N/A")) - character_status_node.update_value("MP", data.get('mp', "N/A")) - character_status_node.update_value("FP", data.get('fp', "N/A")) - character_status_node.update_value("EXP", data.get('exp', "N/A")) - - # --- Section: Update All DisplayValueNodes --- - for node in graph.all_nodes(): - if hasattr(node, "process_input"): - node.process_input() - -# ----------------------------------------------------------------------------- -# Context Menu: Add DisplayValueNode Command -# ----------------------------------------------------------------------------- -def add_display_node(graph): - """ - Adds a new DisplayValueNode to the graph at a default position. - - Args: - graph (NodeGraph): The NodeGraphQt graph instance. - """ - # Use NodeGraphQt's create_node() with the correct node type string. - new_node = graph.create_node('io.github.nicole.display.DisplayValueNode', - name='Display Value Node', pos=[400, 300]) - print("Added a new Display Value node.") - -# ----------------------------------------------------------------------------- -# Main Application -# ----------------------------------------------------------------------------- if __name__ == '__main__': app = QtWidgets.QApplication([]) - # --- Section: Setup NodeGraphQT --- + # Create the NodeGraph controller. graph = NodeGraph() - graph.widget.setWindowTitle("Project Borealis - Flyff Information Overlay") - # Register nodes using their classes. - graph.register_node(CharacterStatusNode) - graph.register_node(DisplayValueNode) + graph.widget.setWindowTitle("Modular Nodes Demo") - # --- Section: Setup Context Menu for Adding DisplayValueNodes --- + # Dynamically import custom node classes from the 'Nodes' package. + custom_nodes = import_nodes_from_folder('Nodes') + for node_class in custom_nodes: + graph.register_node(node_class) + + # Add context menu commands for dynamic node creation. graph_context_menu = graph.get_context_menu('graph') + for node_class in custom_nodes: + # Build the node type string: "<__identifier__>." + node_type = f"{node_class.__identifier__}.{node_class.__name__}" + node_name = node_class.NODE_NAME + graph_context_menu.add_command( + f"Add {node_name}", + make_node_command(graph, node_type) + ) + + # Add a "Remove Selected Node" command to the graph context menu. graph_context_menu.add_command( - 'Add Display Value Node', - lambda: add_display_node(graph), - shortcut='Ctrl+D' + "Remove Selected Node", + lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None ) - # --- Section: Create and Connect Nodes --- - # Create the Character Status node. - character_status_node = graph.create_node('io.github.nicole.status.CharacterStatusNode', - name='Character Status', pos=[0, 0]) - # Create a Display Value node. - display_node = graph.create_node('io.github.nicole.display.DisplayValueNode', - name='Display Value Node', pos=[500, 200]) - # Connect the EXP output (index 3) from the Character Status node to the display node's input. - character_status_node.set_output(3, display_node.input(0)) + # Resize and show the graph widget. + graph.widget.resize(1200, 800) + graph.widget.show() - # --- Section: Timer Setup for API Polling and Node Updates --- + # Global update timer: + # - Call process_input() on every node that implements it. + def global_update(): + for node in graph.all_nodes(): + if hasattr(node, "process_input"): + try: + node.process_input() + except Exception as e: + print("Error updating node", node, e) timer = QtCore.QTimer() - timer.timeout.connect(lambda: update_nodes(graph, character_status_node)) + timer.timeout.connect(global_update) timer.start(500) - graph.widget.resize(1000, 600) - graph.widget.show() - app.exec_() + sys.exit(app.exec())