Commit before rebuilding in OdenGraphQT
This commit is contained in:
parent
6e311de24b
commit
2a95535e0a
102
Experimental/experimental_nodes.py
Normal file
102
Experimental/experimental_nodes.py
Normal 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.
BIN
Nodes/__pycache__/backdrop_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/backdrop_node.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/__pycache__/base_circle_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/base_circle_node.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/__pycache__/base_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/base_node.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Nodes/__pycache__/group_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/group_node.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/__pycache__/math_operation_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/math_operation_node.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Nodes/__pycache__/port_node.cpython-312.pyc
Normal file
BIN
Nodes/__pycache__/port_node.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -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.
|
||||
|
@ -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
184
Nodes/backdrop_node.py
Normal 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
46
Nodes/base_circle_node.py
Normal 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
876
Nodes/base_node.py
Normal 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
|
@ -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):
|
||||
|
@ -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"
|
@ -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)
|
||||
|
176
Nodes/group_node.py
Normal file
176
Nodes/group_node.py
Normal 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)
|
121
Nodes/math_operation_node.py
Normal file
121
Nodes/math_operation_node.py
Normal 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()
|
@ -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
135
Nodes/port_node.py
Normal 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)
|
||||
)
|
@ -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 |
Loading…
x
Reference in New Issue
Block a user