node groups
This commit is contained in:
parent
f7f74a4cc1
commit
cebc165282
4 changed files with 522 additions and 9 deletions
45
__init__.py
45
__init__.py
|
|
@ -16,6 +16,7 @@ bl_info = {
|
|||
}
|
||||
|
||||
_registered_nodeslicer = False
|
||||
_keymaps = []
|
||||
|
||||
def register():
|
||||
global _registered_nodeslicer
|
||||
|
|
@ -27,20 +28,52 @@ def register():
|
|||
bpy.types.Object.nodeslicer = bpy.props.PointerProperty(type=SlicerObjectPropertyGroup, name="nodeslicer") #type: ignore
|
||||
_registered_nodeslicer = True
|
||||
|
||||
# Register Keymaps
|
||||
wm = bpy.context.window_manager
|
||||
if wm.keyconfigs.addon:
|
||||
km = wm.keyconfigs.addon.keymaps.new(name='Node Editor', space_type='NODE_EDITOR')
|
||||
kmi = km.keymap_items.new("slicenode.group_make", 'G', 'PRESS', ctrl=True)
|
||||
_keymaps.append((km, kmi))
|
||||
kmi = km.keymap_items.new("node.group_ungroup", 'G', 'PRESS', ctrl=True, alt=True)
|
||||
_keymaps.append((km, kmi))
|
||||
# Custom Tab toggle for groups
|
||||
kmi = km.keymap_items.new("slicenode.group_toggle", 'TAB', 'PRESS')
|
||||
_keymaps.append((km, kmi))
|
||||
|
||||
print('Nodeslicer Loaded')
|
||||
|
||||
def unregister():
|
||||
global _registered_nodeslicer
|
||||
if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler)
|
||||
try:
|
||||
if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler)
|
||||
except Exception as e:
|
||||
print(f"Error removing handler: {e}")
|
||||
|
||||
bpy.types.NODE_MT_add.remove(tree.draw_add_menu)
|
||||
try:
|
||||
bpy.types.NODE_MT_add.remove(tree.draw_add_menu)
|
||||
except Exception as e:
|
||||
print(f"Error removing menu: {e}")
|
||||
|
||||
# Unregister Keymaps safely
|
||||
try:
|
||||
for km, kmi in _keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
except Exception as e:
|
||||
print(f"Error removing keymaps: {e}")
|
||||
finally:
|
||||
_keymaps.clear()
|
||||
|
||||
registry.blender_unregister_classes()
|
||||
|
||||
if _registered_nodeslicer:
|
||||
if hasattr(bpy.types.Object, "nodeslicer"):
|
||||
del bpy.types.Object.nodeslicer #type: ignore
|
||||
_registered_nodeslicer = False
|
||||
try:
|
||||
if hasattr(bpy.types.Object, "nodeslicer"):
|
||||
del bpy.types.Object.nodeslicer #type: ignore
|
||||
except Exception as e:
|
||||
print(f"Error removing property: {e}")
|
||||
finally:
|
||||
_registered_nodeslicer = False
|
||||
|
||||
print('Nodeslicer Unloaded')
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ class LabelNodeSocket(SliceNodeSocket):
|
|||
class InputNodeSocket(SliceNodeSocket):
|
||||
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
assert layout
|
||||
if node and isinstance(node, bpy.types.NodeCustomGroup):
|
||||
if self.is_output or self.is_linked:
|
||||
layout.label(text=text or self.name)
|
||||
else:
|
||||
self._draw_prop(text, layout)
|
||||
return
|
||||
|
||||
if not self.is_output and self.is_linked:
|
||||
layout.label(text=text or self.name)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -115,11 +115,300 @@ def depsgraph_update_handler(scene, depsgraph):
|
|||
tree_update_handler(scene, depsgraph)
|
||||
object_init_handler(scene, depsgraph)
|
||||
|
||||
# Sync SLICENODE_Group sockets if interface changed
|
||||
for tree in bpy.data.node_groups:
|
||||
if tree.bl_idname == "SLICETREE_NodeTree":
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname == "SLICENODE_Group" and hasattr(node, "sync_sockets"):
|
||||
try:
|
||||
node.sync_sockets()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@register_class
|
||||
class SLICETREE_NodeTree(NodeTree):
|
||||
bl_idname = "SLICETREE_NodeTree"
|
||||
bl_label = "NodeSlicer Tree"
|
||||
bl_icon = "NODETREE"
|
||||
bl_group_idname = "SLICENODE_Group"
|
||||
bl_use_group_interface = True
|
||||
|
||||
@register_class
|
||||
class SLICENODE_OT_GroupToggle(Operator):
|
||||
bl_idname = "slicenode.group_toggle"
|
||||
bl_label = "Toggle Group"
|
||||
bl_description = "Enter or exit the selected node group"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.space_data.type == 'NODE_EDITOR' and
|
||||
context.space_data.tree_type == "SLICETREE_NodeTree")
|
||||
|
||||
def execute(self, context):
|
||||
space = context.space_data
|
||||
path = getattr(space, 'path', None)
|
||||
if not path:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# If we are inside a group, popping returns us to the parent
|
||||
# We want to exit if: 1. No node is selected, OR 2. We press Tab while editing inside a group
|
||||
active_node = context.active_node
|
||||
|
||||
# If we have an active group node, we try to enter it
|
||||
if active_node and active_node.bl_idname == "SLICENODE_Group":
|
||||
group_tree = getattr(active_node, "slicer_tree", None)
|
||||
if group_tree:
|
||||
path.append(group_tree, node=active_node)
|
||||
return {'FINISHED'}
|
||||
|
||||
# Otherwise, if we are nested, we pop out
|
||||
if len(path) > 1:
|
||||
path.pop()
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'CANCELLED'}
|
||||
|
||||
@register_class
|
||||
class SLICENODE_OT_GroupMake(Operator):
|
||||
bl_idname = "slicenode.group_make"
|
||||
bl_label = "Make Group"
|
||||
bl_description = "Make a group from selected nodes"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.space_data.type == 'NODE_EDITOR' and
|
||||
context.space_data.edit_tree and
|
||||
context.space_data.edit_tree.bl_idname == "SLICETREE_NodeTree" and
|
||||
any(n.select for n in context.space_data.edit_tree.nodes))
|
||||
|
||||
def execute(self, context):
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
space = context.space_data
|
||||
tree = space.edit_tree
|
||||
|
||||
selected_nodes = [n for n in tree.nodes if n.select and n.bl_idname != 'NodeReroute']
|
||||
if not selected_nodes:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Store selection to restore later or for mapping
|
||||
original_selection = selected_nodes[:]
|
||||
|
||||
# Create new tree
|
||||
new_tree = bpy.data.node_groups.new("NodeGroup", "SLICETREE_NodeTree")
|
||||
|
||||
# 1. Map connections
|
||||
node_map = {}
|
||||
for old_node in selected_nodes:
|
||||
new_node = new_tree.nodes.new(old_node.bl_idname)
|
||||
new_node.location = old_node.location
|
||||
new_node.name = old_node.name
|
||||
# Copy node properties (simplified)
|
||||
for prop in old_node.bl_rna.properties:
|
||||
if prop.is_readonly or prop.identifier in {'rna_type', 'name', 'location', 'dimensions'}:
|
||||
continue
|
||||
try:
|
||||
setattr(new_node, prop.identifier, getattr(old_node, prop.identifier))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Copy socket properties (value, default_value, etc)
|
||||
for old_sock, new_sock in zip(old_node.inputs, new_node.inputs):
|
||||
for prop in old_sock.bl_rna.properties:
|
||||
if prop.is_readonly or prop.identifier in {'rna_type', 'name', 'identifier', 'is_linked', 'is_output', 'hide'}:
|
||||
continue
|
||||
try:
|
||||
setattr(new_sock, prop.identifier, getattr(old_sock, prop.identifier))
|
||||
except:
|
||||
pass
|
||||
|
||||
for old_sock, new_sock in zip(old_node.outputs, new_node.outputs):
|
||||
for prop in old_sock.bl_rna.properties:
|
||||
if prop.is_readonly or prop.identifier in {'rna_type', 'name', 'identifier', 'is_linked', 'is_output', 'hide'}:
|
||||
continue
|
||||
try:
|
||||
setattr(new_sock, prop.identifier, getattr(old_sock, prop.identifier))
|
||||
except:
|
||||
pass
|
||||
|
||||
node_map[old_node] = new_node
|
||||
|
||||
internal_links = []
|
||||
external_inputs = [] # link
|
||||
external_outputs = [] # link
|
||||
|
||||
for link in tree.links:
|
||||
from_in_selection = link.from_node in node_map
|
||||
to_in_selection = link.to_node in node_map
|
||||
|
||||
if from_in_selection and to_in_selection:
|
||||
internal_links.append(link)
|
||||
elif to_in_selection:
|
||||
external_inputs.append(link)
|
||||
elif from_in_selection:
|
||||
external_outputs.append(link)
|
||||
|
||||
# 2. Create interface and Group Input/Output nodes
|
||||
has_interface = hasattr(new_tree, "interface")
|
||||
avg_loc = sum((n.location for n in selected_nodes), Vector((0,0))) / len(selected_nodes)
|
||||
|
||||
print(f"GroupMake: new_tree '{new_tree.name}' interface type: {type(new_tree.interface)}")
|
||||
|
||||
group_input_node = new_tree.nodes.new("NodeGroupInput")
|
||||
group_input_node.location = (min(n.location.x for n in node_map.values()) - 400, avg_loc.y)
|
||||
|
||||
group_output_node = new_tree.nodes.new("NodeGroupOutput")
|
||||
group_output_node.location = (max(n.location.x for n in node_map.values()) + 400, avg_loc.y)
|
||||
|
||||
# Map external inputs to new tree inputs
|
||||
input_mapping = [] # (old_link, interface_item)
|
||||
print(f"GroupMake: Processing {len(external_inputs)} external inputs")
|
||||
for link in external_inputs:
|
||||
# Check if we already have an interface item for this source socket
|
||||
item = next((m[1] for m in input_mapping if m[0].from_socket == link.from_socket), None)
|
||||
if not item:
|
||||
name = link.to_socket.name
|
||||
if has_interface:
|
||||
socket_type = link.to_socket.bl_idname
|
||||
print(f"GroupMake: Creating interface input '{name}' of type '{socket_type}'")
|
||||
try:
|
||||
item = new_tree.interface.new_socket(name=name, in_out='INPUT', socket_type=socket_type)
|
||||
print(f"GroupMake: Successfully created item '{item.name}' (ID: {item.identifier})")
|
||||
except Exception as e:
|
||||
print(f"GroupMake: FAILED to create interface input: {e}")
|
||||
continue
|
||||
else:
|
||||
item = new_tree.inputs.new(link.to_socket.bl_idname, name)
|
||||
input_mapping.append((link, item))
|
||||
|
||||
# Find the corresponding socket on group_input_node
|
||||
target_socket = None
|
||||
if has_interface:
|
||||
print(f"GroupMake: Looking for GroupInput socket with identifier '{item.identifier}' or name '{item.name}'")
|
||||
target_socket = group_input_node.outputs.get(item.identifier) or group_input_node.outputs.get(item.name)
|
||||
else:
|
||||
target_socket = group_input_node.outputs[new_tree.inputs.find(item.name)]
|
||||
|
||||
if target_socket:
|
||||
new_tree.links.new(target_socket, node_map[link.to_node].inputs[link.to_socket.identifier])
|
||||
else:
|
||||
print(f"GroupMake: Warning - target socket not found on GroupInput. Available: {[o.name for o in group_input_node.outputs]}")
|
||||
|
||||
# Map external outputs to new tree outputs
|
||||
output_mapping = [] # (old_link, interface_item)
|
||||
print(f"GroupMake: Processing {len(external_outputs)} external outputs")
|
||||
for link in external_outputs:
|
||||
item = next((m[1] for m in output_mapping if m[0].from_socket == link.from_socket), None)
|
||||
if not item:
|
||||
name = link.from_socket.name
|
||||
if has_interface:
|
||||
socket_type = link.from_socket.bl_idname
|
||||
print(f"GroupMake: Creating interface output '{name}' of type '{socket_type}'")
|
||||
try:
|
||||
item = new_tree.interface.new_socket(name=name, in_out='OUTPUT', socket_type=socket_type)
|
||||
print(f"GroupMake: Successfully created item '{item.name}' (ID: {item.identifier})")
|
||||
except Exception as e:
|
||||
print(f"GroupMake: FAILED to create interface output: {e}")
|
||||
continue
|
||||
else:
|
||||
item = new_tree.outputs.new(link.from_socket.bl_idname, name)
|
||||
output_mapping.append((link, item))
|
||||
|
||||
target_socket = None
|
||||
if has_interface:
|
||||
print(f"GroupMake: Looking for GroupOutput socket with identifier '{item.identifier}' or name '{item.name}'")
|
||||
target_socket = group_output_node.inputs.get(item.identifier) or group_output_node.inputs.get(item.name)
|
||||
else:
|
||||
target_socket = group_output_node.inputs[new_tree.outputs.find(item.name)]
|
||||
|
||||
if target_socket:
|
||||
new_tree.links.new(node_map[link.from_node].outputs[link.from_socket.identifier], target_socket)
|
||||
else:
|
||||
print(f"GroupMake: Warning - target socket not found on GroupOutput. Available: {[i.name for i in group_output_node.inputs]}")
|
||||
|
||||
# 3. Re-create internal links
|
||||
for link in internal_links:
|
||||
try:
|
||||
new_tree.links.new(node_map[link.from_node].outputs[link.from_socket.identifier],
|
||||
node_map[link.to_node].inputs[link.to_socket.identifier])
|
||||
except:
|
||||
pass
|
||||
|
||||
# 4. Create the group node and assign tree
|
||||
group_node = tree.nodes.new("SLICENODE_Group")
|
||||
group_node.location = avg_loc
|
||||
|
||||
# Robustly get interface items count
|
||||
interface_items = []
|
||||
if has_interface:
|
||||
items_attr = getattr(new_tree.interface, "items", None)
|
||||
if items_attr is not None and not callable(items_attr):
|
||||
interface_items = items_attr
|
||||
else:
|
||||
# Try iterating directly
|
||||
try:
|
||||
interface_items = [item for item in new_tree.interface]
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"GroupMake: Assigning new tree to group node. Interface items found: {len(interface_items)}")
|
||||
for item in interface_items:
|
||||
print(f" - Item: {getattr(item, 'name', 'unnamed')}, Type: {getattr(item, 'item_type', 'unknown')}, Socket: {getattr(item, 'socket_type', 'unknown')}")
|
||||
|
||||
# Assigning slicer_tree triggers mirroring via update callback
|
||||
group_node.slicer_tree = new_tree
|
||||
|
||||
# Force an update of the external sockets to be sure
|
||||
if hasattr(new_tree, "interface_update"):
|
||||
print("GroupMake: Calling interface_update(bpy.context)")
|
||||
try:
|
||||
new_tree.interface_update(bpy.context)
|
||||
except Exception as e:
|
||||
print(f"GroupMake: interface_update failed: {e}")
|
||||
|
||||
# Manually force sync
|
||||
if hasattr(group_node, "sync_sockets"):
|
||||
group_node.sync_sockets()
|
||||
|
||||
print(f"GroupMake: Post-sync socket count: {len(group_node.inputs)} in, {len(group_node.outputs)} out")
|
||||
|
||||
# Re-link external
|
||||
# The group_node should have sockets mirrored from new_tree.interface
|
||||
for link, item in input_mapping:
|
||||
target_socket = None
|
||||
if has_interface:
|
||||
target_socket = group_node.inputs.get(item.name) # Try name first
|
||||
if not target_socket:
|
||||
target_socket = group_node.inputs.get(item.identifier)
|
||||
else:
|
||||
target_socket = group_node.inputs.get(item.name)
|
||||
|
||||
if target_socket:
|
||||
tree.links.new(link.from_socket, target_socket)
|
||||
|
||||
for link, item in output_mapping:
|
||||
target_socket = None
|
||||
if has_interface:
|
||||
target_socket = group_node.outputs.get(item.name)
|
||||
if not target_socket:
|
||||
target_socket = group_node.outputs.get(item.identifier)
|
||||
else:
|
||||
target_socket = group_node.outputs.get(item.name)
|
||||
|
||||
if target_socket:
|
||||
tree.links.new(target_socket, link.to_socket)
|
||||
|
||||
# 5. Delete old nodes
|
||||
for node in selected_nodes:
|
||||
tree.nodes.remove(node)
|
||||
|
||||
# 6. Enter the group
|
||||
space.path.append(new_tree, node=group_node) # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@register_class
|
||||
class SLICENODE_OT_Execute(Operator):
|
||||
|
|
@ -239,6 +528,21 @@ class SLICENODE_MT_ActionIO(Menu):
|
|||
layout.operator("node.add_node", text="Executor").type = "SLICENODE_Executor"
|
||||
layout.operator("node.add_node", text="Test").type = "SLICENODE_Test"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_MT_Group(Menu):
|
||||
bl_idname = "SLICENODE_MT_Group"
|
||||
bl_label = "Group"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
assert layout
|
||||
layout.operator("slicenode.group_make", text="Make Group")
|
||||
layout.operator("node.group_ungroup", text="Ungroup")
|
||||
layout.operator("slicenode.group_toggle", text="Edit Group")
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Group Input").type = "NodeGroupInput"
|
||||
layout.operator("node.add_node", text="Group Output").type = "NodeGroupOutput"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_MT_Menu(Menu):
|
||||
bl_idname = "SLICENODE_MT_Menu"
|
||||
|
|
@ -252,6 +556,8 @@ class SLICENODE_MT_Menu(Menu):
|
|||
layout.menu("SLICENODE_MT_Gcode")
|
||||
layout.menu("SLICENODE_MT_Joiners")
|
||||
layout.menu("SLICENODE_MT_ActionIO")
|
||||
layout.separator()
|
||||
layout.menu("SLICENODE_MT_Group")
|
||||
|
||||
|
||||
def draw_add_menu(self, context):
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName):
|
|||
def draw_color(self, context, node) -> tuple[float, float, float, float]: # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
return self.bl_color
|
||||
|
||||
from ...registry import get_label_by_name
|
||||
from ...registry import get_label_by_name, register_class
|
||||
class SliceNode(Node, AutoBlIdName):
|
||||
def update(self) -> None:
|
||||
self._update_multi_groups()
|
||||
|
|
@ -156,6 +156,166 @@ def node_execution_feedback(node: bpy.types.Node):
|
|||
node.color = old_color
|
||||
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
|
||||
|
||||
@contextmanager
|
||||
def _group_context(node: bpy.types.Node):
|
||||
stack = _get_group_stack()
|
||||
stack.append(node)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
stack.pop()
|
||||
|
||||
def _get_group_stack() -> list[bpy.types.Node]:
|
||||
cache = _current_execution_cache
|
||||
if cache is None: return []
|
||||
if '_group_stack' not in cache:
|
||||
cache['_group_stack'] = []
|
||||
return cache['_group_stack']
|
||||
|
||||
def _get_current_group_node() -> bpy.types.Node | None:
|
||||
stack = _get_group_stack()
|
||||
return stack[-1] if stack else None
|
||||
|
||||
@register_class
|
||||
class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
|
||||
bl_label = "Group"
|
||||
|
||||
def _tree_updated(self, context):
|
||||
try:
|
||||
if not self.name: return
|
||||
_ = len(self.inputs)
|
||||
_ = len(self.outputs)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
self.sync_sockets()
|
||||
|
||||
slicer_tree: bpy.props.PointerProperty(
|
||||
type=bpy.types.NodeTree,
|
||||
name="Node Tree",
|
||||
update=_tree_updated
|
||||
)
|
||||
|
||||
def sync_sockets(self):
|
||||
try:
|
||||
if not self.name: return
|
||||
_ = len(self.inputs)
|
||||
_ = len(self.outputs)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if not self.slicer_tree:
|
||||
self.inputs.clear()
|
||||
self.outputs.clear()
|
||||
return
|
||||
|
||||
# 1. Collect expected sockets from interface
|
||||
expected_inputs = [] # (identifier, name, type)
|
||||
expected_outputs = []
|
||||
|
||||
has_interface = hasattr(self.slicer_tree, "interface")
|
||||
if has_interface:
|
||||
# In Blender 4.0+, the collection of items is items_tree
|
||||
items_tree = getattr(self.slicer_tree.interface, "items_tree", [])
|
||||
for item in items_tree:
|
||||
if getattr(item, "item_type", "") != 'SOCKET': continue
|
||||
target_list = expected_inputs if item.in_out == 'INPUT' else expected_outputs
|
||||
target_list.append((item.identifier, item.name, item.socket_type))
|
||||
else:
|
||||
# Fallback for older Blender API without interface
|
||||
for sock in getattr(self.slicer_tree, "inputs", []):
|
||||
expected_inputs.append((sock.identifier, sock.name, sock.bl_idname))
|
||||
for sock in getattr(self.slicer_tree, "outputs", []):
|
||||
expected_outputs.append((sock.identifier, sock.name, sock.bl_idname))
|
||||
|
||||
# 2. Sync Inputs
|
||||
self._sync_socket_collection(self.inputs, expected_inputs)
|
||||
|
||||
# 3. Sync Outputs
|
||||
self._sync_socket_collection(self.outputs, expected_outputs)
|
||||
|
||||
def _sync_socket_collection(self, collection, expected):
|
||||
# Remove sockets that shouldn't be there
|
||||
expected_ids = {e[0] for e in expected}
|
||||
to_remove = [s for s in collection if s.identifier not in expected_ids]
|
||||
|
||||
for sock in to_remove:
|
||||
collection.remove(sock)
|
||||
|
||||
# Add or update sockets
|
||||
sock_indexes = {s.identifier: i for i, s in enumerate(collection)}
|
||||
for ident, name, stype in expected:
|
||||
if ident not in sock_indexes:
|
||||
try:
|
||||
# Explicitly pass identifier to keep it perfectly synced
|
||||
sock = collection.new(stype, name, identifier=ident)
|
||||
sock_indexes[sock.identifier] = len(collection) - 1
|
||||
except Exception as e:
|
||||
print(f"Failed to create socket {name} ({stype}): {e}")
|
||||
else:
|
||||
# Update name if changed
|
||||
sock = collection[sock_indexes[ident]]
|
||||
if sock.name != name:
|
||||
sock.name = name
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "slicer_tree", text="")
|
||||
|
||||
def draw_label(self):
|
||||
if self.slicer_tree:
|
||||
return self.slicer_tree.name
|
||||
return "Group"
|
||||
|
||||
|
||||
def _resolve_group_output(socket: NodeSocket, cache: _EvalCache | None, visited: set[tuple[int, str]] | None):
|
||||
node = socket.node
|
||||
assert isinstance(node, (bpy.types.NodeGroup, SLICENODE_Group))
|
||||
|
||||
if isinstance(node, SLICENODE_Group):
|
||||
tree = node.slicer_tree
|
||||
else:
|
||||
tree = node.node_tree
|
||||
|
||||
if not tree: return None
|
||||
|
||||
try:
|
||||
idx = list(node.outputs).index(socket)
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
# Find the NodeGroupOutput node that has this input linked, or the first one
|
||||
output_node = None
|
||||
for n in tree.nodes:
|
||||
if isinstance(n, bpy.types.NodeGroupOutput):
|
||||
# NodeGroupOutput inputs usually match the group outputs by index
|
||||
if idx < len(n.inputs) and n.inputs[idx].is_linked:
|
||||
output_node = n
|
||||
break
|
||||
|
||||
if not output_node:
|
||||
output_node = next((n for n in tree.nodes if isinstance(n, bpy.types.NodeGroupOutput)), None)
|
||||
|
||||
if not output_node or idx >= len(output_node.inputs): return None
|
||||
|
||||
inner_socket = output_node.inputs[idx]
|
||||
|
||||
with _group_context(node):
|
||||
return resolve_input(typing.cast(Any, inner_socket), cache=cache, visited=visited)
|
||||
|
||||
def _resolve_group_input(socket: NodeSocket, cache: _EvalCache | None, visited: set[tuple[int, str]] | None):
|
||||
group_node = _get_current_group_node()
|
||||
if not group_node: return None
|
||||
|
||||
# NodeGroupInput outputs usually match the group inputs by index
|
||||
try:
|
||||
idx = list(socket.node.outputs).index(socket)
|
||||
outer_socket = group_node.inputs[idx]
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
return resolve_input(typing.cast(Any, outer_socket), cache=cache, visited=visited)
|
||||
|
||||
def _follow_reroute(socket: NodeSocket) -> NodeSocket:
|
||||
current: NodeSocket = socket
|
||||
seen: set[int] = set()
|
||||
|
|
@ -222,18 +382,25 @@ def _resolve_output(
|
|||
visited = set() if visited is None else visited
|
||||
|
||||
assert socket.node
|
||||
# Include group stack in cache key to ensure independent evaluation for multiple instances
|
||||
group_stack = tuple(n.as_pointer() for n in _get_group_stack())
|
||||
|
||||
# Use identifier instead of name for the cache key, as multiple sockets
|
||||
# (especially in multi-groups) can share the same name but must have unique identifiers.
|
||||
key = (socket.node.as_pointer(), socket.node.name, socket.identifier)
|
||||
key = (group_stack, socket.node.as_pointer(), socket.node.name, socket.identifier)
|
||||
if key in cache: return cache[key]
|
||||
|
||||
node_key = (socket.node.as_pointer(), socket.node.name)
|
||||
node_key = (group_stack, socket.node.as_pointer(), socket.node.name)
|
||||
if node_key in visited: return default # cycle guard
|
||||
|
||||
visited.add(node_key)
|
||||
try:
|
||||
with node_execution_feedback(socket.node):
|
||||
if not isinstance(socket.node, SliceNode):
|
||||
if isinstance(socket.node, (bpy.types.NodeGroup, SLICENODE_Group)):
|
||||
return _resolve_group_output(socket, cache=cache, visited=visited)
|
||||
if isinstance(socket.node, bpy.types.NodeGroupInput):
|
||||
return _resolve_group_input(socket, cache=cache, visited=visited)
|
||||
return default
|
||||
|
||||
# Set these temporarily so self.resolve() can find them
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue