diff --git a/Experimental/experimental_nodes.py b/Experimental/experimental_nodes.py new file mode 100644 index 0000000..d88b197 --- /dev/null +++ b/Experimental/experimental_nodes.py @@ -0,0 +1,102 @@ +#!/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_()) diff --git a/data_collector.py b/Legacy_Code/Orphaned Code/data_collector.py similarity index 100% rename from data_collector.py rename to Legacy_Code/Orphaned Code/data_collector.py diff --git a/Nodes/__pycache__/__init__.cpython-312.pyc b/Nodes/__pycache__/__init__.cpython-312.pyc index d824339..a4bdc8d 100644 Binary files a/Nodes/__pycache__/__init__.cpython-312.pyc 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 index 5a2ef80..62dd897 100644 Binary files a/Nodes/__pycache__/array_node.cpython-312.pyc 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 deleted file mode 100644 index 3227399..0000000 Binary files a/Nodes/__pycache__/average_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/backdrop_node.cpython-312.pyc b/Nodes/__pycache__/backdrop_node.cpython-312.pyc new file mode 100644 index 0000000..c344722 Binary files /dev/null and b/Nodes/__pycache__/backdrop_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/base_circle_node.cpython-312.pyc b/Nodes/__pycache__/base_circle_node.cpython-312.pyc new file mode 100644 index 0000000..571e8f6 Binary files /dev/null and b/Nodes/__pycache__/base_circle_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/base_node.cpython-312.pyc b/Nodes/__pycache__/base_node.cpython-312.pyc new file mode 100644 index 0000000..739b435 Binary files /dev/null and b/Nodes/__pycache__/base_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 index 8501683..4ed2a54 100644 Binary files a/Nodes/__pycache__/character_status_node.cpython-312.pyc 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 deleted file mode 100644 index b1721cb..0000000 Binary files a/Nodes/__pycache__/convert_to_percent_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/data_node.cpython-312.pyc b/Nodes/__pycache__/data_node.cpython-312.pyc index 03561c1..7536ed3 100644 Binary files a/Nodes/__pycache__/data_node.cpython-312.pyc 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 deleted file mode 100644 index ff29655..0000000 Binary files a/Nodes/__pycache__/display_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/display_string_node.cpython-312.pyc b/Nodes/__pycache__/display_string_node.cpython-312.pyc deleted file mode 100644 index 425735b..0000000 Binary files a/Nodes/__pycache__/display_string_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/display_text_node.cpython-312.pyc b/Nodes/__pycache__/display_text_node.cpython-312.pyc deleted file mode 100644 index edc29d3..0000000 Binary files a/Nodes/__pycache__/display_text_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/display_value_node.cpython-312.pyc b/Nodes/__pycache__/display_value_node.cpython-312.pyc deleted file mode 100644 index 7ff124e..0000000 Binary files a/Nodes/__pycache__/display_value_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/group_node.cpython-312.pyc b/Nodes/__pycache__/group_node.cpython-312.pyc new file mode 100644 index 0000000..9381b3b Binary files /dev/null and b/Nodes/__pycache__/group_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/math_operation_node.cpython-312.pyc b/Nodes/__pycache__/math_operation_node.cpython-312.pyc new file mode 100644 index 0000000..5a68825 Binary files /dev/null and b/Nodes/__pycache__/math_operation_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/multiply_node.cpython-312.pyc b/Nodes/__pycache__/multiply_node.cpython-312.pyc deleted file mode 100644 index 3540a7c..0000000 Binary files a/Nodes/__pycache__/multiply_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/number_input_node.cpython-312.pyc b/Nodes/__pycache__/number_input_node.cpython-312.pyc deleted file mode 100644 index 1e03916..0000000 Binary files a/Nodes/__pycache__/number_input_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/__pycache__/port_node.cpython-312.pyc b/Nodes/__pycache__/port_node.cpython-312.pyc new file mode 100644 index 0000000..0539463 Binary files /dev/null and b/Nodes/__pycache__/port_node.cpython-312.pyc differ diff --git a/Nodes/__pycache__/subtract_node.cpython-312.pyc b/Nodes/__pycache__/subtract_node.cpython-312.pyc deleted file mode 100644 index 6705f01..0000000 Binary files a/Nodes/__pycache__/subtract_node.cpython-312.pyc and /dev/null differ diff --git a/Nodes/array_node.py b/Nodes/array_node.py index b069fe2..9403b51 100644 --- a/Nodes/array_node.py +++ b/Nodes/array_node.py @@ -8,9 +8,11 @@ class ArrayNode(BaseNode): - Stores incoming values in an array with size defined by ArraySize. When full, it removes the oldest value. """ - __identifier__ = 'io.github.nicole.array' + __identifier__ = 'bunny-lab.io.array_node' NODE_NAME = 'Array' + + def __init__(self): super(ArrayNode, self).__init__() self.values = {} # Ensure values is a dictionary. diff --git a/Nodes/average_node.py b/Nodes/average_node.py deleted file mode 100644 index 2f8be35..0000000 --- a/Nodes/average_node.py +++ /dev/null @@ -1,36 +0,0 @@ -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/backdrop_node.py b/Nodes/backdrop_node.py new file mode 100644 index 0000000..88968cb --- /dev/null +++ b/Nodes/backdrop_node.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +Enhanced Backdrop Node (Inherited from BaseNode) + +Features: + - Inherits from `BaseNode` so it can be discovered in your node scanning. + - Custom context menu to rename (set title) or pick a new color. + - Forces geometry updates to reduce "ghosting" or partial redraws. +""" + +from Qt import QtWidgets, QtGui +from NodeGraphQt import BaseNode +from NodeGraphQt.constants import NodePropWidgetEnum +from NodeGraphQt.qgraphics.node_backdrop import BackdropNodeItem + + +class BackdropNode(BaseNode): + """ + Backdrop Node: + - Allows grouping or annotating other nodes by resizing a large rectangle. + - Provides a custom context menu for renaming and recoloring (via on_context_menu). + """ + + __identifier__ = 'bunny-lab.io.backdrop' + NODE_NAME = 'Backdrop' + + def __init__(self): + # Use BackdropNodeItem for the specialized QGraphicsItem. + super(BackdropNode, self).__init__(qgraphics_item=BackdropNodeItem) + + # Default color (teal). + self.model.color = (5, 129, 138, 255) + + # Multi-line text property for storing the backdrop text. + self.create_property( + 'backdrop_text', + '', + widget_type=NodePropWidgetEnum.QTEXT_EDIT.value, + tab='Backdrop' + ) + + # -------------------------------------------------------------------------- + # Resizing / Geometry + # -------------------------------------------------------------------------- + def on_backdrop_updated(self, update_prop, value=None): + """ + Triggered when the user resizes or double-clicks the backdrop sizer handle. + """ + if not self.graph: + return + + if update_prop == 'sizer_mouse_release': + # User finished dragging the resize handle + self.view.prepareGeometryChange() + self.graph.begin_undo(f'resized "{self.name()}"') + self.set_property('width', value['width']) + self.set_property('height', value['height']) + self.set_pos(*value['pos']) + self.graph.end_undo() + self.view.update() + + elif update_prop == 'sizer_double_clicked': + # User double-clicked the resize handle (auto-resize) + self.view.prepareGeometryChange() + self.graph.begin_undo(f'"{self.name()}" auto resize') + self.set_property('width', value['width']) + self.set_property('height', value['height']) + self.set_pos(*value['pos']) + self.graph.end_undo() + self.view.update() + + def auto_size(self): + """ + Auto-resize the backdrop to fit around intersecting nodes. + """ + if not self.graph: + return + self.view.prepareGeometryChange() + self.graph.begin_undo(f'"{self.name()}" auto resize') + size = self.view.calc_backdrop_size() + self.set_property('width', size['width']) + self.set_property('height', size['height']) + self.set_pos(*size['pos']) + self.graph.end_undo() + self.view.update() + + def wrap_nodes(self, nodes): + """ + Fit the backdrop around the specified nodes. + """ + if not self.graph or not nodes: + return + self.view.prepareGeometryChange() + self.graph.begin_undo(f'"{self.name()}" wrap nodes') + size = self.view.calc_backdrop_size([n.view for n in nodes]) + self.set_property('width', size['width']) + self.set_property('height', size['height']) + self.set_pos(*size['pos']) + self.graph.end_undo() + self.view.update() + + def nodes(self): + """ + Return a list of nodes wrapped by this backdrop. + """ + node_ids = [n.id for n in self.view.get_nodes()] + return [self.graph.get_node_by_id(nid) for nid in node_ids] + + def set_text(self, text=''): + """ + Set the multi-line text in the backdrop. + """ + self.set_property('backdrop_text', text) + + def text(self): + """ + Return the text content in the backdrop. + """ + return self.get_property('backdrop_text') + + def set_size(self, width, height): + """ + Manually set the backdrop size. + """ + if self.graph: + self.view.prepareGeometryChange() + self.graph.begin_undo('backdrop size') + self.set_property('width', width) + self.set_property('height', height) + self.graph.end_undo() + self.view.update() + else: + self.view.width, self.view.height = width, height + self.model.width, self.model.height = width, height + + def size(self): + """ + Return (width, height) of the backdrop. + """ + self.model.width = self.view.width + self.model.height = self.view.height + return self.model.width, self.model.height + + # No ports for a backdrop: + def inputs(self): + return + + def outputs(self): + return + + # -------------------------------------------------------------------------- + # Custom Context Menu + # -------------------------------------------------------------------------- + def on_context_menu(self, menu): + """ + Called manually by the node context menu callback in older NodeGraphQt versions. + """ + rename_action = menu.addAction("Set Title...") + rename_action.triggered.connect(self._change_title) + + color_action = menu.addAction("Set Color...") + color_action.triggered.connect(self._change_color) + + def _change_title(self): + """ + Prompt for a new backdrop title (header). + """ + new_title, ok = QtWidgets.QInputDialog.getText( + None, "Backdrop Title", "Enter new backdrop title:" + ) + if ok and new_title: + self.set_name(new_title) + + def _change_color(self): + """ + Prompt for a new backdrop color via QColorDialog. + """ + current_color = QtGui.QColor(*self.model.color) + color = QtWidgets.QColorDialog.getColor( + current_color, None, "Select Backdrop Color" + ) + if color.isValid(): + self.model.color = (color.red(), color.green(), color.blue(), color.alpha()) + self.view.update() diff --git a/Nodes/base_circle_node.py b/Nodes/base_circle_node.py new file mode 100644 index 0000000..790c8f6 --- /dev/null +++ b/Nodes/base_circle_node.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +from NodeGraphQt.nodes.base_node import BaseNode +from NodeGraphQt.qgraphics.node_circle import CircleNodeItem + + +class BaseNodeCircle(BaseNode): + """ + `Implemented in` ``v0.5.2`` + + The ``NodeGraphQt.BaseNodeCircle`` is pretty much the same class as the + :class:`NodeGraphQt.BaseNode` except with a different design. + + .. inheritance-diagram:: NodeGraphQt.BaseNodeCircle + + .. image:: ../_images/node_circle.png + :width: 250px + + example snippet: + + .. code-block:: python + :linenos: + + from NodeGraphQt import BaseNodeCircle + + class ExampleNode(BaseNodeCircle): + + # unique node identifier domain. + __identifier__ = 'io.jchanvfx.github' + + # initial default node name. + NODE_NAME = 'My Node' + + def __init__(self): + super(ExampleNode, self).__init__() + + # create an input port. + self.add_input('in') + + # create an output port. + self.add_output('out') + """ + + NODE_NAME = 'Circle Node' + + def __init__(self, qgraphics_item=None): + super(BaseNodeCircle, self).__init__(qgraphics_item or CircleNodeItem) \ No newline at end of file diff --git a/Nodes/base_node.py b/Nodes/base_node.py new file mode 100644 index 0000000..4212bb5 --- /dev/null +++ b/Nodes/base_node.py @@ -0,0 +1,876 @@ +#!/usr/bin/python +from collections import OrderedDict + +from NodeGraphQt.base.commands import NodeVisibleCmd, NodeWidgetVisibleCmd +from NodeGraphQt.base.node import NodeObject +from NodeGraphQt.base.port import Port +from NodeGraphQt.constants import NodePropWidgetEnum, PortTypeEnum +from NodeGraphQt.errors import ( + PortError, + PortRegistrationError, + NodeWidgetError +) +from NodeGraphQt.qgraphics.node_base import NodeItem +from NodeGraphQt.widgets.node_widgets import ( + NodeBaseWidget, + NodeCheckBox, + NodeComboBox, + NodeLineEdit +) + + +class BaseNode(NodeObject): + """ + The ``NodeGraphQt.BaseNode`` class is the base class for nodes that allows + port connections from one node to another. + + .. inheritance-diagram:: NodeGraphQt.BaseNode + + .. image:: ../_images/node.png + :width: 250px + + example snippet: + + .. code-block:: python + :linenos: + + from NodeGraphQt import BaseNode + + class ExampleNode(BaseNode): + + # unique node identifier domain. + __identifier__ = 'io.jchanvfx.github' + + # initial default node name. + NODE_NAME = 'My Node' + + def __init__(self): + super(ExampleNode, self).__init__() + + # create an input port. + self.add_input('in') + + # create an output port. + self.add_output('out') + """ + + NODE_NAME = 'Node' + + def __init__(self, qgraphics_item=None): + super(BaseNode, self).__init__(qgraphics_item or NodeItem) + self._inputs = [] + self._outputs = [] + + def update_model(self): + """ + Update the node model from view. + """ + for name, val in self.view.properties.items(): + if name in ['inputs', 'outputs']: + continue + self.model.set_property(name, val) + + for name, widget in self.view.widgets.items(): + self.model.set_property(name, widget.get_value()) + + def set_property(self, name, value, push_undo=True): + """ + Set the value on the node custom property. + + Args: + name (str): name of the property. + value (object): property data (python built in types). + push_undo (bool): register the command to the undo stack. (default: True) + """ + # prevent signals from causing a infinite loop. + if self.get_property(name) == value: + return + + if name == 'visible': + if self.graph: + undo_cmd = NodeVisibleCmd(self, value) + if push_undo: + self.graph.undo_stack().push(undo_cmd) + else: + undo_cmd.redo() + return + elif name == 'disabled': + # redraw the connected pipes in the scene. + ports = self.view.inputs + self.view.outputs + for port in ports: + for pipe in port.connected_pipes: + pipe.update() + super(BaseNode, self).set_property(name, value, push_undo) + + def set_layout_direction(self, value=0): + """ + Sets the node layout direction to either horizontal or vertical on + the current node only. + + `Implemented in` ``v0.3.0`` + + See Also: + :meth:`NodeGraph.set_layout_direction`, + :meth:`NodeObject.layout_direction` + + + Warnings: + This function does not register to the undo stack. + + Args: + value (int): layout direction mode. + """ + # base logic to update the model and view attributes only. + super(BaseNode, self).set_layout_direction(value) + # redraw the node. + self._view.draw_node() + + def set_icon(self, icon=None): + """ + Set the node icon. + + Args: + icon (str): path to the icon image. + """ + self.set_property('icon', icon) + + def icon(self): + """ + Node icon path. + + Returns: + str: icon image file path. + """ + return self.model.icon + + def widgets(self): + """ + Returns all embedded widgets from this node. + + See Also: + :meth:`BaseNode.get_widget` + + Returns: + dict: embedded node widgets. {``property_name``: ``node_widget``} + """ + return self.view.widgets + + def get_widget(self, name): + """ + Returns the embedded widget associated with the property name. + + See Also: + :meth:`BaseNode.add_combo_menu`, + :meth:`BaseNode.add_text_input`, + :meth:`BaseNode.add_checkbox`, + + Args: + name (str): node property name. + + Returns: + NodeBaseWidget: embedded node widget. + """ + return self.view.widgets.get(name) + + def add_custom_widget(self, widget, widget_type=None, tab=None): + """ + Add a custom node widget into the node. + + see example :ref:`Embedding Custom Widgets`. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + widget (NodeBaseWidget): node widget class object. + widget_type: widget flag to display in the + :class:`NodeGraphQt.PropertiesBinWidget` + (default: :attr:`NodeGraphQt.constants.NodePropWidgetEnum.HIDDEN`). + tab (str): name of the widget tab to display in. + """ + if not isinstance(widget, NodeBaseWidget): + raise NodeWidgetError( + '\'widget\' must be an instance of a NodeBaseWidget') + + widget_type = widget_type or NodePropWidgetEnum.HIDDEN.value + self.create_property(widget.get_name(), + widget.get_value(), + widget_type=widget_type, + tab=tab) + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + widget._node = self + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + #: HACK: calling the .parent() function here on the widget as it seems + # to address a seg fault issue when exiting the application. + widget.parent() + + def add_combo_menu(self, name, label='', items=None, tooltip=None, + tab=None): + """ + Creates a custom property with the :meth:`NodeObject.create_property` + function and embeds a :class:`PySide2.QtWidgets.QComboBox` widget + into the node. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + name (str): name for the custom property. + label (str): label to be displayed. + items (list[str]): items to be added into the menu. + tooltip (str): widget tooltip. + tab (str): name of the widget tab to display in. + """ + self.create_property( + name, + value=items[0] if items else None, + items=items or [], + widget_type=NodePropWidgetEnum.QCOMBO_BOX.value, + widget_tooltip=tooltip, + tab=tab + ) + widget = NodeComboBox(self.view, name, label, items) + widget.setToolTip(tooltip or '') + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + def add_text_input(self, name, label='', text='', placeholder_text='', + tooltip=None, tab=None): + """ + Creates a custom property with the :meth:`NodeObject.create_property` + function and embeds a :class:`PySide2.QtWidgets.QLineEdit` widget + into the node. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + name (str): name for the custom property. + label (str): label to be displayed. + text (str): pre-filled text. + placeholder_text (str): placeholder text. + tooltip (str): widget tooltip. + tab (str): name of the widget tab to display in. + """ + self.create_property( + name, + value=text, + widget_type=NodePropWidgetEnum.QLINE_EDIT.value, + widget_tooltip=tooltip, + tab=tab + ) + widget = NodeLineEdit(self.view, name, label, text, placeholder_text) + widget.setToolTip(tooltip or '') + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + def add_checkbox(self, name, label='', text='', state=False, tooltip=None, + tab=None): + """ + Creates a custom property with the :meth:`NodeObject.create_property` + function and embeds a :class:`PySide2.QtWidgets.QCheckBox` widget + into the node. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + name (str): name for the custom property. + label (str): label to be displayed. + text (str): checkbox text. + state (bool): pre-check. + tooltip (str): widget tooltip. + tab (str): name of the widget tab to display in. + """ + self.create_property( + name, + value=state, + widget_type=NodePropWidgetEnum.QCHECK_BOX.value, + widget_tooltip=tooltip, + tab=tab + ) + widget = NodeCheckBox(self.view, name, label, text, state) + widget.setToolTip(tooltip or '') + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + def hide_widget(self, name, push_undo=True): + """ + Hide an embedded node widget. + + Args: + name (str): node property name for the widget. + push_undo (bool): register the command to the undo stack. (default: True) + + See Also: + :meth:`BaseNode.add_custom_widget`, + :meth:`BaseNode.show_widget`, + :meth:`BaseNode.get_widget` + """ + if not self.view.has_widget(name): + return + undo_cmd = NodeWidgetVisibleCmd(self, name, visible=False) + if push_undo: + self.graph.undo_stack().push(undo_cmd) + else: + undo_cmd.redo() + + def show_widget(self, name, push_undo=True): + """ + Show an embedded node widget. + + Args: + name (str): node property name for the widget. + push_undo (bool): register the command to the undo stack. (default: True) + + See Also: + :meth:`BaseNode.add_custom_widget`, + :meth:`BaseNode.hide_widget`, + :meth:`BaseNode.get_widget` + """ + if not self.view.has_widget(name): + return + undo_cmd = NodeWidgetVisibleCmd(self, name, visible=True) + if push_undo: + self.graph.undo_stack().push(undo_cmd) + else: + undo_cmd.redo() + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + """ + Add input :class:`Port` to node. + + Warnings: + Undo is NOT supported for this function. + + Args: + name (str): name for the input port. + multi_input (bool): allow port to have more than one connection. + display_name (bool): display the port name on the node. + color (tuple): initial port color (r, g, b) ``0-255``. + locked (bool): locked state see :meth:`Port.set_locked` + painter_func (function or None): custom function to override the drawing + of the port shape see example: :ref:`Creating Custom Shapes` + + Returns: + NodeGraphQt.Port: the created port object. + """ + if name in self.inputs().keys(): + raise PortRegistrationError( + 'port name "{}" already registered.'.format(name)) + + port_args = [name, multi_input, display_name, locked] + if painter_func and callable(painter_func): + port_args.append(painter_func) + view = self.view.add_input(*port_args) + + if color: + view.color = color + view.border_color = [min([255, max([0, i + 80])]) for i in color] + + port = Port(self, view) + port.model.type_ = PortTypeEnum.IN.value + port.model.name = name + port.model.display_name = display_name + port.model.multi_connection = multi_input + port.model.locked = locked + self._inputs.append(port) + self.model.inputs[port.name()] = port.model + return port + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + """ + Add output :class:`Port` to node. + + Warnings: + Undo is NOT supported for this function. + + Args: + name (str): name for the output port. + multi_output (bool): allow port to have more than one connection. + display_name (bool): display the port name on the node. + color (tuple): initial port color (r, g, b) ``0-255``. + locked (bool): locked state see :meth:`Port.set_locked` + painter_func (function or None): custom function to override the drawing + of the port shape see example: :ref:`Creating Custom Shapes` + + Returns: + NodeGraphQt.Port: the created port object. + """ + if name in self.outputs().keys(): + raise PortRegistrationError( + 'port name "{}" already registered.'.format(name)) + + port_args = [name, multi_output, display_name, locked] + if painter_func and callable(painter_func): + port_args.append(painter_func) + view = self.view.add_output(*port_args) + + if color: + view.color = color + view.border_color = [min([255, max([0, i + 80])]) for i in color] + port = Port(self, view) + port.model.type_ = PortTypeEnum.OUT.value + port.model.name = name + port.model.display_name = display_name + port.model.multi_connection = multi_output + port.model.locked = locked + self._outputs.append(port) + self.model.outputs[port.name()] = port.model + return port + + def get_input(self, port): + """ + Get input port by the name or index. + + Args: + port (str or int): port name or index. + + Returns: + NodeGraphQt.Port: node port. + """ + if type(port) is int: + if port < len(self._inputs): + return self._inputs[port] + elif type(port) is str: + return self.inputs().get(port, None) + + def get_output(self, port): + """ + Get output port by the name or index. + + Args: + port (str or int): port name or index. + + Returns: + NodeGraphQt.Port: node port. + """ + if type(port) is int: + if port < len(self._outputs): + return self._outputs[port] + elif type(port) is str: + return self.outputs().get(port, None) + + def delete_input(self, port): + """ + Delete input port. + + Warnings: + Undo is NOT supported for this function. + + You can only delete ports if :meth:`BaseNode.port_deletion_allowed` + returns ``True`` otherwise a port error is raised see also + :meth:`BaseNode.set_port_deletion_allowed`. + + Args: + port (str or int): port name or index. + """ + if type(port) in [int, str]: + port = self.get_input(port) + if port is None: + return + if not self.port_deletion_allowed(): + raise PortError( + 'Port "{}" can\'t be deleted on this node because ' + '"ports_removable" is not enabled.'.format(port.name())) + if port.locked(): + raise PortError('Error: Can\'t delete a port that is locked!') + self._inputs.remove(port) + self._model.inputs.pop(port.name()) + self._view.delete_input(port.view) + port.model.node = None + self._view.draw_node() + + def delete_output(self, port): + """ + Delete output port. + + Warnings: + Undo is NOT supported for this function. + + You can only delete ports if :meth:`BaseNode.port_deletion_allowed` + returns ``True`` otherwise a port error is raised see also + :meth:`BaseNode.set_port_deletion_allowed`. + + Args: + port (str or int): port name or index. + """ + if type(port) in [int, str]: + port = self.get_output(port) + if port is None: + return + if not self.port_deletion_allowed(): + raise PortError( + 'Port "{}" can\'t be deleted on this node because ' + '"ports_removable" is not enabled.'.format(port.name())) + if port.locked(): + raise PortError('Error: Can\'t delete a port that is locked!') + self._outputs.remove(port) + self._model.outputs.pop(port.name()) + self._view.delete_output(port.view) + port.model.node = None + self._view.draw_node() + + def set_port_deletion_allowed(self, mode=False): + """ + Allow ports to be removable on this node. + + See Also: + :meth:`BaseNode.port_deletion_allowed` and + :meth:`BaseNode.set_ports` + + Args: + mode (bool): true to allow. + """ + self.model.port_deletion_allowed = mode + + def port_deletion_allowed(self): + """ + Return true if ports can be deleted on this node. + + See Also: + :meth:`BaseNode.set_port_deletion_allowed` + + Returns: + bool: true if ports can be deleted. + """ + return self.model.port_deletion_allowed + + def set_ports(self, port_data): + """ + Create node input and output ports from serialized port data. + + Warnings: + You can only use this function if the node has + :meth:`BaseNode.port_deletion_allowed` is `True` + see :meth:`BaseNode.set_port_deletion_allowed` + + Hint: + example snippet of port data. + + .. highlight:: python + .. code-block:: python + + { + 'input_ports': + [{ + 'name': 'input', + 'multi_connection': True, + 'display_name': 'Input', + 'locked': False + }], + 'output_ports': + [{ + 'name': 'output', + 'multi_connection': True, + 'display_name': 'Output', + 'locked': False + }] + } + + Args: + port_data(dict): port data. + """ + if not self.port_deletion_allowed(): + raise PortError( + 'Ports cannot be set on this node because ' + '"set_port_deletion_allowed" is not enabled on this node.') + + for port in self._inputs: + self._view.delete_input(port.view) + port.model.node = None + for port in self._outputs: + self._view.delete_output(port.view) + port.model.node = None + self._inputs = [] + self._outputs = [] + self._model.outputs = {} + self._model.inputs = {} + + [self.add_input(name=port['name'], + multi_input=port['multi_connection'], + display_name=port['display_name'], + locked=port.get('locked') or False) + for port in port_data['input_ports']] + [self.add_output(name=port['name'], + multi_output=port['multi_connection'], + display_name=port['display_name'], + locked=port.get('locked') or False) + for port in port_data['output_ports']] + self._view.draw_node() + + def inputs(self): + """ + Returns all the input ports from the node. + + Returns: + dict: {: } + """ + return {p.name(): p for p in self._inputs} + + def input_ports(self): + """ + Return all input ports. + + Returns: + list[NodeGraphQt.Port]: node input ports. + """ + return self._inputs + + def outputs(self): + """ + Returns all the output ports from the node. + + Returns: + dict: {: } + """ + return {p.name(): p for p in self._outputs} + + def output_ports(self): + """ + Return all output ports. + + Returns: + list[NodeGraphQt.Port]: node output ports. + """ + return self._outputs + + def input(self, index): + """ + Return the input port with the matching index. + + Args: + index (int): index of the input port. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._inputs[index] + + def set_input(self, index, port): + """ + Creates a connection pipe to the targeted output :class:`Port`. + + Args: + index (int): index of the port. + port (NodeGraphQt.Port): port object. + """ + src_port = self.input(index) + src_port.connect_to(port) + + def output(self, index): + """ + Return the output port with the matching index. + + Args: + index (int): index of the output port. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._outputs[index] + + def set_output(self, index, port): + """ + Creates a connection pipe to the targeted input :class:`Port`. + + Args: + index (int): index of the port. + port (NodeGraphQt.Port): port object. + """ + src_port = self.output(index) + src_port.connect_to(port) + + def connected_input_nodes(self): + """ + Returns all nodes connected from the input ports. + + Returns: + dict: {: } + """ + nodes = OrderedDict() + for p in self.input_ports(): + nodes[p] = [cp.node() for cp in p.connected_ports()] + return nodes + + def connected_output_nodes(self): + """ + Returns all nodes connected from the output ports. + + Returns: + dict: {: } + """ + nodes = OrderedDict() + for p in self.output_ports(): + nodes[p] = [cp.node() for cp in p.connected_ports()] + return nodes + + def add_accept_port_type(self, port, port_type_data): + """ + Add an accept constrain to a specified node port. + + Once a constraint has been added only ports of that type specified will + be allowed a pipe connection. + + port type data example + + .. highlight:: python + .. code-block:: python + + { + 'port_name': 'foo' + 'port_type': PortTypeEnum.IN.value + 'node_type': 'io.github.jchanvfx.NodeClass' + } + + See Also: + :meth:`NodeGraphQt.BaseNode.accepted_port_types` + + Args: + port (NodeGraphQt.Port): port to assign constrain to. + port_type_data (dict): port type data to accept a connection + """ + node_ports = self._inputs + self._outputs + if port not in node_ports: + raise PortError('Node does not contain port: "{}"'.format(port)) + + self._model.add_port_accept_connection_type( + port_name=port.name(), + port_type=port.type_(), + node_type=self.type_, + accept_pname=port_type_data['port_name'], + accept_ptype=port_type_data['port_type'], + accept_ntype=port_type_data['node_type'] + ) + + def accepted_port_types(self, port): + """ + Returns a dictionary of connection constrains of the port types + that allow for a pipe connection to this node. + + Args: + port (NodeGraphQt.Port): port object. + + Returns: + dict: {: {: []}} + """ + ports = self._inputs + self._outputs + if port not in ports: + raise PortError('Node does not contain port "{}"'.format(port)) + + accepted_types = self.graph.model.port_accept_connection_types( + node_type=self.type_, + port_type=port.type_(), + port_name=port.name() + ) + return accepted_types + + def add_reject_port_type(self, port, port_type_data): + """ + Add a reject constrain to a specified node port. + + Once a constraint has been added only ports of that type specified will + NOT be allowed a pipe connection. + + port type data example + + .. highlight:: python + .. code-block:: python + + { + 'port_name': 'foo' + 'port_type': PortTypeEnum.IN.value + 'node_type': 'io.github.jchanvfx.NodeClass' + } + + See Also: + :meth:`NodeGraphQt.Port.rejected_port_types` + + Args: + port (NodeGraphQt.Port): port to assign constrain to. + port_type_data (dict): port type data to reject a connection + """ + node_ports = self._inputs + self._outputs + if port not in node_ports: + raise PortError('Node does not contain port: "{}"'.format(port)) + + self._model.add_port_reject_connection_type( + port_name=port.name(), + port_type=port.type_(), + node_type=self.type_, + reject_pname=port_type_data['port_name'], + reject_ptype=port_type_data['port_type'], + reject_ntype=port_type_data['node_type'] + ) + + def rejected_port_types(self, port): + """ + Returns a dictionary of connection constrains of the port types + that are NOT allowed for a pipe connection to this node. + + Args: + port (NodeGraphQt.Port): port object. + + Returns: + dict: {: {: []}} + """ + ports = self._inputs + self._outputs + if port not in ports: + raise PortError('Node does not contain port "{}"'.format(port)) + + rejected_types = self.graph.model.port_reject_connection_types( + node_type=self.type_, + port_type=port.type_(), + port_name=port.name() + ) + return rejected_types + + def on_input_connected(self, in_port, out_port): + """ + Callback triggered when a new pipe connection is made. + + *The default of this function does nothing re-implement if you require + logic to run for this event.* + + Note: + to work with undo & redo for this method re-implement + :meth:`BaseNode.on_input_disconnected` with the reverse logic. + + Args: + in_port (NodeGraphQt.Port): source input port from this node. + out_port (NodeGraphQt.Port): output port that connected to this node. + """ + return + + def on_input_disconnected(self, in_port, out_port): + """ + Callback triggered when a pipe connection has been disconnected + from a INPUT port. + + *The default of this function does nothing re-implement if you require + logic to run for this event.* + + Note: + to work with undo & redo for this method re-implement + :meth:`BaseNode.on_input_connected` with the reverse logic. + + Args: + in_port (NodeGraphQt.Port): source input port from this node. + out_port (NodeGraphQt.Port): output port that was disconnected. + """ + return \ No newline at end of file diff --git a/Nodes/character_status_node.py b/Nodes/character_status_node.py index 62eb026..5e12394 100644 --- a/Nodes/character_status_node.py +++ b/Nodes/character_status_node.py @@ -44,7 +44,7 @@ def get_draw_stat_port(color, border_color=None, alpha=127): return painter_func class CharacterStatusNode(BaseNode): - __identifier__ = 'io.github.nicole.status' + __identifier__ = 'bunny-lab.io.status_node' NODE_NAME = 'Character Status' def __init__(self): diff --git a/Nodes/convert_to_percent_node.py b/Nodes/convert_to_percent_node.py deleted file mode 100644 index ea7d127..0000000 --- a/Nodes/convert_to_percent_node.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -""" -Convert to Percent Node - -This node takes two numerical inputs (A and B), computes (A / B) * 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 two input ports for separate numerator and denominator. - self.add_input("Numerator") - self.add_input("Denominator") - # 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): - numerator_input = self.input(0) - denominator_input = self.input(1) - - numerator = self.get_connected_value(numerator_input) - denominator = self.get_connected_value(denominator_input) - - try: - numerator = float(numerator) - denominator = float(denominator) - 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}" - - # 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 - - def get_connected_value(self, input_port): - """ - Helper function to retrieve the value from a connected port. - """ - if input_port and input_port.connected_ports(): - connected_output = input_port.connected_ports()[0] - parent_node = connected_output.node() - port_name = connected_output.name() - if hasattr(parent_node, 'values') and isinstance(parent_node.values, dict): - return parent_node.values.get(port_name, "0") - return getattr(parent_node, 'value', "0") - return "0" diff --git a/Nodes/data_node.py b/Nodes/data_node.py index a63b630..7c89dc1 100644 --- a/Nodes/data_node.py +++ b/Nodes/data_node.py @@ -18,7 +18,7 @@ Behavior: from NodeGraphQt import BaseNode class DataNode(BaseNode): - __identifier__ = 'io.github.nicole.data' + __identifier__ = 'bunny-lab.io.data_node' NODE_NAME = 'Data Node' def __init__(self): @@ -50,7 +50,6 @@ class DataNode(BaseNode): """ current_text = self.get_property('value') self.value = current_text - self.set_name(f"Data Node") def property_changed(self, property_name): """ @@ -114,7 +113,6 @@ class DataNode(BaseNode): Receives data from connected nodes and updates the internal value. """ self.set_property('value', str(data)) # Ensure it's always stored as a string - self.set_name(f"Data Node") # Transmit data further if there's an output connection output_port = self.output(0) diff --git a/Nodes/group_node.py b/Nodes/group_node.py new file mode 100644 index 0000000..f87fe32 --- /dev/null +++ b/Nodes/group_node.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +from NodeGraphQt.nodes.base_node import BaseNode +from NodeGraphQt.nodes.port_node import PortInputNode, PortOutputNode +from NodeGraphQt.qgraphics.node_group import GroupNodeItem + + +class GroupNode(BaseNode): + """ + `Implemented in` ``v0.2.0`` + + The ``NodeGraphQt.GroupNode`` class extends from the :class:`NodeGraphQt.BaseNode` + class with the ability to nest other nodes inside of it. + + .. inheritance-diagram:: NodeGraphQt.GroupNode + + .. image:: ../_images/group_node.png + :width: 250px + + - + """ + + NODE_NAME = 'Group' + + def __init__(self, qgraphics_item=None): + super(GroupNode, self).__init__(qgraphics_item or GroupNodeItem) + self._input_port_nodes = {} + self._output_port_nodes = {} + + @property + def is_expanded(self): + """ + Returns if the group node is expanded or collapsed. + + Returns: + bool: true if the node is expanded. + """ + if not self.graph: + return False + return bool(self.id in self.graph.sub_graphs) + + def get_sub_graph(self): + """ + Returns the sub graph controller to the group node if initialized + or returns None. + + Returns: + SubGraph: sub graph controller. + """ + return self.graph.sub_graphs.get(self.id) + + def get_sub_graph_session(self): + """ + Returns the serialized sub graph session. + + Returns: + dict: serialized sub graph session. + """ + return self.model.subgraph_session + + def set_sub_graph_session(self, serialized_session): + """ + Sets the sub graph session data to the group node. + + Args: + serialized_session (dict): serialized session. + """ + serialized_session = serialized_session or {} + self.model.subgraph_session = serialized_session + + def expand(self): + """ + Expand the group node session. + + See Also: + :meth:`NodeGraph.expand_group_node`, + :meth:`SubGraph.expand_group_node`. + + Returns: + SubGraph: node graph used to manage the nodes expaneded session. + """ + sub_graph = self.graph.expand_group_node(self) + return sub_graph + + def collapse(self): + """ + Collapse the group node session it's expanded child sub graphs. + + See Also: + :meth:`NodeGraph.collapse_group_node`, + :meth:`SubGraph.collapse_group_node`. + """ + self.graph.collapse_group_node(self) + + def set_name(self, name=''): + super(GroupNode, self).set_name(name) + # update the tab bar and navigation labels. + sub_graph = self.get_sub_graph() + if sub_graph: + nav_widget = sub_graph.navigation_widget + nav_widget.update_label_item(self.name(), self.id) + + if sub_graph.parent_graph.is_root: + root_graph = sub_graph.parent_graph + tab_bar = root_graph.widget.tabBar() + for idx in range(tab_bar.count()): + if tab_bar.tabToolTip(idx) == self.id: + tab_bar.setTabText(idx, self.name()) + break + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + port = super(GroupNode, self).add_input( + name=name, + multi_input=multi_input, + display_name=display_name, + color=color, + locked=locked, + painter_func=painter_func + ) + if self.is_expanded: + input_node = PortInputNode(parent_port=port) + input_node.NODE_NAME = port.name() + input_node.model.set_property('name', port.name()) + input_node.add_output(port.name()) + sub_graph = self.get_sub_graph() + sub_graph.add_node(input_node, selected=False, push_undo=False) + + return port + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + port = super(GroupNode, self).add_output( + name=name, + multi_output=multi_output, + display_name=display_name, + color=color, + locked=locked, + painter_func=painter_func + ) + if self.is_expanded: + output_port = PortOutputNode(parent_port=port) + output_port.NODE_NAME = port.name() + output_port.model.set_property('name', port.name()) + output_port.add_input(port.name()) + sub_graph = self.get_sub_graph() + sub_graph.add_node(output_port, selected=False, push_undo=False) + + return port + + def delete_input(self, port): + if type(port) in [int, str]: + port = self.get_input(port) + if port is None: + return + + if self.is_expanded: + sub_graph = self.get_sub_graph() + port_node = sub_graph.get_node_by_port(port) + if port_node: + sub_graph.remove_node(port_node, push_undo=False) + + super(GroupNode, self).delete_input(port) + + def delete_output(self, port): + if type(port) in [int, str]: + port = self.get_output(port) + if port is None: + return + + if self.is_expanded: + sub_graph = self.get_sub_graph() + port_node = sub_graph.get_node_by_port(port) + if port_node: + sub_graph.remove_node(port_node, push_undo=False) + + super(GroupNode, self).delete_output(port) \ No newline at end of file diff --git a/Nodes/math_operation_node.py b/Nodes/math_operation_node.py new file mode 100644 index 0000000..2bf84e4 --- /dev/null +++ b/Nodes/math_operation_node.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Math Operation Node: + - Inputs: Two input ports ("A" and "B"). + - Output: One output port ("Result"). + - Operation: A dropdown (combo menu) to select: + Add, Subtract, Multiply, Divide, Average. + - Displays the computed result in a read-only text box labeled "Result". +""" + +from NodeGraphQt import BaseNode + +class MathOperationNode(BaseNode): + __identifier__ = 'bunny-lab.io.math_node' + NODE_NAME = 'Math Operation' + + def __init__(self): + super(MathOperationNode, self).__init__() + + # ---------------------------------------------------------------------- + # Initialization Section: + # - Create two input ports: A, B + # - Create one output port: Result + # - Add a combo box for operator selection + # - Add a text input for displaying the computed result + # ---------------------------------------------------------------------- + self.add_input('A') + self.add_input('B') + self.add_output('Result') + + # Operator combo box (Add, Subtract, Multiply, Divide, Average) + self.add_combo_menu('operator', 'Operator', items=[ + 'Add', 'Subtract', 'Multiply', 'Divide', 'Average' + ]) + + # Text input for displaying the computed result. + # We'll make it read-only by accessing the underlying QLineEdit. + self.add_text_input('calc_result', 'Result', text='0') + result_widget = self.get_widget('calc_result') # This is a NodeLineEdit wrapper + if result_widget: + # Get the underlying QLineEdit + line_edit = result_widget.get_custom_widget() + # Make the QLineEdit read-only + line_edit.setReadOnly(True) + + self.value = 0 + self.set_name("Math Operation") + self.process_input() + + def process_input(self, event=None): + """ + Compute Section: + - For each input port (A, B), if connected, grab the 'value' from + the upstream node; otherwise default to 0.0. + - Convert to float, apply the selected operation from the combo box, + update the "Result" text box, node title, and output port. + """ + # Gather input A + input_a = self.input(0) + if input_a and input_a.connected_ports(): + a_raw = input_a.connected_ports()[0].node().get_property('value') + else: + a_raw = 0.0 + + # Gather input B + input_b = self.input(1) + if input_b and input_b.connected_ports(): + b_raw = input_b.connected_ports()[0].node().get_property('value') + else: + b_raw = 0.0 + + # Convert raw inputs to floats (default 0.0 on failure). + try: + a_val = float(a_raw) + except (ValueError, TypeError): + a_val = 0.0 + try: + b_val = float(b_raw) + except (ValueError, TypeError): + b_val = 0.0 + + # Retrieve the selected operator from the combo box. + operator = self.get_property('operator') + result = 0.0 + + if operator == 'Add': + result = a_val + b_val + elif operator == 'Subtract': + result = a_val - b_val + elif operator == 'Multiply': + result = a_val * b_val + elif operator == 'Divide': + result = a_val / b_val if b_val != 0 else 0.0 + elif operator == 'Average': + result = (a_val + b_val) / 2.0 + + self.value = result + + # Update the read-only text input and node title. + self.set_property('calc_result', str(result)) + + # Transmit the numeric result to any connected output nodes. + output_port = self.output(0) + if output_port and output_port.connected_ports(): + for cp in output_port.connected_ports(): + connected_node = cp.node() + if hasattr(connected_node, 'receive_data'): + connected_node.receive_data(result, source_port_name='Result') + + def on_input_connected(self, input_port, output_port): + self.process_input() + + def on_input_disconnected(self, input_port, output_port): + self.process_input() + + def property_changed(self, property_name): + if property_name in ['operator']: + self.process_input() + + def receive_data(self, data, source_port_name=None): + self.process_input() diff --git a/Nodes/multiply_node.py b/Nodes/multiply_node.py deleted file mode 100644 index c5211c9..0000000 --- a/Nodes/multiply_node.py +++ /dev/null @@ -1,36 +0,0 @@ -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/port_node.py b/Nodes/port_node.py new file mode 100644 index 0000000..6a71524 --- /dev/null +++ b/Nodes/port_node.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +from NodeGraphQt.errors import PortRegistrationError +from NodeGraphQt.nodes.base_node import BaseNode +from NodeGraphQt.qgraphics.node_port_in import PortInputNodeItem +from NodeGraphQt.qgraphics.node_port_out import PortOutputNodeItem + + +class PortInputNode(BaseNode): + """ + The ``PortInputNode`` is the node that represents a input port from a + :class:`NodeGraphQt.GroupNode` when expanded in a + :class:`NodeGraphQt.SubGraph`. + + .. inheritance-diagram:: NodeGraphQt.nodes.port_node.PortInputNode + :parts: 1 + + .. image:: ../_images/port_in_node.png + :width: 150px + + - + """ + + NODE_NAME = 'InputPort' + + def __init__(self, qgraphics_item=None, parent_port=None): + super(PortInputNode, self).__init__(qgraphics_item or PortInputNodeItem) + self._parent_port = parent_port + + @property + def parent_port(self): + """ + The parent group node port representing this node. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._parent_port + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This is not available for the ``PortInputNode`` class. + """ + raise PortRegistrationError( + '"{}.add_input()" is not available for {}.' + .format(self.__class__.__name__, self) + ) + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This function is called by :meth:`NodeGraphQt.SubGraph.expand_group_node` + and is not available for the ``PortInputNode`` class. + """ + if self._outputs: + raise PortRegistrationError( + '"{}.add_output()" only ONE output is allowed for this node.' + .format(self.__class__.__name__, self) + ) + super(PortInputNode, self).add_output( + name=name, + multi_output=multi_output, + display_name=False, + color=color, + locked=locked, + painter_func=None + ) + + +class PortOutputNode(BaseNode): + """ + The ``PortOutputNode`` is the node that represents a output port from a + :class:`NodeGraphQt.GroupNode` when expanded in a + :class:`NodeGraphQt.SubGraph`. + + .. inheritance-diagram:: NodeGraphQt.nodes.port_node.PortOutputNode + :parts: 1 + + .. image:: ../_images/port_out_node.png + :width: 150px + + - + """ + + NODE_NAME = 'OutputPort' + + def __init__(self, qgraphics_item=None, parent_port=None): + super(PortOutputNode, self).__init__( + qgraphics_item or PortOutputNodeItem + ) + self._parent_port = parent_port + + @property + def parent_port(self): + """ + The parent group node port representing this node. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._parent_port + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This function is called by :meth:`NodeGraphQt.SubGraph.expand_group_node` + and is not available for the ``PortOutputNode`` class. + """ + if self._inputs: + raise PortRegistrationError( + '"{}.add_input()" only ONE input is allowed for this node.' + .format(self.__class__.__name__, self) + ) + super(PortOutputNode, self).add_input( + name=name, + multi_input=multi_input, + display_name=False, + color=color, + locked=locked, + painter_func=None + ) + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This is not available for the ``PortOutputNode`` class. + """ + raise PortRegistrationError( + '"{}.add_output()" is not available for {}.' + .format(self.__class__.__name__, self) + ) \ No newline at end of file diff --git a/Nodes/subtract_node.py b/Nodes/subtract_node.py deleted file mode 100644 index 08d37af..0000000 --- a/Nodes/subtract_node.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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/debug_processed.png b/debug_processed.png index 981699c..6a2aa34 100644 Binary files a/debug_processed.png and b/debug_processed.png differ diff --git a/debug_screenshot.png b/debug_screenshot.png index a69d3d1..b67056c 100644 Binary files a/debug_screenshot.png and b/debug_screenshot.png differ