Skip to content

Commit 194dc50

Browse files
author
Bryan Howard
committed
socket types are not determined by type hints. no longer hardcoded
1 parent fa97faf commit 194dc50

File tree

4 files changed

+115
-66
lines changed

4 files changed

+115
-66
lines changed

color_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# color_utils.py
2+
# A utility for generating consistent, bright colors from arbitrary strings.
3+
4+
import hashlib
5+
from PySide6.QtGui import QColor
6+
7+
# A cache to store generated colors so we don't have to re-hash every time.
8+
COLOR_CACHE = {}
9+
10+
11+
def generate_color_from_string(type_str: str) -> QColor:
12+
"""
13+
Generates a consistent, bright QColor from a given string (e.g., a type hint).
14+
15+
This function uses a hashing algorithm to ensure the same string always
16+
produces the same color. The hash is then used to generate HSV values
17+
that are constrained to a bright and colorful range.
18+
19+
:param type_str: The string to generate a color for.
20+
:return: A QColor instance.
21+
"""
22+
type_str = type_str.lower()
23+
24+
if type_str in COLOR_CACHE:
25+
return COLOR_CACHE[type_str]
26+
27+
# Special case for a generic 'any' type for reroute nodes, etc.
28+
if type_str == "any":
29+
color = QColor("#C0C0C0") # Grey
30+
COLOR_CACHE[type_str] = color
31+
return color
32+
33+
# Use SHA1 to get a consistent hash from the string
34+
hash_object = hashlib.sha1(type_str.encode("utf-8"))
35+
hex_digest = hash_object.hexdigest()
36+
37+
# Use parts of the hash to determine Hue, Saturation, and Value
38+
# We use integer conversion on slices of the hex hash string
39+
40+
# Hue: Full 360-degree range for variety
41+
hue = int(hex_digest[:4], 16) % 360
42+
43+
# Saturation: Constrained to be high (180-255) to avoid dull colors
44+
saturation = 180 + (int(hex_digest[4:8], 16) % 76)
45+
46+
# Value: Constrained to be high (200-255) to avoid dark colors
47+
value = 200 + (int(hex_digest[8:12], 16) % 56)
48+
49+
color = QColor.fromHsv(hue, saturation, value)
50+
COLOR_CACHE[type_str] = color
51+
52+
return color

node.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
# node.py
22
# Represents a single processing unit (node) in the graph.
3-
# This version contains the definitive fix for dynamic resizing and re-implements
4-
# the node properties context menu.
3+
# Now uses the simplified, dynamic pin creation system.
54

65
import uuid
76
import ast
87
from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem, QGraphicsProxyWidget, QPushButton, QVBoxLayout, QWidget
98
from PySide6.QtCore import QRectF, Qt, QPointF, Signal
109
from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QFont, QLinearGradient, QPainterPath, QContextMenuEvent
1110
from pin import Pin
12-
from socket_type import SocketType
1311
from code_editor_dialog import CodeEditorDialog
1412
from node_properties_dialog import NodePropertiesDialog
1513

@@ -78,13 +76,12 @@ def contextMenuEvent(self, event: QContextMenuEvent):
7876
self._title_item.setPlainText(self.title)
7977
self.color_title_bar = QColor(props["title_color"])
8078
self.color_body = QColor(props["body_color"])
81-
self.update() # Redraw the node with new colors
79+
self.update()
8280

8381
def _create_content_widget(self):
8482
"""Creates the main content area with the custom GUI and a single control button."""
8583
self.content_container = ResizableWidgetContainer()
8684
self.content_container.setAttribute(Qt.WA_TranslucentBackground)
87-
# Connect the custom resized signal to our layout update function
8885
self.content_container.resized.connect(self._update_layout)
8986

9087
main_layout = QVBoxLayout(self.content_container)
@@ -127,7 +124,6 @@ def rebuild_gui(self):
127124
error_label.setStyleSheet("color: red;")
128125
self.custom_widget_layout.addWidget(error_label)
129126

130-
# Trigger an initial layout update after building the GUI
131127
self._update_layout()
132128

133129
def get_gui_values(self):
@@ -206,6 +202,7 @@ def _update_layout(self):
206202
num_pins = max(len(self.input_pins), len(self.output_pins))
207203
pin_area_height = (num_pins * pin_spacing) if num_pins > 0 else 0
208204

205+
self.content_container.layout().activate()
209206
content_size = self.content_container.sizeHint()
210207
content_height = content_size.height()
211208

@@ -249,7 +246,6 @@ def paint(self, painter: QPainter, option, widget=None):
249246
painter.drawLine(0, 32, self.width, 32)
250247
painter.restore()
251248

