940 lines
40 KiB
Python
940 lines
40 KiB
Python
from typing import TYPE_CHECKING, Any, cast
|
|
import bpy
|
|
from bpy.types import NodeTree, Operator, Menu
|
|
from bpy.app.handlers import persistent
|
|
|
|
from ...infra.blender.blender_slice_nodes_generic import execution_cache, resolve_input
|
|
from ...registry import register_class
|
|
|
|
if TYPE_CHECKING:
|
|
from bpy.stub_internal.rna_enums import OperatorReturnItems
|
|
|
|
def _get_scene_object(name: str) -> Any:
|
|
if not name:
|
|
return None
|
|
objects = getattr(bpy.data, "objects", None)
|
|
if objects is None:
|
|
return None
|
|
if isinstance(objects, dict):
|
|
return objects.get(name)
|
|
if "Mock" in type(objects).__name__:
|
|
return None
|
|
if hasattr(objects, "get"):
|
|
return objects.get(name)
|
|
if hasattr(objects, "__getitem__"):
|
|
try:
|
|
return objects[name]
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
def _is_btype(val: Any, type_name: str) -> bool:
|
|
if val is None:
|
|
return False
|
|
btype = getattr(bpy.types, type_name, None)
|
|
if isinstance(btype, type):
|
|
try:
|
|
if isinstance(val, btype):
|
|
return True
|
|
except TypeError:
|
|
pass
|
|
val_cls_name = getattr(val, "__class__", type(None)).__name__
|
|
return val_cls_name == type_name or f"Dummy{type_name}" in val_cls_name
|
|
|
|
def get_referenced_data(node: Any, visited: set[int] | None = None) -> tuple[set[Any], set[Any]]:
|
|
"""Traverse upstream from node and collect all referenced bpy.types.Object and bpy.types.Collection."""
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
ptr = node.as_pointer() if hasattr(node, "as_pointer") and callable(node.as_pointer) else id(node)
|
|
if ptr in visited:
|
|
return set(), set()
|
|
visited.add(ptr)
|
|
|
|
objs: set[Any] = set()
|
|
cols: set[Any] = set()
|
|
|
|
# 1. Group Node: check internal nodes inside the group
|
|
if getattr(node, "bl_idname", "") == "SLICENODE_Group":
|
|
group_tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
|
|
if group_tree and hasattr(group_tree, "nodes"):
|
|
for internal_node in group_tree.nodes:
|
|
sub_objs, sub_cols = get_referenced_data(internal_node, visited)
|
|
objs.update(sub_objs)
|
|
cols.update(sub_cols)
|
|
|
|
# 2. CollectionObjects Node
|
|
if getattr(node, "bl_idname", "") == "SLICENODE_CollectionObjects":
|
|
col = getattr(node, "collection", None)
|
|
if col:
|
|
cols.add(col)
|
|
for o in getattr(col, "all_objects", getattr(col, "objects", [])):
|
|
if _is_btype(o, "Object"):
|
|
objs.add(o)
|
|
for outp in getattr(node, "outputs", []):
|
|
obj = _get_scene_object(getattr(outp, "identifier", "")) or _get_scene_object(getattr(outp, "name", ""))
|
|
if obj:
|
|
objs.add(obj)
|
|
|
|
# 3. Check node RNA pointer properties for Object or Collection
|
|
for prop in getattr(getattr(node, "bl_rna", None), "properties", []):
|
|
if getattr(prop, "type", "") == 'POINTER':
|
|
val = getattr(node, prop.identifier, None)
|
|
if _is_btype(val, "Object"):
|
|
objs.add(val)
|
|
elif _is_btype(val, "Collection"):
|
|
cols.add(val)
|
|
for o in getattr(val, "all_objects", getattr(val, "objects", [])):
|
|
if _is_btype(o, "Object"):
|
|
objs.add(o)
|
|
|
|
# 4. Check all input sockets
|
|
for inp in getattr(node, "inputs", []):
|
|
val = getattr(inp, "value", getattr(inp, "default_value", None))
|
|
if _is_btype(val, "Object"):
|
|
objs.add(val)
|
|
elif _is_btype(val, "Collection"):
|
|
cols.add(val)
|
|
|
|
# Follow upstream links
|
|
if getattr(inp, "is_linked", False):
|
|
for link in getattr(inp, "links", []):
|
|
from_node = getattr(link, "from_node", None)
|
|
if from_node:
|
|
if getattr(from_node, "bl_idname", "") == "SLICENODE_CollectionObjects":
|
|
from_sock = getattr(link, "from_socket", None)
|
|
if from_sock:
|
|
obj = _get_scene_object(getattr(from_sock, "identifier", "")) or _get_scene_object(getattr(from_sock, "name", ""))
|
|
if obj:
|
|
objs.add(obj)
|
|
sub_objs, sub_cols = get_referenced_data(from_node, visited)
|
|
objs.update(sub_objs)
|
|
cols.update(sub_cols)
|
|
|
|
# 5. Check all output sockets
|
|
for outp in getattr(node, "outputs", []):
|
|
val = getattr(outp, "value", getattr(outp, "default_value", None))
|
|
if _is_btype(val, "Object"):
|
|
objs.add(val)
|
|
elif _is_btype(val, "Collection"):
|
|
cols.add(val)
|
|
|
|
# 6. Expand all objects to include their recursive children and parents/ancestors
|
|
from ...infra.blender.blender_bridge import get_all_children
|
|
all_objs = set(objs)
|
|
for o in list(objs):
|
|
# Children
|
|
try:
|
|
for child in get_all_children(o):
|
|
all_objs.add(child)
|
|
except Exception:
|
|
pass
|
|
# Ancestors
|
|
curr = getattr(o, "parent", None)
|
|
seen_parents = set()
|
|
while curr and curr not in seen_parents and len(seen_parents) < 100:
|
|
seen_parents.add(curr)
|
|
all_objs.add(curr)
|
|
if "Mock" in type(curr).__name__:
|
|
break
|
|
curr = getattr(curr, "parent", None)
|
|
|
|
return all_objs, cols
|
|
|
|
def get_referenced_objects(node: Any, visited: set[int] | None = None) -> set[Any]:
|
|
objs, _ = get_referenced_data(node, visited)
|
|
return objs
|
|
|
|
def get_referenced_trees(node: Any, visited: set[int] | None = None) -> set[Any]:
|
|
"""Collect all NodeTree datablocks referenced upstream (e.g. via group nodes)."""
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
ptr = node.as_pointer() if hasattr(node, "as_pointer") and callable(node.as_pointer) else id(node)
|
|
if ptr in visited:
|
|
return set()
|
|
visited.add(ptr)
|
|
|
|
trees = set()
|
|
if getattr(node, "bl_idname", "") == "SLICENODE_Group":
|
|
group_tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
|
|
if group_tree:
|
|
trees.add(group_tree)
|
|
if hasattr(group_tree, "nodes"):
|
|
for internal_node in group_tree.nodes:
|
|
trees.update(get_referenced_trees(internal_node, visited))
|
|
|
|
for inp in getattr(node, "inputs", []):
|
|
if getattr(inp, "is_linked", False):
|
|
for link in getattr(inp, "links", []):
|
|
from_node = getattr(link, "from_node", None)
|
|
if from_node:
|
|
trees.update(get_referenced_trees(from_node, visited))
|
|
|
|
return trees
|
|
|
|
_is_updating = False
|
|
_tree_changed = False
|
|
_changed_trees: set[Any] = set()
|
|
_timer_scheduled = False
|
|
|
|
def _has_real_timers() -> bool:
|
|
timers = getattr(bpy.app, "timers", None)
|
|
if timers is None:
|
|
return False
|
|
if "Mock" in type(timers).__name__:
|
|
return False
|
|
reg = getattr(timers, "register", None)
|
|
if reg is None or "Mock" in type(reg).__name__:
|
|
return False
|
|
return True
|
|
|
|
def cancel_timer() -> None:
|
|
global _timer_scheduled
|
|
if _timer_scheduled and hasattr(bpy.app, "timers") and hasattr(bpy.app.timers, "unregister"):
|
|
try:
|
|
bpy.app.timers.unregister(_timer_callback)
|
|
except Exception:
|
|
pass
|
|
_timer_scheduled = False
|
|
|
|
def _timer_callback() -> float | None:
|
|
global _timer_scheduled, _is_updating
|
|
if _is_updating:
|
|
# Retry in 50ms if currently evaluating
|
|
return 0.05
|
|
_timer_scheduled = False
|
|
run_auto_refresh()
|
|
return None
|
|
|
|
def schedule_auto_refresh(delay: float = 0.02) -> None:
|
|
global _timer_scheduled
|
|
if _timer_scheduled:
|
|
return
|
|
if _has_real_timers():
|
|
try:
|
|
_timer_scheduled = True
|
|
bpy.app.timers.register(_timer_callback, first_interval=delay, persistent=True)
|
|
return
|
|
except Exception:
|
|
_timer_scheduled = False
|
|
# Fallback if timers are not running / available (e.g. tests or non-interactive CLI)
|
|
if not _is_updating:
|
|
run_auto_refresh()
|
|
|
|
def request_auto_refresh(changed_tree: Any = None) -> None:
|
|
global _tree_changed, _changed_trees
|
|
if _is_updating:
|
|
return
|
|
_tree_changed = True
|
|
if changed_tree is not None:
|
|
_changed_trees.add(changed_tree)
|
|
schedule_auto_refresh()
|
|
|
|
def _tag_redraw() -> None:
|
|
try:
|
|
wm = getattr(bpy.context, "window_manager", None)
|
|
if wm:
|
|
for window in getattr(wm, "windows", []):
|
|
screen = getattr(window, "screen", None)
|
|
if screen:
|
|
for area in getattr(screen, "areas", []):
|
|
if getattr(area, "type", "") in ('VIEW_3D', 'NODE_EDITOR'):
|
|
area.tag_redraw()
|
|
except Exception:
|
|
pass
|
|
|
|
def run_auto_refresh(
|
|
updated_object_names: set[str] | None = None,
|
|
updated_collection_names: set[str] | None = None,
|
|
changed_trees: set[Any] | None = None,
|
|
force_refresh: bool = False
|
|
) -> None:
|
|
global _is_updating, _tree_changed, _changed_trees
|
|
|
|
if _is_updating:
|
|
return
|
|
|
|
active_changed_trees = set(_changed_trees)
|
|
if changed_trees:
|
|
active_changed_trees.update(changed_trees)
|
|
_changed_trees.clear()
|
|
|
|
tree_needs_refresh = force_refresh or _tree_changed
|
|
_tree_changed = False
|
|
|
|
has_updates = tree_needs_refresh or bool(updated_object_names) or bool(updated_collection_names)
|
|
if not has_updates:
|
|
return
|
|
|
|
# Collect all SLICETREE_NodeTree datablocks
|
|
trees_to_check: list[Any] = []
|
|
node_groups = getattr(bpy.data, "node_groups", None)
|
|
if node_groups is not None:
|
|
trees_to_check.extend(list(node_groups))
|
|
try:
|
|
space_tree = getattr(getattr(bpy.context, "space_data", None), "edit_tree", None)
|
|
if space_tree and space_tree not in trees_to_check:
|
|
trees_to_check.append(space_tree)
|
|
except Exception:
|
|
pass
|
|
|
|
executed_any = False
|
|
for tree in trees_to_check:
|
|
if getattr(tree, "bl_idname", "") != "SLICETREE_NodeTree":
|
|
continue
|
|
|
|
for node in getattr(tree, "nodes", []):
|
|
if getattr(node, "bl_idname", "") == "SLICENODE_Executor" and getattr(node, "auto_refresh", False):
|
|
should_refresh = False
|
|
|
|
if tree_needs_refresh:
|
|
if not active_changed_trees:
|
|
should_refresh = True
|
|
else:
|
|
owner_tree = getattr(node, "id_data", tree)
|
|
dep_trees = {owner_tree} | get_referenced_trees(node)
|
|
if active_changed_trees & dep_trees:
|
|
should_refresh = True
|
|
|
|
if not should_refresh:
|
|
ref_objs, ref_cols = get_referenced_data(node)
|
|
if updated_object_names:
|
|
ref_names = {o.name for o in ref_objs if o and hasattr(o, "name")}
|
|
if updated_object_names & ref_names:
|
|
should_refresh = True
|
|
if not should_refresh and updated_collection_names:
|
|
ref_col_names = {c.name for c in ref_cols if c and hasattr(c, "name")}
|
|
if updated_collection_names & ref_col_names:
|
|
should_refresh = True
|
|
|
|
if should_refresh:
|
|
_is_updating = True
|
|
try:
|
|
with execution_cache():
|
|
node.evaluate()
|
|
executed_any = True
|
|
except Exception as e:
|
|
print(f"Auto-refresh failed: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
finally:
|
|
_is_updating = False
|
|
|
|
if executed_any:
|
|
_tag_redraw()
|
|
|
|
def tree_update_handler(scene, depsgraph):
|
|
global _tree_changed, _is_updating
|
|
if _is_updating:
|
|
return
|
|
|
|
# Names of objects that have changed
|
|
updated_object_names = {update.id.name for update in depsgraph.updates if _is_btype(update.id, "Object")}
|
|
|
|
# Also check for mesh updates (happens in Edit Mode)
|
|
updated_meshes = {update.id.name for update in depsgraph.updates if _is_btype(update.id, "Mesh")}
|
|
if updated_meshes:
|
|
objects = getattr(bpy.data, "objects", [])
|
|
for obj in objects:
|
|
if getattr(obj, "type", "") == 'MESH' and getattr(getattr(obj, "data", None), "name", None) in updated_meshes:
|
|
updated_object_names.add(obj.name)
|
|
|
|
# Check for collection updates
|
|
updated_collection_names = {update.id.name for update in depsgraph.updates if _is_btype(update.id, "Collection")}
|
|
|
|
# Check if any NodeTree updated in depsgraph
|
|
updated_trees = {update.id for update in depsgraph.updates if _is_btype(update.id, "NodeTree")}
|
|
changed_trees = {t for t in updated_trees if getattr(t, "bl_idname", "") == "SLICETREE_NodeTree"}
|
|
if changed_trees:
|
|
_tree_changed = True
|
|
|
|
if not updated_object_names and not updated_collection_names and not _tree_changed:
|
|
return
|
|
|
|
run_auto_refresh(
|
|
updated_object_names=updated_object_names,
|
|
updated_collection_names=updated_collection_names,
|
|
changed_trees=changed_trees
|
|
)
|
|
|
|
@persistent
|
|
def depsgraph_update_handler(scene, depsgraph):
|
|
global _is_updating, _tree_changed
|
|
if _is_updating: return
|
|
|
|
tree_update_handler(scene, depsgraph)
|
|
|
|
# Sync SLICENODE_Group sockets if interface changed and migrate legacy sockets
|
|
for tree in bpy.data.node_groups:
|
|
if tree.bl_idname == "SLICETREE_NodeTree":
|
|
try:
|
|
from ...infra.blender.list_utils import sync_tree_socket_shapes
|
|
sync_tree_socket_shapes(tree)
|
|
except Exception:
|
|
pass
|
|
for node in tree.nodes:
|
|
if node.bl_idname == "SLICENODE_Group":
|
|
if hasattr(node, "sync_sockets"):
|
|
try:
|
|
node.sync_sockets()
|
|
except Exception:
|
|
pass
|
|
elif hasattr(node, "sync_socket_shapes"):
|
|
try:
|
|
node.sync_socket_shapes()
|
|
except Exception:
|
|
pass
|
|
elif node.bl_idname == "SLICENODE_CollectionObjects" and hasattr(node, "_update_outputs"):
|
|
try:
|
|
node._update_outputs()
|
|
except Exception:
|
|
pass
|
|
if hasattr(node, "_update_multi_groups"):
|
|
try:
|
|
node._update_multi_groups()
|
|
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
|
|
|
|
def update(self):
|
|
global _is_updating
|
|
if _is_updating or getattr(self, "_is_syncing_shapes", False):
|
|
return
|
|
try:
|
|
self._is_syncing_shapes = True
|
|
from ...infra.blender.list_utils import sync_tree_socket_shapes
|
|
sync_tree_socket_shapes(self)
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
self._is_syncing_shapes = False
|
|
request_auto_refresh(self)
|
|
|
|
|
|
@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'}
|
|
|
|
node_name: str = cast(Any, bpy.props.StringProperty())
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
assert context.space_data
|
|
return (context.space_data.type == 'NODE_EDITOR' and
|
|
context.space_data.tree_type == "SLICETREE_NodeTree") # pyright: ignore[reportAttributeAccessIssue]
|
|
|
|
def execute(self, context) -> set['OperatorReturnItems']:
|
|
space = context.space_data
|
|
path = getattr(space, 'path', None)
|
|
if not path:
|
|
return {'CANCELLED'}
|
|
|
|
tree = getattr(space, 'edit_tree', None)
|
|
|
|
target_node = None
|
|
if self.node_name and tree and self.node_name in tree.nodes:
|
|
target_node = tree.nodes[self.node_name]
|
|
else:
|
|
target_node = context.active_node
|
|
|
|
# If we have a target group node, we try to enter it
|
|
if target_node and target_node.bl_idname == "SLICENODE_Group":
|
|
group_tree = getattr(target_node, "slicer_tree", None) or getattr(target_node, "node_tree", None)
|
|
if group_tree:
|
|
path.append(group_tree, node=target_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):
|
|
assert context.space_data
|
|
return (context.space_data.type == 'NODE_EDITOR' and
|
|
context.space_data.edit_tree and # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
|
context.space_data.edit_tree.bl_idname == "SLICETREE_NodeTree" and # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
|
any(n.select for n in context.space_data.edit_tree.nodes)) # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
|
|
|
def execute(self, context):
|
|
import bpy
|
|
from mathutils import Vector
|
|
space = context.space_data
|
|
tree = space.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
|
|
|
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):
|
|
bl_idname = "slicenode.execute"
|
|
bl_label = "Execute"
|
|
bl_description = "Execute"
|
|
|
|
node_name: str = cast(Any, bpy.props.StringProperty())
|
|
|
|
def execute(self, context) -> set['OperatorReturnItems']:
|
|
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
|
node = node_tree.nodes[self.node_name]
|
|
|
|
try:
|
|
with execution_cache():
|
|
from ...infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
|
|
wrapped_op = NodeErrorReportingProxy(self, node)
|
|
res = node.evaluate(operator=wrapped_op)
|
|
if res == 'ok':
|
|
self.report({"INFO"}, "Execution Successful")
|
|
except Exception as e:
|
|
from ...infra.blender.blender_slice_nodes_generic import attach_node_failure_info
|
|
attach_node_failure_info(node, e)
|
|
self.report({"ERROR"}, str(e))
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
return {"FINISHED"}
|
|
|
|
@register_class
|
|
class SLICENODE_OT_DebugDump(Operator):
|
|
bl_idname = "slicenode.debug_dump"
|
|
bl_label = "Debug Dump Node"
|
|
bl_description = "Capture and dump the selected node's inputs and state to disk for review"
|
|
|
|
node_name: str = cast(Any, bpy.props.StringProperty(default=""))
|
|
|
|
def execute(self, context) -> set['OperatorReturnItems']:
|
|
node_tree = getattr(context.space_data, "edit_tree", None) # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
|
if not node_tree:
|
|
self.report({"WARNING"}, "No active node tree")
|
|
return {"CANCELLED"}
|
|
|
|
node = None
|
|
if self.node_name:
|
|
node = node_tree.nodes.get(self.node_name)
|
|
if not node and hasattr(node_tree.nodes, "active"):
|
|
node = node_tree.nodes.active
|
|
|
|
if not node:
|
|
self.report({"WARNING"}, "No node selected to debug")
|
|
return {"CANCELLED"}
|
|
|
|
try:
|
|
from ...infra.debug import dump_node_execution, get_debug_dump_dir
|
|
dump_node_execution(node, "Output", output_val=None, duration_s=0.0)
|
|
dump_dir = get_debug_dump_dir()
|
|
self.report({"INFO"}, f"Dumped '{node.name}' inputs to {dump_dir / 'last_execution.txt'}")
|
|
except Exception as e:
|
|
self.report({"ERROR"}, f"Failed to dump node: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
return {"FINISHED"}
|
|
|
|
@register_class
|
|
class SLICENODE_MT_DataInput(Menu):
|
|
bl_idname = "SLICENODE_MT_DataInput"
|
|
bl_label = "Data & Input"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
assert layout
|
|
layout.operator("node.add_node", text="String").type = "SLICENODE_InputString"
|
|
layout.operator("node.add_node", text="Float").type = "SLICENODE_InputFloat"
|
|
layout.operator("node.add_node", text="Integer").type = "SLICENODE_InputInt"
|
|
layout.operator("node.add_node", text="Rotation").type = "SLICENODE_InputVectorRotation"
|
|
layout.operator("node.add_node", text="Vector").type = "SLICENODE_InputVector"
|
|
layout.operator("node.add_node", text="Layers").type = "SLICENODE_Layers"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
|
layout.operator("node.add_node", text="Collection Objects").type = "SLICENODE_CollectionObjects"
|
|
layout.operator("node.add_node", text="Settings").type = "SLICENODE_InputSettings"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter"
|
|
layout.operator("node.add_node", text="Printer List").type = "SLICENODE_PrinterList"
|
|
layout.operator("node.add_node", text="Printer Intrinsics").type = "SLICENODE_InputIntrinsics"
|
|
|
|
@register_class
|
|
class SLICENODE_MT_Geometry(Menu):
|
|
bl_idname = "SLICENODE_MT_Geometry"
|
|
bl_label = "Geometry"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
assert layout
|
|
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
|
layout.operator("node.add_node", text="Rename Mesh").type = "SLICENODE_RenameMesh"
|
|
layout.operator("node.add_node", text="Transform Geometry").type = "SLICENODE_TransformGeometry"
|
|
layout.operator("node.add_node", text="Get Transforms").type = "SLICENODE_GetTransforms"
|
|
layout.operator("node.add_node", text="Drop Geometry").type = "SLICENODE_DropGeometry"
|
|
layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh"
|
|
layout.operator("node.add_node", text="Separate Mesh by Range").type = "SLICENODE_SeparateMeshByRange"
|
|
layout.operator("node.add_node", text="Arrange Slicing Group").type = "SLICENODE_ArrangeMesh"
|
|
layout.operator("node.add_node", text="Group to List").type = "SLICENODE_GroupToList"
|
|
layout.operator("node.add_node", text="List to Group").type = "SLICENODE_ListToGroup"
|
|
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Layers").type = "SLICENODE_Layers"
|
|
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
|
|
layout.operator("node.add_node", text="Slice Shells (Layers)").type = "SLICENODE_SliceShellsLayers"
|
|
layout.operator("node.add_node", text="Seam Placement").type = "SLICENODE_SeamPlacement"
|
|
layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells"
|
|
layout.operator("node.add_node", text="Buffer Shells Multiple").type = "SLICENODE_BufferShellsMultiple"
|
|
layout.operator("node.add_node", text="Buffer Curve").type = "SLICENODE_BufferCurve"
|
|
layout.operator("node.add_node", text="Offset Curve").type = "SLICENODE_OffsetCurve"
|
|
layout.operator("node.add_node", text="Path Offset").type = "SLICENODE_PathOffset"
|
|
layout.operator("node.add_node", text="Rotate Path").type = "SLICENODE_RotatePath"
|
|
layout.operator("node.add_node", text="Simplify Paths").type = "SLICENODE_SimplifyPaths"
|
|
layout.operator("node.add_node", text="Infill").type = "SLICENODE_Infill"
|
|
layout.operator("node.add_node", text="Overhang Mask").type = "SLICENODE_OverhangMask"
|
|
layout.operator("node.add_node", text="Path Operations").type = "SLICENODE_PathOperations"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath"
|
|
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath"
|
|
|
|
@register_class
|
|
class SLICENODE_MT_PdfExporter(Menu):
|
|
bl_idname = "SLICENODE_MT_PdfExporter"
|
|
bl_label = "PDF Exporter"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
assert layout
|
|
layout.operator("node.add_node", text="Assign PDF Properties").type = "SLICENODE_AssignPdfProperties"
|
|
layout.operator("node.add_node", text="Assign PDF Object Properties").type = "SLICENODE_AssignPropertyToObject"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Mesh to PDF").type = "SLICENODE_MeshToPdf"
|
|
|
|
@register_class
|
|
class SLICENODE_MT_Gcode(Menu):
|
|
bl_idname = "SLICENODE_MT_Gcode"
|
|
bl_label = "Gcode"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
assert layout
|
|
layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode"
|
|
layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock"
|
|
layout.operator("node.add_node", text="Gcode Regex Replace").type = "SLICENODE_GcodeRegexReplace"
|
|
layout.operator("node.add_node", text="Assign Metadata").type = "SLICENODE_AssignMetadata"
|
|
layout.operator("node.add_node", text="Gcode Helper").type = "SLICENODE_GcodeHelper"
|
|
layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode"
|
|
|
|
@register_class
|
|
class SLICENODE_MT_Joiners(Menu):
|
|
bl_idname = "SLICENODE_MT_Joiners"
|
|
bl_label = "Joiners"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
assert layout
|
|
layout.operator("node.add_node", text="Objects to List").type = "SLICENODE_ObjectsToList"
|
|
layout.operator("node.add_node", text="Meshes to List").type = "SLICENODE_MeshesToList"
|
|
layout.operator("node.add_node", text="Paths to List").type = "SLICENODE_PathsToList"
|
|
layout.operator("node.add_node", text="Gcodes to List").type = "SLICENODE_GcodesToList"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Join Mesh Parts").type = "SLICENODE_JoinMesh"
|
|
layout.operator("node.add_node", text="Join Mesh Groups").type = "SLICENODE_JoinMeshGroups"
|
|
layout.operator("node.add_node", text="Path Operations").type = "SLICENODE_PathOperations"
|
|
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode"
|
|
layout.operator("node.add_node", text="Join Settings").type = "SLICENODE_JoinSettings"
|
|
layout.operator("node.add_node", text="Join Execution").type = "SLICENODE_JoinExecution"
|
|
|
|
@register_class
|
|
class SLICENODE_MT_ActionIO(Menu):
|
|
bl_idname = "SLICENODE_MT_ActionIO"
|
|
bl_label = "Action & I/O"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
assert layout
|
|
layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile"
|
|
layout.operator("node.add_node", text="Gcode to Text Block").type = "SLICENODE_GcodeToTextBlock"
|
|
layout.operator("node.add_node", text="Start Print").type = "SLICENODE_StartPrint"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Baserow Submit").type = "SLICENODE_BaserowSubmit"
|
|
layout.operator("node.add_node", text="Airtable Submit").type = "SLICENODE_AirtableSubmit"
|
|
layout.separator()
|
|
layout.operator("node.add_node", text="Gcode to Preview Mesh").type = "SLICENODE_GcodeToMesh"
|
|
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="Add Group Node").type = "SLICENODE_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"
|
|
bl_label = "SliceNodes"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
assert layout
|
|
layout.menu("SLICENODE_MT_DataInput")
|
|
layout.menu("SLICENODE_MT_Geometry")
|
|
layout.menu("SLICENODE_MT_PdfExporter")
|
|
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):
|
|
space = context.space_data
|
|
tree = getattr(space, "edit_tree", None)
|
|
if tree and tree.bl_idname == "SLICETREE_NodeTree":
|
|
self.layout.separator()
|
|
self.layout.menu("SLICENODE_MT_Menu")
|