From 3b6f996724d33b15127080dbcd36cc36af19285b Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Fri, 15 May 2026 17:48:08 +0300 Subject: [PATCH] realtime also when changing parameters --- core/nodeslicer/nodes.py | 58 ++++++++++++++++++++++---------------- core/nodeslicer/sockets.py | 38 +++++++++++++++---------- core/nodeslicer/tree.py | 29 +++++++++++++++---- 3 files changed, 80 insertions(+), 45 deletions(-) diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index 521b470..e9c95cb 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -17,6 +17,7 @@ from .. import slice from ..slice import Pather, slice_at_range, SlicePaths from ...infra.blender_mesh_capture import SlicingObject, capture_mesh_edges, SlicingGroup from ...infra.blender_slice_nodes_generic import SliceNode, resolve_input +from .tree import notify_tree_changed from ...infra._3mf_writer._3mf import prepare_3mf class SliceIntrinsics: @@ -90,6 +91,8 @@ class SLICENODE_PrusaslicerConf(SliceNode): if self.printer_enum != 'NONE': cache.evaluate_compatibility({self.printer_enum}, set(cache.vendors)) + + notify_tree_changed(self, context) def get_printers(self, context): cache = self.get_cache() @@ -148,12 +151,14 @@ class SLICENODE_PrusaslicerConf(SliceNode): filament_enum: EnumProperty( name="Filament", - items=get_filaments + items=get_filaments, + update=notify_tree_changed ) # pyright: ignore[reportInvalidTypeForm] print_enum: EnumProperty( name="Print", - items=get_prints + items=get_prints, + update=notify_tree_changed ) # pyright: ignore[reportInvalidTypeForm] def init(self, context): @@ -186,6 +191,7 @@ class SLICENODE_InputPrinter(SliceNode): def update_label(self, context): self.label = self.printer_label + notify_tree_changed(self, context) printer_label: StringProperty(name="Label", default="Printer", update=update_label) # pyright: ignore[reportInvalidTypeForm] @@ -196,7 +202,8 @@ class SLICENODE_InputPrinter(SliceNode): ('CREALITY', "Creality", "Creality Cloud API"), ('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"), ], - default='PRUSALINK' + default='PRUSALINK', + update=notify_tree_changed ) # pyright: ignore[reportInvalidTypeForm] def init(self, context): @@ -503,14 +510,16 @@ class SLICENODE_Infill(SliceNode): bl_label = "Infill" infill_type: EnumProperty( - name="Infill Type", + name="Type", items=[ - ('GRID', "Grid", "Cross-hatch grid infill"), + ('GRID', "Grid", "Cross-hatch grid"), ('RECTILINEAR', "Rectilinear", "Alternating parallel lines"), ], - default='GRID' + default='GRID', + update=notify_tree_changed ) # pyright: ignore[reportInvalidTypeForm] + def init(self, context): self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Float", "Density").value = 2.0 @@ -562,12 +571,12 @@ class SLICENODE_Infill(SliceNode): class SLICENODE_PathsToGcode(SliceNode): bl_label = "Paths to Gcode" - use_header: BoolProperty(name="Header", default=True) # pyright: ignore[reportInvalidTypeForm] + use_header: BoolProperty(name="Header", default=True, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] def init(self, context): self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Intrinsics") - self.add_output("SLICESOCK_Gcode") + self.add_output("SLICESOCK_GcodeRo") def draw_buttons(self, context, layout): assert layout @@ -593,17 +602,11 @@ class SLICENODE_PathsToGcode(SliceNode): class SLICENODE_GcodeBlock(SliceNode): bl_label = "Gcode Block" - gcode: StringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm] - def init(self, context): - self.add_output("SLICESOCK_Gcode") - - def draw_buttons(self, context, layout): - assert layout - layout.prop(self, "gcode", text="") + self.add_output("SLICESOCK_Gcode", "Gcode") def evaluate_output(self, socket_name: str, **_kwargs) -> str: - return self.gcode + return self.outputs['Gcode'].value @register_class class SLICENODE_GcodeHelper(SliceNode): @@ -644,7 +647,7 @@ class SLICENODE_GcodeHelper(SliceNode): parameters: StringProperty(name="Params", default="") # pyright: ignore[reportInvalidTypeForm] def init(self, context): - self.add_output("SLICESOCK_Gcode") + self.add_output("SLICESOCK_GcodeRo") def draw_buttons(self, context, layout): assert layout @@ -675,7 +678,9 @@ class SliceNodeJoiner(SliceNode): def update(self) -> None: def is_active(input) -> bool: - return input.is_linked or getattr(input, 'value', False) + a: bool = input.is_linked or getattr(input, 'value', False) + b: bool = getattr(self.inputs[-1], 'value', False) + return a or b if len(self.inputs) == 0: self.add_input(self.socket_type) @@ -695,6 +700,7 @@ class SliceNodeJoiner(SliceNode): class SLICENODE_JoinGcode(SliceNodeJoiner): bl_label = "Join Gcode" socket_type = "SLICESOCK_Gcode" + output_socket_type = "SLICESOCK_GcodeRo" def evaluate_output(self, socket_name: str, **_kwargs) -> str: gcode_input: list[str] = [] @@ -841,7 +847,7 @@ class SLICENODE_LoadGcode(SliceNode): def init(self, context): self.add_input("SLICESOCK_FilePath", "Filepath") - self.add_output("SLICESOCK_Gcode") + self.add_output("SLICESOCK_GcodeRo") def evaluate_output(self, socket_name: str, **_kwargs) -> str: path = resolve_input(self.inputs.get("Filepath")) @@ -1053,14 +1059,14 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): bl_label = "PrusaSlicer Slice" button_label = "Slice with PrusaSlicer" - prusaslicer_executable: StringProperty(name="PrusaSlicer Path", subtype='FILE_PATH') # pyright: ignore[reportInvalidTypeForm] + prusaslicer_executable: StringProperty(name="PrusaSlicer Path", subtype='FILE_PATH', update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] gcode_result: StringProperty(name="Gcode Result", default="") # pyright: ignore[reportInvalidTypeForm] def init(self, context): self._init_exec() self.add_input("SLICESOCK_Object", "Object") self.add_input("SLICESOCK_PrusaslicerConf", "Config") - self.add_output("SLICESOCK_Gcode") + self.add_output("SLICESOCK_GcodeRo") def draw_buttons(self, context, layout): super().draw_buttons(context, layout) @@ -1186,10 +1192,13 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): def init(self, context): self._init_exec() - self.add_input("SLICESOCK_Gcode") + self.add_input("SLICESOCK_Gcode", "Gcode") + self.add_input("SLICESOCK_String", "Name") def evaluate(self, operator=None): - gcode: str = resolve_input(self.inputs.get("Gcode")) + gcode: str = self.resolve("Gcode") + mesh_name: str = self.resolve("Name") + if not gcode: return 'empty' @@ -1247,7 +1256,6 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): return 'no segments' # Create or find "Debug" mesh object - mesh_name = "Debug" if mesh_name in bpy.data.objects: obj = bpy.data.objects[mesh_name] mesh = obj.data @@ -1290,7 +1298,7 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner): button_label = "Execute All" socket_type = "SLICESOCK_ExecutionLink" - auto_refresh: bpy.props.BoolProperty(name="Auto-refresh", default=False) # pyright: ignore[reportInvalidTypeForm] + auto_refresh: bpy.props.BoolProperty(name="Auto-refresh", default=False, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] def init(self, context): self._init_exec() diff --git a/core/nodeslicer/sockets.py b/core/nodeslicer/sockets.py index f128cae..2a2fcbd 100644 --- a/core/nodeslicer/sockets.py +++ b/core/nodeslicer/sockets.py @@ -7,6 +7,8 @@ from ...registry import register_class from ...infra.blender_slice_nodes_generic import SliceNodeSocket from ..slice import SlicePaths +from .tree import notify_tree_changed + class LabelNodeSocket(SliceNodeSocket): default = "" def draw(self, context: Any, layout: Any, node: Any, text: str | None): @@ -38,6 +40,7 @@ class SLICESOCK_Object(InputNodeSocket): value: PointerProperty( name="Object", type=bpy.types.Object, + update=notify_tree_changed ) # pyright: ignore[reportInvalidTypeForm] bl_color = (0.9, 0.7, 0.2, 1.0) @@ -45,7 +48,7 @@ class SLICESOCK_Object(InputNodeSocket): class SLICESOCK_Float_Common(InputNodeSocket): bl_label = "Float" - value: FloatProperty(name="Float", default=0) # pyright: ignore[reportInvalidTypeForm] + value: FloatProperty(name="Float", default=0, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] bl_color = (161/255, 161/255, 161/255, 1.0) @register_class @@ -57,7 +60,7 @@ class SLICESOCK_Float(SLICESOCK_Float_Common): ... class SLICESOCK_Int_Common(InputNodeSocket): bl_label = "Integer" - value: IntProperty(name="Int", default=0) # pyright: ignore[reportInvalidTypeForm] + value: IntProperty(name="Int", default=0, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] bl_color = (161/255, 161/255, 161/255, 1.0) @register_class @@ -70,21 +73,21 @@ class SLICESOCK_IntRo(ReadonlySocket, SLICESOCK_Int_Common): ... class SLICESOCK_String(InputNodeSocket): bl_label = "String" - value: StringProperty(name="String", default="") # pyright: ignore[reportInvalidTypeForm] + value: StringProperty(name="String", default="", update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] bl_color = (0.4, 0.7, 1.0, 1.0) @register_class class SLICESOCK_FilePath(InputNodeSocket): bl_label = "Filepath" - value: StringProperty(name="Filepath", default="", subtype='FILE_PATH') # pyright: ignore[reportInvalidTypeForm] + value: StringProperty(name="Filepath", default="", subtype='FILE_PATH', update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] bl_color = (0.4, 0.7, 1.0, 1.0) @register_class class SLICESOCK_VectorRotation_Common(InputNodeSocket): bl_label = "VectorRotation" - value: bpy.props.FloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0), subtype='EULER') # pyright: ignore[reportInvalidTypeForm] + value: bpy.props.FloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0), subtype='EULER', update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] bl_color = (0.39, 0.39, 0.78, 1.0) @register_class class SLICESOCK_VectorRotation(SLICESOCK_VectorRotation_Common): ... @@ -103,12 +106,16 @@ class SLICESOCK_Paths(LabelNodeSocket): default = SlicePaths() bl_color = (214/255, 0, 120/255, 1.0) -@register_class -class SLICESOCK_Gcode(LabelNodeSocket): +class SLICESOCK_Gcode_Common(InputNodeSocket): bl_label = "Gcode" - default = "" + value: StringProperty(name="Gcode", default="", update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] bl_color = (1., 1., 1., 1.) +@register_class +class SLICESOCK_Gcode(SLICESOCK_Gcode_Common): ... +@register_class +class SLICESOCK_GcodeRo(ReadonlySocket, SLICESOCK_Gcode_Common): ... + @register_class class SLICESOCK_Intrinsics(LabelNodeSocket): bl_label = "Intrinsics" @@ -131,12 +138,12 @@ class SLICESOCK_ExecutionLink(LabelNodeSocket): class SLICESOCK_Printer(InputNodeSocket): bl_label = "Printer" - host: StringProperty(name="Host", default="localhost") # pyright: ignore[reportInvalidTypeForm] - port: IntProperty(name="Port", default=80, min=1, max=65535) # pyright: ignore[reportInvalidTypeForm] - prefix: StringProperty(name="Prefix", default="") # pyright: ignore[reportInvalidTypeForm] - label: StringProperty(name="Label", default="Printer") # pyright: ignore[reportInvalidTypeForm] - username: StringProperty(name="Username", default="") # pyright: ignore[reportInvalidTypeForm] - password: StringProperty(name="Password", default="", subtype='PASSWORD') # pyright: ignore[reportInvalidTypeForm] + host: StringProperty(name="Host", default="localhost", update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] + port: IntProperty(name="Port", default=80, min=1, max=65535, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] + prefix: StringProperty(name="Prefix", default="", update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] + label: StringProperty(name="Label", default="Printer", update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] + username: StringProperty(name="Username", default="", update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] + password: StringProperty(name="Password", default="", subtype='PASSWORD', update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm] host_type: EnumProperty( name="Type", @@ -145,7 +152,8 @@ class SLICESOCK_Printer(InputNodeSocket): ('CREALITY', "Creality", "Creality Cloud API"), ('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"), ], - default='PRUSALINK' + default='PRUSALINK', + update=notify_tree_changed ) # pyright: ignore[reportInvalidTypeForm] bl_color = (1.0, 0.45, 0.1, 1.0) # Orange diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index b62d50e..d5771b3 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -39,9 +39,15 @@ def get_referenced_objects(node, visited=None): return objs _is_updating = False +_tree_changed = False + +def notify_tree_changed(self, context): + global _tree_changed + _tree_changed = True + @persistent def depsgraph_update_handler(scene, depsgraph): - global _is_updating + global _is_updating, _tree_changed if _is_updating: return @@ -55,18 +61,31 @@ def depsgraph_update_handler(scene, depsgraph): if obj.type == 'MESH' and obj.data in updated_meshes: updated_object_names.add(obj.name) - if not updated_object_names: + # 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): - referenced = get_referenced_objects(node) - referenced_names = {obj.name for obj in referenced if obj} - if updated_object_names & referenced_names: + # 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: node.evaluate()