From 2bfc1ef781e798232b8324bef67519cbe61ee256 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Tue, 25 Aug 2026 13:57:44 +0300 Subject: [PATCH] refactor sockets --- core/nodeslicer/nodes/base.py | 24 +++- core/nodeslicer/nodes/gcode.py | 10 +- core/nodeslicer/nodes/inputs.py | 56 ++++---- core/nodeslicer/nodes/math.py | 2 + core/nodeslicer/nodes/mesh.py | 32 +++-- core/nodeslicer/nodes/path.py | 44 +++--- core/nodeslicer/nodes/preview.py | 4 +- core/nodeslicer/sockets.py | 135 ++++++------------- core/nodeslicer/tree.py | 15 ++- core/slice.py | 2 +- doc/MODULES.md | 1 + doc/NODES.md | 12 +- infra/blender/blender_slice_nodes_generic.py | 64 ++++++++- tests/test_node_groups.py | 3 + 14 files changed, 213 insertions(+), 191 deletions(-) diff --git a/core/nodeslicer/nodes/base.py b/core/nodeslicer/nodes/base.py index cb05f1a..9afed37 100644 --- a/core/nodeslicer/nodes/base.py +++ b/core/nodeslicer/nodes/base.py @@ -34,16 +34,29 @@ class SliceIntrinsics: class InputNode(SliceNode): input_type: str = "" + socket_type: str = "" def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) if "bl_label" not in cls.__dict__: cls.bl_label = cls.input_type def init(self, context) -> None: - self.add_output(f"SLICESOCK_{self.bl_label}") + stype = self.socket_type or (f"NodeSocket{self.bl_label}" if not self.bl_label.startswith("NodeSocket") else self.bl_label) + self.add_output(stype) + + def draw_buttons(self, context, layout): + assert layout + if self.outputs: + sock = self.outputs[0] + if hasattr(sock, "default_value"): + layout.prop(sock, "default_value", text="") + elif hasattr(sock, "value"): + layout.prop(sock, "value", text="") def evaluate_output(self, socket_name: str, **_kwargs): - return self.outputs.get(socket_name).value + out = self.outputs.get(socket_name) or (self.outputs[0] if self.outputs else None) + if out is None: return None + return getattr(out, "value", getattr(out, "default_value", None)) class MathNode(SliceNode): bl_label = "Float Math" @@ -72,12 +85,13 @@ class MathNode(SliceNode): math_type: TypeAlias = float socket_label: str = "Float" + socket_type: str = "NodeSocketFloat" def init(self, context): - self.add_output(f"SLICESOCK_{self.socket_label}Ro", "Value") + self.add_output(self.socket_type, "Value") - self.add_input(f"SLICESOCK_{self.socket_label}", "Value") - self.add_input(f"SLICESOCK_{self.socket_label}", "Value") + self.add_input(self.socket_type, "Value") + self.add_input(self.socket_type, "Value") def draw_buttons(self, context, layout): assert layout diff --git a/core/nodeslicer/nodes/gcode.py b/core/nodeslicer/nodes/gcode.py index fd6e30a..7a287f9 100644 --- a/core/nodeslicer/nodes/gcode.py +++ b/core/nodeslicer/nodes/gcode.py @@ -63,9 +63,9 @@ class SLICENODE_AssignMetadata(SliceNode): bl_label = "Assign Metadata" def init(self, context): - self.add_input("SLICESOCK_String", "Name") - self.add_input("SLICESOCK_String", "Filament Type") - self.add_input("SLICESOCK_String", "Printer Model") + self.add_input("NodeSocketString", "Name") + self.add_input("NodeSocketString", "Filament Type") + self.add_input("NodeSocketString", "Printer Model") self.add_output("SLICESOCK_GcodeRo", "Gcode") def evaluate_output(self, socket_name: str, **_kwargs) -> str: @@ -87,8 +87,8 @@ class SLICENODE_GcodeRegexReplace(SliceNode): def init(self, context): self.add_input("SLICESOCK_Gcode", "Gcode") - self.add_input("SLICESOCK_String", "Find") - self.add_input("SLICESOCK_String", "Replace") + self.add_input("NodeSocketString", "Find") + self.add_input("NodeSocketString", "Replace") self.add_output("SLICESOCK_GcodeRo", "Gcode") def evaluate_output(self, socket_name: str, **_kwargs) -> str: diff --git a/core/nodeslicer/nodes/inputs.py b/core/nodeslicer/nodes/inputs.py index a500469..45407f1 100644 --- a/core/nodeslicer/nodes/inputs.py +++ b/core/nodeslicer/nodes/inputs.py @@ -10,27 +10,33 @@ from .base import InputNode, SliceNode, SliceIntrinsics @register_class class SLICENODE_InputString(InputNode): - input_type = "String" + bl_label = "String" + socket_type = "NodeSocketString" @register_class class SLICENODE_InputFloat(InputNode): - input_type = "Float" + bl_label = "Float" + socket_type = "NodeSocketFloat" @register_class class SLICENODE_InputInt(InputNode): - input_type = "Int" + bl_label = "Integer" + socket_type = "NodeSocketInt" @register_class class SLICENODE_InputVectorRotation(InputNode): - input_type = "VectorRotation" + bl_label = "Vector Rotation" + socket_type = "NodeSocketRotation" @register_class class SLICENODE_InputVector(InputNode): - input_type = "Vector" + bl_label = "Vector" + socket_type = "NodeSocketVector" @register_class class SLICENODE_InputObject(InputNode): - input_type = "Object" + bl_label = "Object" + socket_type = "NodeSocketObject" bl_icon = "OBJECT_DATA" @register_class @@ -70,7 +76,7 @@ class SLICENODE_CollectionObjects(SliceNode): # Add new outputs for name in expected_names: if name not in self.outputs: - self.add_output("SLICESOCK_ObjectRo", name, identifier=name) + self.add_output("NodeSocketObject", name, identifier=name) def evaluate_output(self, socket_name: str, **kwargs) -> Any: if self.collection and socket_name in bpy.data.objects: @@ -82,8 +88,8 @@ class SLICENODE_WipeTower(SliceNode): bl_label = "Wipe Tower" def init(self, context): - self.add_input("SLICESOCK_Float", "X Position").value = 170.0 - self.add_input("SLICESOCK_Float", "Y Position").value = 140.0 + self.add_input("NodeSocketFloat", "X Position").value = 170.0 + self.add_input("NodeSocketFloat", "Y Position").value = 140.0 self.add_output("SLICESOCK_SettingsRo", "Settings") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: @@ -108,9 +114,9 @@ class SLICENODE_OffsetObject(SliceNode): bl_label = "Offset Object" def init(self, context): - self.add_input("SLICESOCK_Float", "X Offset").value = 0.0 - self.add_input("SLICESOCK_Float", "Y Offset").value = 0.0 - self.add_input("SLICESOCK_Float", "Z Offset").value = 0.0 + self.add_input("NodeSocketFloat", "X Offset").value = 0.0 + self.add_input("NodeSocketFloat", "Y Offset").value = 0.0 + self.add_input("NodeSocketFloat", "Z Offset").value = 0.0 self.add_output("SLICESOCK_SettingsRo", "Settings") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: @@ -250,11 +256,11 @@ class SLICENODE_InputPrinter(SliceNode): def init(self, context): self.add_output("SLICESOCK_Printer") - self.add_input("SLICESOCK_String", "Host").value = "localhost" - self.add_input("SLICESOCK_Int", "Port").value = 80 - self.add_input("SLICESOCK_String", "Prefix") - self.add_input("SLICESOCK_String", "Username") - self.add_input("SLICESOCK_String", "Password") + self.add_input("NodeSocketString", "Host").value = "localhost" + self.add_input("NodeSocketInt", "Port").value = 80 + self.add_input("NodeSocketString", "Prefix") + self.add_input("NodeSocketString", "Username") + self.add_input("NodeSocketString", "Password") def draw_buttons(self, context, layout): layout.prop(self, "printer_label") # pyright: ignore[reportOptionalMemberAccess] @@ -280,14 +286,14 @@ class SLICENODE_InputIntrinsics(SliceNode): def init(self, context): self.add_output("SLICESOCK_Intrinsics") - self.add_input("SLICESOCK_Float", "Bed X").value = 250.0 - self.add_input("SLICESOCK_Float", "Bed Y").value = 210.0 - self.add_input("SLICESOCK_Float", "Feedrate").value = 1200.0 - self.add_input("SLICESOCK_Float", "Travel Feedrate").value = 2400.0 - self.add_input("SLICESOCK_Float", "Extrusion Rate").value = 0.035 - self.add_input("SLICESOCK_Float", "Retraction").value = 0.0 - self.add_input("SLICESOCK_Float", "Retraction Feedrate").value = 0.0 - self.add_input("SLICESOCK_Float", "Lift").value = 0.0 + self.add_input("NodeSocketFloat", "Bed X").value = 250.0 + self.add_input("NodeSocketFloat", "Bed Y").value = 210.0 + self.add_input("NodeSocketFloat", "Feedrate").value = 1200.0 + self.add_input("NodeSocketFloat", "Travel Feedrate").value = 2400.0 + self.add_input("NodeSocketFloat", "Extrusion Rate").value = 0.035 + self.add_input("NodeSocketFloat", "Retraction").value = 0.0 + self.add_input("NodeSocketFloat", "Retraction Feedrate").value = 0.0 + self.add_input("NodeSocketFloat", "Lift").value = 0.0 def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics: ret_fr = 0.0 diff --git a/core/nodeslicer/nodes/math.py b/core/nodeslicer/nodes/math.py index 6b53012..1a1dc21 100644 --- a/core/nodeslicer/nodes/math.py +++ b/core/nodeslicer/nodes/math.py @@ -7,9 +7,11 @@ class SLICENODE_FloatMath(MathNode): bl_label = "Math" math_type = float socket_label: str = "Float" + socket_type: str = "NodeSocketFloat" @register_class class SLICENODE_IntMath(MathNode): bl_label = "Integer Math" math_type = int socket_label: str = "Int" + socket_type: str = "NodeSocketInt" diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index d85a5f4..2ae35d7 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -14,7 +14,7 @@ class SLICENODE_GetMesh(SliceNode): include_children: NfBoolProperty(name="Include Children", default=True) # pyright: ignore[reportInvalidTypeForm] def init(self, context): - self.add_input("SLICESOCK_Object") + self.add_input("NodeSocketObject") self.add_output("SLICESOCK_Mesh") def draw_buttons(self, context, layout): @@ -89,8 +89,8 @@ class SLICENODE_SeparateMesh(SliceNode): def init(self, context): self.add_input("SLICESOCK_Mesh") - self.add_input("SLICESOCK_String", "Attribute") - self.add_input("SLICESOCK_Float", "Value") + self.add_input("NodeSocketString", "Attribute") + self.add_input("NodeSocketFloat", "Value") self.add_output("SLICESOCK_Mesh", "Selection") self.add_output("SLICESOCK_Mesh", "Inverted") @@ -172,7 +172,7 @@ class SLICENODE_SeparateMeshByRange(SliceNode): def init(self, context): self.add_input("SLICESOCK_Mesh") - self.add_input("SLICESOCK_String", "Attribute") + self.add_input("NodeSocketString", "Attribute") self._update_outputs() def update(self): @@ -277,9 +277,9 @@ class SLICENODE_ArrangeMesh(SliceNode): def init(self, context): self.add_input("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST') - self.add_input("SLICESOCK_Float", "Bed X").value = 250.0 - self.add_input("SLICESOCK_Float", "Bed Y").value = 250.0 - self.add_input("SLICESOCK_Float", "Spacing").value = 2.0 + self.add_input("NodeSocketFloat", "Bed X").value = 250.0 + self.add_input("NodeSocketFloat", "Bed Y").value = 250.0 + self.add_input("NodeSocketFloat", "Spacing").value = 2.0 self.add_output("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST') def draw_buttons(self, context, layout): @@ -584,18 +584,16 @@ class SLICENODE_TransformGeometry(SliceNode): def init(self, context): self.add_input("SLICESOCK_Geometry", "Geometry") - self.add_input("SLICESOCK_Vector", "Translation") - self.add_input("SLICESOCK_VectorRotation", "Rotation") - self.add_input("SLICESOCK_Vector", "Scale").value = (1.0, 1.0, 1.0) + self.add_input("NodeSocketVector", "Translation") + self.add_input("NodeSocketRotation", "Rotation") + self.add_input("NodeSocketVector", "Scale").value = (1.0, 1.0, 1.0) self.add_output("SLICESOCK_Geometry", "Geometry") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: - if socket_name not in ("Geometry", "Mesh"): + if socket_name not in ("Geometry"): return None geom_data: Any = self.resolve("Geometry") - if geom_data is None: - geom_data = self.resolve("Mesh") if geom_data is None: return None @@ -623,9 +621,9 @@ class SLICENODE_DropGeometry(SliceNode): def init(self, context): self.add_input("SLICESOCK_Mesh", "Mesh") - self.add_input("SLICESOCK_Float", "Z") + self.add_input("NodeSocketFloat", "Z") self.add_output("SLICESOCK_Mesh", "Mesh") - self.add_output("SLICESOCK_VectorRo", "Translation") + self.add_output("NodeSocketVector", "Translation") def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | SlicingGroup | tuple[float, float, float] | None: if socket_name == "Translation": @@ -754,7 +752,7 @@ class SLICENODE_RenameMesh(SliceNode): def init(self, context): self.add_input("SLICESOCK_Mesh", "Geometry") - self.add_input("SLICESOCK_String", "Name") + self.add_input("NodeSocketString", "Name") self.add_output("SLICESOCK_Mesh", "Geometry") def draw_buttons(self, context, layout): @@ -821,7 +819,7 @@ class SLICENODE_AssignPdfProperties(SliceNode): def init(self, context): self.add_input("SLICESOCK_Mesh", "Geometry") - self.add_input("SLICESOCK_Float", "Line Width") + self.add_input("NodeSocketFloat", "Line Width") self.add_output("SLICESOCK_Mesh", "Geometry") def draw_buttons(self, context, layout): diff --git a/core/nodeslicer/nodes/path.py b/core/nodeslicer/nodes/path.py index 0611ebb..c80b5b8 100644 --- a/core/nodeslicer/nodes/path.py +++ b/core/nodeslicer/nodes/path.py @@ -11,9 +11,9 @@ class SLICENODE_GetPaths(SliceNode): bl_label = "Get Paths" def init(self, context): - self.add_input("SLICESOCK_Object") - self.add_input("SLICESOCK_String", "Extrusion Multiplier Attribute").value = 'e_mult' - self.add_input("SLICESOCK_String", "Feedrate Attribute").value = 'f_rate' + self.add_input("NodeSocketObject") + self.add_input("NodeSocketString", "Extrusion Multiplier Attribute").value = 'e_mult' + self.add_input("NodeSocketString", "Feedrate Attribute").value = 'f_rate' self.add_input("SLICESOCK_Settings", "Extra Attributes") self.add_output("SLICESOCK_Paths") @@ -44,9 +44,9 @@ class SLICENODE_SliceShells(SliceNode): def init(self, context): self.add_input("SLICESOCK_Mesh") - self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2 - self.add_input("SLICESOCK_Float", "First Layer Height").value = 0.2 - self.add_input("SLICESOCK_VectorRotation", "Rotation") + self.add_input("NodeSocketFloat", "Layer Height").value = 0.2 + self.add_input("NodeSocketFloat", "First Layer Height").value = 0.2 + self.add_input("NodeSocketRotation", "Rotation") self.add_output("SLICESOCK_Paths") def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths: @@ -211,8 +211,8 @@ class SLICENODE_BufferCurve(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths") - self.add_input("SLICESOCK_Float", "Distance").value = 0.0 - self.add_input("SLICESOCK_VectorRotation", "Rotation") + self.add_input("NodeSocketFloat", "Distance").value = 0.0 + self.add_input("NodeSocketRotation", "Rotation") self.add_output("SLICESOCK_Paths") def draw_buttons(self, context, layout): @@ -254,8 +254,8 @@ class SLICENODE_OffsetCurve(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths") - self.add_input("SLICESOCK_Float", "Distance").value = 0.0 - self.add_input("SLICESOCK_VectorRotation", "Rotation") + self.add_input("NodeSocketFloat", "Distance").value = 0.0 + self.add_input("NodeSocketRotation", "Rotation") self.add_output("SLICESOCK_Paths") def draw_buttons(self, context, layout): @@ -283,7 +283,7 @@ class SLICENODE_PathOffset(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths", "Paths") - self.add_input("SLICESOCK_Vector", "Offset") + self.add_input("NodeSocketVector", "Offset") self.add_output("SLICESOCK_Paths", "Paths") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: @@ -300,8 +300,8 @@ class SLICENODE_RotatePath(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths", "Paths") - self.add_input("SLICESOCK_Float", "Angle").value = 0.0 - self.add_input("SLICESOCK_Vector", "Center") + self.add_input("NodeSocketFloat", "Angle").value = 0.0 + self.add_input("NodeSocketVector", "Center") self.add_output("SLICESOCK_Paths", "Paths") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: @@ -330,10 +330,10 @@ class SLICENODE_BufferShellsMultiple(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths") - self.add_input("SLICESOCK_Float", "Distance").value = 0 - self.add_input("SLICESOCK_Float", "Width Step").value = 0.4 - self.add_input("SLICESOCK_Int", "Count").value = 2 - self.add_input("SLICESOCK_VectorRotation", "Rotation") + self.add_input("NodeSocketFloat", "Distance").value = 0 + self.add_input("NodeSocketFloat", "Width Step").value = 0.4 + self.add_input("NodeSocketInt", "Count").value = 2 + self.add_input("NodeSocketRotation", "Rotation") self.add_output("SLICESOCK_Paths") def draw_buttons(self, context, layout): @@ -370,9 +370,9 @@ class SLICENODE_Infill(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths") - self.add_input("SLICESOCK_Float", "Density").value = 2.0 - self.add_input("SLICESOCK_Float", "Infill Rotation").value = 0.0 - self.add_input("SLICESOCK_VectorRotation", "Layer Rotation") + self.add_input("NodeSocketFloat", "Density").value = 2.0 + self.add_input("NodeSocketFloat", "Infill Rotation").value = 0.0 + self.add_input("NodeSocketRotation", "Layer Rotation") self.add_output("SLICESOCK_Paths") def draw_buttons(self, context, layout): @@ -422,7 +422,7 @@ class SLICENODE_OverhangMask(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths") - self.add_input("SLICESOCK_Float", "Tolerance").value = 0.05 + self.add_input("NodeSocketFloat", "Tolerance").value = 0.05 self.add_output("SLICESOCK_Paths") def draw_buttons(self, context, layout): @@ -534,7 +534,7 @@ class SLICENODE_SimplifyPaths(SliceNode): def init(self, context): self.add_input("SLICESOCK_Paths") - self.add_input("SLICESOCK_Float", "Tolerance").value = 0.1 + self.add_input("NodeSocketFloat", "Tolerance").value = 0.1 self.add_output("SLICESOCK_Paths") def draw_buttons(self, context, layout): diff --git a/core/nodeslicer/nodes/preview.py b/core/nodeslicer/nodes/preview.py index 968c0ba..fe2ac59 100644 --- a/core/nodeslicer/nodes/preview.py +++ b/core/nodeslicer/nodes/preview.py @@ -15,7 +15,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): def init(self, context): self._init_exec() - self.add_input("SLICESOCK_String", "Name") + self.add_input("NodeSocketString", "Name") self.add_multi_socket_group("SLICESOCK_Gcode", name="Gcode") def draw_buttons(self, context, layout): @@ -242,7 +242,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): for i, gcode in enumerate(gcodes): if total_parts > 1: - mesh_name = f"{base_mesh_name}{i+1:0{num_digits}d}" + mesh_name = f"{base_mesh_name}{i:0{num_digits}d}" else: mesh_name = base_mesh_name diff --git a/core/nodeslicer/sockets.py b/core/nodeslicer/sockets.py index f86f21b..7f4ec2b 100644 --- a/core/nodeslicer/sockets.py +++ b/core/nodeslicer/sockets.py @@ -44,101 +44,18 @@ class ReadonlySocket(): def draw(self, context: Any, layout: Any, node: Any, text: str | None): return self._draw_as_ro(context, layout, node, text) # pyright: ignore[reportAttributeAccessIssue] -class SLICESOCK_ObjectCommon(InputNodeSocket): - bl_label = "Object" - - value: NfPointerProperty( - name="Object", - type=bpy.types.Object, - update=notify_tree_changed - ) # pyright: ignore[reportInvalidTypeForm] - bl_color = (0.9, 0.7, 0.2, 1.0) - -@register_class -class SLICESOCK_ObjectRo(ReadonlySocket, SLICESOCK_ObjectCommon): ... -@register_class -class SLICESOCK_Object(SLICESOCK_ObjectCommon): ... - -class SLICESOCK_Float_Common(InputNodeSocket): - bl_label = "Float" - - value: NfFloatProperty(name="Float", default=0) # pyright: ignore[reportInvalidTypeForm] - bl_color = (161/255, 161/255, 161/255, 1.0) - -@register_class -class SLICESOCK_FloatRo(ReadonlySocket, SLICESOCK_Float_Common): ... -@register_class -class SLICESOCK_Float(SLICESOCK_Float_Common): ... - - -class SLICESOCK_Int_Common(InputNodeSocket): - bl_label = "Integer" - - value: NfIntProperty(name="Int", default=0) # pyright: ignore[reportInvalidTypeForm] - bl_color = (161/255, 161/255, 161/255, 1.0) - -@register_class -class SLICESOCK_Int(SLICESOCK_Int_Common): ... -@register_class -class SLICESOCK_IntRo(ReadonlySocket, SLICESOCK_Int_Common): ... - - -@register_class -class SLICESOCK_String(InputNodeSocket): - bl_label = "String" - - value: NfStringProperty(name="String", default="") # pyright: ignore[reportInvalidTypeForm] - bl_color = (0.4, 0.7, 1.0, 1.0) - -@register_class -class SLICESOCK_FilePath(InputNodeSocket): - bl_label = "Filepath" - - value: NfStringProperty(name="Filepath", default="", subtype='FILE_PATH') # pyright: ignore[reportInvalidTypeForm] - bl_color = (0.4, 0.7, 1.0, 1.0) - -@register_class -class SLICESOCK_DirPath(InputNodeSocket): - bl_label = "Filepath" - - value: NfStringProperty(name="Folder", default="", subtype='DIR_PATH') # pyright: ignore[reportInvalidTypeForm] - bl_color = (0.4, 0.7, 1.0, 1.0) - -@register_class -class SLICESOCK_VectorRotation_Common(InputNodeSocket): - bl_label = "VectorRotation" - - value: NfFloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0), subtype='EULER') # pyright: ignore[reportInvalidTypeForm] - bl_color = (166/255, 99/255, 199/255, 1.0) - -@register_class -class SLICESOCK_VectorRotation(SLICESOCK_VectorRotation_Common): ... -@register_class -class SLICESOCK_VectorRotationRo(ReadonlySocket, SLICESOCK_VectorRotation_Common): ... - -@register_class -class SLICESOCK_Vector_Common(InputNodeSocket): - bl_label = "Vector" - - value: NfFloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0)) # pyright: ignore[reportInvalidTypeForm] - bl_color = (0.39, 0.39, 0.78, 1.0) - -@register_class -class SLICESOCK_Vector(SLICESOCK_Vector_Common): ... -@register_class -class SLICESOCK_VectorRo(ReadonlySocket, SLICESOCK_Vector_Common): ... - -@register_class -class SLICESOCK_Mesh(LabelNodeSocket): - bl_label = "Mesh" - default = None - bl_color = (0, 214/255, 163/255, 1.0) - -@register_class -class SLICESOCK_Paths(LabelNodeSocket): - bl_label = "Paths" - default = SlicePaths() - bl_color = (214/255, 0, 120/255, 1.0) +# Native Blender socket type aliases +SLICESOCK_Object = "NodeSocketObject" +SLICESOCK_ObjectRo = "NodeSocketObject" +SLICESOCK_Float = "NodeSocketFloat" +SLICESOCK_FloatRo = "NodeSocketFloat" +SLICESOCK_Int = "NodeSocketInt" +SLICESOCK_IntRo = "NodeSocketInt" +SLICESOCK_String = "NodeSocketString" +SLICESOCK_VectorRotation = "NodeSocketRotation" +SLICESOCK_VectorRotationRo = "NodeSocketRotation" +SLICESOCK_Vector = "NodeSocketVector" +SLICESOCK_VectorRo = "NodeSocketVector" @register_class class SLICESOCK_Geometry(LabelNodeSocket): @@ -159,7 +76,7 @@ class SLICESOCK_Geometry(LabelNodeSocket): pass return getattr(from_sock, "bl_color", self.bl_color) - if self.is_output: + if getattr(self, "is_output", False): if node and hasattr(node, "inputs"): sock_name = getattr(self, "name", "") matching_in = (node.inputs.get(sock_name) if sock_name else None) or node.inputs.get("Geometry") @@ -169,6 +86,32 @@ class SLICESOCK_Geometry(LabelNodeSocket): else: return _get_upstream_color(self) +@register_class +class SLICESOCK_FilePath(InputNodeSocket): + bl_label = "Filepath" + + value: NfStringProperty(name="Filepath", default="", subtype='FILE_PATH') # pyright: ignore[reportInvalidTypeForm] + bl_color = (0.4, 0.7, 1.0, 1.0) + +@register_class +class SLICESOCK_DirPath(InputNodeSocket): + bl_label = "Filepath" + + value: NfStringProperty(name="Folder", default="", subtype='DIR_PATH') # pyright: ignore[reportInvalidTypeForm] + bl_color = (0.4, 0.7, 1.0, 1.0) + +@register_class +class SLICESOCK_Mesh(LabelNodeSocket): + bl_label = "Mesh" + default = None + bl_color = (0, 214/255, 163/255, 1.0) + +@register_class +class SLICESOCK_Paths(LabelNodeSocket): + bl_label = "Paths" + default = SlicePaths() + bl_color = (214/255, 0, 120/255, 1.0) + class SLICESOCK_Gcode_Common(InputNodeSocket): diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index 17ab704..dfeb937 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -30,9 +30,11 @@ def get_referenced_objects(node, visited=None): # 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) + if inp.bl_idname == "NodeSocketObject": + if not inp.is_linked and getattr(inp, "value", getattr(inp, "default_value", None)): + val = getattr(inp, "value", getattr(inp, "default_value", None)) + if isinstance(val, bpy.types.Object): + objs.add(val) # Always follow links to find more objects upstream if inp.is_linked: @@ -48,9 +50,10 @@ def get_referenced_objects(node, visited=None): # 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) + if outp.bl_idname == "NodeSocketObject": + val = getattr(outp, "value", getattr(outp, "default_value", None)) + if isinstance(val, bpy.types.Object): + objs.add(val) elif node.bl_idname == "SLICENODE_CollectionObjects" and outp.is_linked: obj = bpy.data.objects.get(outp.identifier) if obj: diff --git a/core/slice.py b/core/slice.py index 26d9f3f..d355667 100644 --- a/core/slice.py +++ b/core/slice.py @@ -752,7 +752,7 @@ def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]: open_paths = [] for p in linked: path_pts = p[:, :2] - if len(path_pts) > 2 and np.allclose(path_pts[0], path_pts[-1]): + if len(path_pts) > 3 and np.allclose(path_pts[0], path_pts[-1]): closed_paths.append(p) else: open_paths.append(p) diff --git a/doc/MODULES.md b/doc/MODULES.md index 2c0cc2a..2bdbe4d 100644 --- a/doc/MODULES.md +++ b/doc/MODULES.md @@ -57,4 +57,5 @@ This file provides a mapping of modules to their primary responsibilities and ke - `blender_slice_nodes_generic.py`: - Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`). + - Native Blender socket type mappings (`NodeSocketFloat`, `NodeSocketInt`, `NodeSocketVector`, `NodeSocketRotation`, `NodeSocketString`, `NodeSocketObject`, `NodeSocketGeometry`). - Core evaluation logic (`resolve_input`). diff --git a/doc/NODES.md b/doc/NODES.md index 49bdc46..c79e988 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -4,11 +4,13 @@ | Socket ID | Type | Underlying Data Structure | Purpose | |-----------|------|---------------------------|---------| -| `SLICESOCK_Object` | `bpy.types.Object` | Blender RNA Object | Reference to a Blender Object | -| `SLICESOCK_Float` | `float` | Python float | Numeric value | -| `SLICESOCK_Int` | `int` | Python int | Integer value | -| `SLICESOCK_String` | `str` | Python string | Text/Path value | -| `SLICESOCK_Vector` | `tuple` | Python tuple (3) | XYZ or Euler rotation value | +| `NodeSocketObject` | `bpy.types.Object` | Blender RNA Object | Reference to a Blender Object | +| `NodeSocketFloat` | `float` | Python float | Numeric value | +| `NodeSocketInt` | `int` | Python int | Integer value | +| `NodeSocketString` | `str` | Python string | Text value | +| `NodeSocketVector` | `tuple` | Python tuple (3) | XYZ vector value | +| `NodeSocketRotation` | `tuple` | Python tuple (3) | Euler rotation value | +| `NodeSocketGeometry` | `Geometry` | Geometry Data | Generic geometry reference | | `SLICESOCK_Mesh` | `SlicingObject` | NumPy NDArray (Triangles) | Internal mesh data (Transformed/Scaled) | | `SLICESOCK_Paths` | `SlicePaths` (list) | List of paths (N, 3) + groups | Generic toolpaths or slice boundaries | | `SLICESOCK_Gcode` | `str` | Multi-line string | Generated G-code text | diff --git a/infra/blender/blender_slice_nodes_generic.py b/infra/blender/blender_slice_nodes_generic.py index 5e6dbf5..eae9c1c 100644 --- a/infra/blender/blender_slice_nodes_generic.py +++ b/infra/blender/blender_slice_nodes_generic.py @@ -11,9 +11,47 @@ class NodeException(Exception): ... _EvalCache = dict[tuple[int, str, str], Any] +NATIVE_SOCKET_MAP: dict[str, str] = {} + +NATIVE_SOCKET_LABELS: dict[str, str] = { + "NodeSocketFloat": "Float", + "NodeSocketInt": "Integer", + "NodeSocketVector": "Vector", + "NodeSocketRotation": "Rotation", + "NodeSocketString": "String", + "NodeSocketObject": "Object", +} + +if hasattr(bpy.types, "NodeSocket") and not hasattr(bpy.types.NodeSocket, "value"): + def _get_socket_value(self): + try: + return self.default_value + except (AttributeError, TypeError): + pass + return getattr(self, "_value", getattr(self, "default", None)) + + def _set_socket_value(self, val): + try: + self.default_value = val + return + except (AttributeError, TypeError): + pass + self._value = val + + try: + bpy.types.NodeSocket.value = property(_get_socket_value, _set_socket_value) + except Exception: + pass + class AutoBlIdName: def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) + if kwargs: + try: + super().__init_subclass__(**kwargs) + except TypeError: + super().__init_subclass__() + else: + super().__init_subclass__() if "bl_idname" not in cls.__dict__: cls.bl_idname = cls.__name__ @@ -40,7 +78,8 @@ class SliceNode(Node, AutoBlIdName): return resolve_input(inputs.get(fieldname), cache=cache, visited=visited) def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = ""): - eval_name = name or get_label_by_name(bl_idname) + bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, bl_idname) + eval_name = name or NATIVE_SOCKET_LABELS.get(bl_idname) or get_label_by_name(bl_idname) or bl_idname sock = self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name) sock.hide = hide if display_shape: @@ -48,7 +87,8 @@ class SliceNode(Node, AutoBlIdName): return sock def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = ""): - eval_name = name or get_label_by_name(bl_idname) + bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, bl_idname) + eval_name = name or NATIVE_SOCKET_LABELS.get(bl_idname) or get_label_by_name(bl_idname) or bl_idname sock = self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name) sock.hide = hide if display_shape: @@ -268,6 +308,7 @@ class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName): # Add or update sockets sock_indexes = {s.identifier: i for i, s in enumerate(collection)} for ident, name, stype in expected: + stype = NATIVE_SOCKET_MAP.get(stype, stype) if ident not in sock_indexes: try: # Explicitly pass identifier to keep it perfectly synced @@ -384,10 +425,19 @@ def resolve_input( visited = getattr(socket.node, "_current_visited", None) if not socket.is_linked: - val = getattr(socket, "value", None) - if val is not None: - return val - return socket.default + try: + val = getattr(socket, "default_value", None) + if val is not None: return val + except Exception: + pass + + try: + val = getattr(socket, "value", None) + if val is not None: return val + except Exception: + pass + + return getattr(socket, "default", None) assert socket.is_linked assert socket.links diff --git a/tests/test_node_groups.py b/tests/test_node_groups.py index 9171acf..db72f1e 100644 --- a/tests/test_node_groups.py +++ b/tests/test_node_groups.py @@ -63,6 +63,9 @@ sys.modules['bpy'] = mock_bpy sys.modules['bpy.types'] = mock_types sys.modules['bpy.props'] = MagicMock() sys.modules['bpy.utils'] = MagicMock() +sys.modules['bpy.utils.previews'] = MagicMock() +sys.modules['bpy.app'] = MagicMock() +sys.modules['bpy.app.handlers'] = MagicMock() from NodeSlicer.infra.blender.blender_slice_nodes_generic import ( _resolve_group_output,