nodeslicer/core/nodeslicer/tree.py
2026-07-02 17:58:55 +03:00

611 lines
27 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
import bpy
from bpy.types import NodeTree, Operator, Menu
from bpy.app.handlers import persistent
from ...infra.blender.mixins import update_collection
from ...infra.blender.blender_slice_nodes_generic import node_execution_feedback, resolve_input
from ...registry import register_class
if TYPE_CHECKING:
from bpy.stub_internal.rna_enums import OperatorReturnItems
def get_referenced_objects(node, visited=None):
if visited is None:
visited = set()
ptr = node.as_pointer()
if ptr in visited:
return set()
visited.add(ptr)
objs = set()
# Special case: if this is a group node, we must also check nodes inside the group
if node.bl_idname == "SLICENODE_Group" and getattr(node, "slicer_tree", None):
for internal_node in node.slicer_tree.nodes:
objs.update(get_referenced_objects(internal_node, visited))
# Check all input sockets for objects
for inp in node.inputs:
# If it's an object socket, it might have a value if not linked
if inp.bl_idname in {"SLICESOCK_Object", "SLICESOCK_ObjectRo"}:
if not inp.is_linked and getattr(inp, "value", None):
objs.add(inp.value)
# Always follow links to find more objects upstream
if inp.is_linked:
for link in inp.links:
# Special case: if from_node is CollectionObjects, the object is identified by the socket identifier
if link.from_node.bl_idname == "SLICENODE_CollectionObjects":
obj = bpy.data.objects.get(link.from_socket.identifier)
if obj:
objs.add(obj)
objs.update(get_referenced_objects(link.from_node, visited))
# Also check output sockets of the node itself,
# as some nodes might "hold" an object (like InputObject)
for outp in node.outputs:
if outp.bl_idname in {"SLICESOCK_Object", "SLICESOCK_ObjectRo"}:
if getattr(outp, "value", None):
objs.add(outp.value)
elif node.bl_idname == "SLICENODE_CollectionObjects" and outp.is_linked:
obj = bpy.data.objects.get(outp.identifier)
if obj:
objs.add(obj)
return objs
_is_updating = False
_tree_changed = False
def tree_update_handler(scene, depsgraph):
global _tree_changed, _is_updating
# Names of objects that have changed
updated_object_names = {update.id.name for update in depsgraph.updates if isinstance(update.id, bpy.types.Object)}
# Also check for mesh updates (happens in Edit Mode)
updated_meshes = {update.id.name for update in depsgraph.updates if isinstance(update.id, bpy.types.Mesh)}
if updated_meshes:
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.data.name in updated_meshes:
updated_object_names.add(obj.name)
# Note: custom NodeTree updates often don't appear in depsgraph.updates
# so we rely on _tree_changed flag set by property update callbacks.
if not updated_object_names and not _tree_changed:
return
tree_needs_refresh = _tree_changed
_tree_changed = False # reset for next run
for tree in bpy.data.node_groups:
if tree.bl_idname != "SLICETREE_NodeTree":
continue
for node in tree.nodes:
if node.bl_idname == "SLICENODE_Executor" and getattr(node, "auto_refresh", False):
# Trigger if the tree changed OR if a referenced object changed
should_refresh = tree_needs_refresh
if not should_refresh:
referenced = get_referenced_objects(node)
referenced_names = {obj.name for obj in referenced if obj}
if updated_object_names & referenced_names:
should_refresh = True
if should_refresh:
_is_updating = True
try:
from ...infra.blender.blender_slice_nodes_generic import feedback_disabled
with feedback_disabled():
with node_execution_feedback(node):
node.evaluate()
except Exception as e:
print(f"Auto-refresh failed: {e}")
import traceback
traceback.print_exc()
finally:
_is_updating = False
def object_init_handler(scene, depsgraph):
obj = bpy.context.active_object
if not obj or not hasattr(obj, "nodeslicer"):
return
pg = getattr(obj, 'nodeslicer', None)
if not pg:
return
update_collection(pg.modifiers)
@persistent
def depsgraph_update_handler(scene, depsgraph):
global _is_updating, _tree_changed
if _is_updating: return
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
elif node.bl_idname == "SLICENODE_CollectionObjects" and hasattr(node, "_update_outputs"):
try:
node._update_outputs()
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'}
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm]
@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)
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: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm]
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 node_execution_feedback(node):
res = node.evaluate(operator=self)
if res == 'ok':
self.report({"INFO"}, f"Execution Successful")
except Exception as e:
self.report({"ERROR"}, f"Execution failed: {str(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.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.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf"
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="Transform Geometry").type = "SLICENODE_TransformGeometry"
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="Get Paths").type = "SLICENODE_GetPaths"
layout.separator()
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
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="Get Infill Mask").type = "SLICENODE_GetInfillMask"
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
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_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="Join Mesh Parts").type = "SLICENODE_JoinMesh"
layout.operator("node.add_node", text="Join Mesh Groups").type = "SLICENODE_JoinMeshGroups"
layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths"
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="PrusaSlicer Preview").type = "SLICENODE_PrusaslicerPreview"
layout.operator("node.add_node", text="PrusaSlicer Slice").type = "SLICENODE_PrusaslicerSlice"
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_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")