252-
# --- Other methods remain the same ---
253249
def get_pin_by_name(self, name):
254250
for pin in self.pins:
255251
if pin.name == name:
@@ -296,6 +292,7 @@ def update_pins_from_code(self):
296292
new_outputs["output_1"] = self._parse_type_hint(return_annotation).lower()
297293
except (SyntaxError, AttributeError):
298294
return
295+
299296
current_inputs = {pin.name: pin for pin in self.input_pins}
300297
current_outputs = {pin.name: pin for pin in self.output_pins}
301298
for name, pin in list(current_inputs.items()):
@@ -313,11 +310,8 @@ def update_pins_from_code(self):
313310
self._update_layout()
314311

315312
def add_pin(self, name, direction, pin_type_str):
316-
type_map = {"STR": "STRING", "BOOL": "BOOLEAN"}
317-
processed_type_str = pin_type_str.upper().split("[")[0]
318-
final_type_str = type_map.get(processed_type_str, processed_type_str)
319-
pin_type = SocketType[final_type_str] if final_type_str in SocketType.__members__ else SocketType.ANY
320-
pin = Pin(self, name, direction, pin_type)
313+
"""Creates a new Pin, passing the raw type string for color generation."""
314+
pin = Pin(self, name, direction, pin_type_str)
321315
self.pins.append(pin)
322316
if direction == "input":
323317
self.input_pins.append(pin)

pin.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
11
# pin.py
22
# Represents an input or output socket on a node.
3-
# Now manages its own text label.
3+
# Now uses a dynamic string for its type and generates its color procedurally.
44

55
import uuid
66
from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem
77
from PySide6.QtCore import QRectF, Qt
88
from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QFont
9-
from socket_type import SocketType
9+
from color_utils import generate_color_from_string
10+
1011

1112
class Pin(QGraphicsItem):
1213
"""
1314
A pin represents an input or output on a Node.
14-
It handles drawing itself, its label, and its connections.
15+
Its type is a string, and its color is generated dynamically from that string.
1516
"""
16-
def __init__(self, node, name, direction, pin_type_enum, parent=None):
17+
18+
def __init__(self, node, name, direction, pin_type_str, parent=None):
1719
super().__init__(node)
18-
20+
1921
self.node = node
2022
self.name = name
2123
self.direction = direction
22-
self.pin_type = pin_type_enum
24+
self.pin_type = pin_type_str # The type is now just a string
2325
self.uuid = str(uuid.uuid4())
24-
26+
2527
self.radius = 6
2628
self.connections = []
2729
self.value = None
2830
self.label_margin = 8
2931

30-
# --- Visuals ---
31-
self.color = self.pin_type.value
32+
# --- Dynamic Color Generation ---
33+
self.color = generate_color_from_string(self.pin_type)
3234
self.brush = QBrush(self.color)
3335
self.pen = QPen(QColor("#F0F0F0"))
3436
self.pen.setWidth(2)
35-
37+
3638
# --- Label ---
3739
self.label = QGraphicsTextItem(self.name.replace("_", " ").title(), self)
3840
self.label.setDefaultTextColor(QColor("#FFDDDDDD"))
@@ -51,11 +53,9 @@ def destroy(self):
5153
def update_label_pos(self):
5254
"""Update the position of the pin's text label relative to the pin."""
5355
if self.direction == "output":
54-
# Align text to the left of the pin
5556
label_x = -self.label.boundingRect().width() - self.label_margin
5657
self.label.setPos(label_x, -self.label.boundingRect().height() / 2)
5758
else:
58-
# Align text to the right of the pin
5959
label_x = self.label_margin
6060
self.label.setPos(label_x, -self.label.boundingRect().height() / 2)
6161

@@ -83,16 +83,24 @@ def update_connections(self):
8383
self.scene().update()
8484

8585
def can_connect_to(self, other_pin):
86-
if not other_pin or self == other_pin: return False
87-
if self.node == other_pin.node: return False
88-
if self.direction == other_pin.direction: return False
89-
if other_pin.direction == "input" and len(other_pin.connections) > 0: return False
90-
if self.direction == "input" and len(self.connections) > 0: return False
91-
92-
if self.pin_type == other_pin.pin_type: return True
93-
if self.pin_type == SocketType.ANY or other_pin.pin_type == SocketType.ANY: return True
94-
95-
return False
86+
"""Checks for compatibility based on pin type strings."""
87+
if not other_pin or self == other_pin:
88+
return False
89+
if self.node == other_pin.node:
90+
return False
91+
if self.direction == other_pin.direction:
92+
return False
93+
if other_pin.direction == "input" and len(other_pin.connections) > 0:
94+
return False
95+
if self.direction == "input" and len(self.connections) > 0:
96+
return False
97+
98+
# Allow 'any' to connect to anything
99+
if self.pin_type == "any" or other_pin.pin_type == "any":
100+
return True
101+
102+
# Otherwise, require an exact type match
103+
return self.pin_type == other_pin.pin_type
96104

97105
def mousePressEvent(self, event):
98106
if event.button() == Qt.LeftButton:
@@ -102,4 +110,4 @@ def mousePressEvent(self, event):
102110
event.ignore()
103111

104112
def serialize(self):
105-
return {"uuid": self.uuid, "name": self.name, "direction": self.direction, "type": self.pin_type.name.lower()}
113+
return {"uuid": self.uuid, "name": self.name, "direction": self.direction, "type": self.pin_type}

reroute_node.py

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
# reroute_node.py
22
# A simple, draggable node for organizing connections.
3-
# Now with a serialize method for saving and loading.
3+
# Now uses the dynamic color generation system.
44

55
import uuid
66
from PySide6.QtWidgets import QGraphicsItem, QStyle
77
from PySide6.QtCore import QRectF, QPointF
8-
from PySide6.QtGui import (QPainter, QColor, QBrush, QPen, QRadialGradient,
9-
QKeyEvent, QPainterPath)
8+
from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QRadialGradient, QKeyEvent, QPainterPath
109
from pin import Pin
11-
from socket_type import SocketType
10+
from color_utils import generate_color_from_string
11+
1212

1313
class RerouteNode(QGraphicsItem):
1414
"""
1515
A small, circular node that passes a connection through and
1616
adopts the color of the data type.
1717
"""
18+
1819
def __init__(self, parent=None):
1920
super().__init__(parent)
2021
self.setFlag(QGraphicsItem.ItemIsMovable)
@@ -25,22 +26,14 @@ def __init__(self, parent=None):
2526
self.title = "Reroute"
2627
self.radius = 8
2728
self.pins = []
28-
29-
self.input_pin = self.add_pin("input", "input", SocketType.ANY)
30-
self.output_pin = self.add_pin("output", "output", SocketType.ANY)
31-
29+
30+
self.input_pin = self.add_pin("input", "input", "any")
31+
self.output_pin = self.add_pin("output", "output", "any")
32+
3233
self.input_pin.setPos(0, 0)
3334
self.output_pin.setPos(0, 0)
34-
35-
self.update_color()
3635

37-
def get_pin_by_name(self, name):
38-
"""Finds a pin on this node by its name ('input' or 'output')."""
39-
if name == 'input':
40-
return self.input_pin
41-
elif name == 'output':
42-
return self.output_pin
43-
return None
36+
self.update_color()
4437

4538
def update_color(self):
4639
"""Updates the node's color based on its input connection."""
@@ -49,8 +42,8 @@ def update_color(self):
4942
new_type = source_pin.pin_type
5043
new_color = source_pin.color
5144
else:
52-
new_type = SocketType.ANY
53-
new_color = SocketType.ANY.value
45+
new_type = "any"
46+
new_color = generate_color_from_string("any")
5447

5548
self.color_base = new_color.darker(110)
5649
self.color_highlight = new_color.lighter(110)
@@ -60,23 +53,30 @@ def update_color(self):
6053

6154
self.output_pin.pin_type = new_type
6255
self.output_pin.color = new_color
63-
56+
6457
if self.scene():
6558
self.output_pin.update_connections()
6659
self.update()
6760

68-
def add_pin(self, name, direction, pin_type_enum):
69-
pin = Pin(self, name, direction, pin_type_enum)
61+
def add_pin(self, name, direction, pin_type_str):
62+
pin = Pin(self, name, direction, pin_type_str)
7063
pin.hide()
7164
self.pins.append(pin)
7265
return pin
7366

67+
def get_pin_by_name(self, name):
68+
if name == "input":
69+
return self.input_pin
70+
elif name == "output":
71+
return self.output_pin
72+
return None
73+
7474
def boundingRect(self):
7575
return QRectF(-self.radius, -self.radius, 2 * self.radius, 2 * self.radius).adjusted(-5, -5, 5, 5)
7676

7777
def shape(self):
7878
path = QPainterPath()
79-
hitbox_radius = self.radius + 4
79+
hitbox_radius = self.radius + 4
8080
path.addEllipse(QPointF(0, 0), hitbox_radius, hitbox_radius)
8181
return path
8282

@@ -89,10 +89,10 @@ def itemChange(self, change, value):
8989
def paint(self, painter: QPainter, option, widget=None):
9090
if option.state & QStyle.State_Selected:
9191
option.state &= ~QStyle.State_Selected
92-
92+
9393
draw_rect = QRectF(-self.radius, -self.radius, self.radius * 2, self.radius * 2)
9494
gradient = QRadialGradient(QPointF(0, 0), self.radius)
95-
95+
9696
if self.isSelected():
9797
painter.setPen(self.pen_selected)
9898
gradient.setColorAt(0, self.color_base.lighter(130))
@@ -106,9 +106,4 @@ def paint(self, painter: QPainter, option, widget=None):
106106
painter.drawEllipse(draw_rect)
107107

108108
def serialize(self):
109-
"""Converts the reroute node's state to a serializable dictionary."""
110-
return {
111-
"uuid": self.uuid,
112-
"pos": (self.pos().x(), self.pos().y()),
113-
"is_reroute": True
114-
}
109+
return {"uuid": self.uuid, "pos": (self.pos().x(), self.pos().y()), "is_reroute": True}

0 commit comments

Comments
 (0)