Commit before rebuilding in OdenGraphQT

This commit is contained in:
Nicole Rappe 2025-02-12 21:06:59 -07:00
parent 6e311de24b
commit 2a95535e0a
36 changed files with 1645 additions and 179 deletions

View File

@ -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_())

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -8,9 +8,11 @@ class ArrayNode(BaseNode):
- Stores incoming values in an array with size defined by ArraySize. - Stores incoming values in an array with size defined by ArraySize.
When full, it removes the oldest value. When full, it removes the oldest value.
""" """
__identifier__ = 'io.github.nicole.array' __identifier__ = 'bunny-lab.io.array_node'
NODE_NAME = 'Array' NODE_NAME = 'Array'
def __init__(self): def __init__(self):
super(ArrayNode, self).__init__() super(ArrayNode, self).__init__()
self.values = {} # Ensure values is a dictionary. self.values = {} # Ensure values is a dictionary.

View File

@ -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}")

184
Nodes/backdrop_node.py Normal file
View File

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

46
Nodes/base_circle_node.py Normal file
View File

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

876
Nodes/base_node.py Normal file
View File

@ -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: {<port_name>: <port_object>}
"""
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: {<port_name>: <port_object>}
"""
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: {<input_port>: <node_list>}
"""
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: {<output_port>: <node_list>}
"""
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: {<node_type>: {<port_type>: [<port_name>]}}
"""
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: {<node_type>: {<port_type>: [<port_name>]}}
"""
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

View File

@ -44,7 +44,7 @@ def get_draw_stat_port(color, border_color=None, alpha=127):
return painter_func return painter_func
class CharacterStatusNode(BaseNode): class CharacterStatusNode(BaseNode):
__identifier__ = 'io.github.nicole.status' __identifier__ = 'bunny-lab.io.status_node'
NODE_NAME = 'Character Status' NODE_NAME = 'Character Status'
def __init__(self): def __init__(self):

View File

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

View File

@ -18,7 +18,7 @@ Behavior:
from NodeGraphQt import BaseNode from NodeGraphQt import BaseNode
class DataNode(BaseNode): class DataNode(BaseNode):
__identifier__ = 'io.github.nicole.data' __identifier__ = 'bunny-lab.io.data_node'
NODE_NAME = 'Data Node' NODE_NAME = 'Data Node'
def __init__(self): def __init__(self):
@ -50,7 +50,6 @@ class DataNode(BaseNode):
""" """
current_text = self.get_property('value') current_text = self.get_property('value')
self.value = current_text self.value = current_text
self.set_name(f"Data Node")
def property_changed(self, property_name): def property_changed(self, property_name):
""" """
@ -114,7 +113,6 @@ class DataNode(BaseNode):
Receives data from connected nodes and updates the internal value. 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_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 # Transmit data further if there's an output connection
output_port = self.output(0) output_port = self.output(0)

176
Nodes/group_node.py Normal file
View File

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

View File

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

View File

@ -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}")

135
Nodes/port_node.py Normal file
View File

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

View File

@ -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}")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB