This commit is contained in:
Nicolas Predella 2026-10-16 12:03:01 +03:00
parent 51ac50fc22
commit 317b1ff45c
8 changed files with 226 additions and 7 deletions

View file

@ -13,7 +13,7 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
self.add_input("NodeSocketString", "Folder") self.add_input("NodeSocketString", "Folder", subtype='DIR_PATH')
self.add_input("NodeSocketString", "Name") self.add_input("NodeSocketString", "Name")
self.add_multi_socket_group("NodeSocketString", name="Gcode") self.add_multi_socket_group("NodeSocketString", name="Gcode")

View file

@ -91,7 +91,7 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
self.add_input("SLICESOCK_DirPath", "Folder") self.add_input("NodeSocketString", "Folder", subtype='DIR_PATH')
self.add_input("SLICESOCK_Settings", "Settings") self.add_input("SLICESOCK_Settings", "Settings")
self.add_input("SLICESOCK_Mesh", "Geometry") self.add_input("SLICESOCK_Mesh", "Geometry")
self.add_input("SLICESOCK_Paths", "Paths") self.add_input("SLICESOCK_Paths", "Paths")

View file

@ -15,6 +15,19 @@ class LabelNodeSocket(SliceNodeSocket):
layout.label(text=text or self.name) layout.label(text=text or self.name)
class InputNodeSocket(SliceNodeSocket): class InputNodeSocket(SliceNodeSocket):
def __init__(self, *args: Any, **kwargs: Any):
try:
super().__init__(*args, **kwargs)
except TypeError:
super().__init__()
if getattr(self, "value", None) is None:
default_val = getattr(self, "default", None)
if default_val is not None:
try:
self.value = default_val
except (AttributeError, TypeError):
pass
def draw(self, context: Any, layout: Any, node: Any, text: str | None): def draw(self, context: Any, layout: Any, node: Any, text: str | None):
assert layout assert layout
if self.is_output or self.is_linked: if self.is_output or self.is_linked:
@ -61,6 +74,20 @@ class SLICESOCK_Intrinsics(LabelNodeSocket):
default = None default = None
bl_color = (0.2, 0.2, 0.2, 1.0) bl_color = (0.2, 0.2, 0.2, 1.0)
@register_class
class SLICESOCK_FilePath(InputNodeSocket):
bl_label = "Filepath"
default = ""
value: str = 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 = "Folder"
default = ""
value: str = NfStringProperty(name="Folder", default="", subtype='DIR_PATH') # pyright: ignore[reportInvalidTypeForm]
bl_color = (0.4, 0.7, 1.0, 1.0)
@register_class @register_class
class SettingItem(bpy.types.PropertyGroup): class SettingItem(bpy.types.PropertyGroup):
field: str = NfStringProperty(name="Field", default="") field: str = NfStringProperty(name="Field", default="")

View file

@ -98,7 +98,8 @@ This file provides a mapping of modules to their primary responsibilities and ke
- `blender_slice_nodes_generic.py`: - `blender_slice_nodes_generic.py`:
- Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`). - Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`).
- Native Blender socket type mappings (`NodeSocketFloat`, `NodeSocketInt`, `NodeSocketVector`, `NodeSocketRotation`, `NodeSocketString`, `NodeSocketObject`, `NodeSocketGeometry`). - Native Blender socket type mappings (`NodeSocketFloat`, `NodeSocketInt`, `NodeSocketVector`, `NodeSocketRotation`, `NodeSocketString`, `NodeSocketObject`, `NodeSocketGeometry`) and custom path picker sockets (`SLICESOCK_DirPath`, `SLICESOCK_FilePath`).
- Socket creation utilities (`add_input`, `add_output`) with support for `subtype='DIR_PATH'` (folder picker) and `subtype='FILE_PATH'` (file picker).
- Core evaluation logic (`resolve_input`, `_resolve_output`). - Core evaluation logic (`resolve_input`, `_resolve_output`).
- `debug.py`: - `debug.py`:

View file

@ -7,7 +7,9 @@
| `NodeSocketObject` | `bpy.types.Object` | Blender RNA Object | Reference to a Blender Object | | `NodeSocketObject` | `bpy.types.Object` | Blender RNA Object | Reference to a Blender Object |
| `NodeSocketFloat` | `float` | Python float | Numeric value | | `NodeSocketFloat` | `float` | Python float | Numeric value |
| `NodeSocketInt` | `int` | Python int | Integer value | | `NodeSocketInt` | `int` | Python int | Integer value |
| `NodeSocketString` | `str` | Python string | Text, G-code, file/folder path value | | `NodeSocketString` | `str` | Python string | Text, G-code value |
| `SLICESOCK_DirPath` | `str` | Python string (`DIR_PATH`) | Directory path value with folder picker |
| `SLICESOCK_FilePath` | `str` | Python string (`FILE_PATH`) | File path value with file picker |
| `NodeSocketVector` | `tuple` | Python tuple (3) | XYZ vector value | | `NodeSocketVector` | `tuple` | Python tuple (3) | XYZ vector value |
| `NodeSocketRotation` | `tuple` | Python tuple (3) | Euler rotation value | | `NodeSocketRotation` | `tuple` | Python tuple (3) | Euler rotation value |
| `NodeSocketGeometry` | `Geometry` | Geometry Data | Generic geometry reference | | `NodeSocketGeometry` | `Geometry` | Geometry Data | Generic geometry reference |

View file

@ -23,6 +23,8 @@ NATIVE_SOCKET_LABELS: dict[str, str] = {
"NodeSocketRotation": "Rotation", "NodeSocketRotation": "Rotation",
"NodeSocketString": "String", "NodeSocketString": "String",
"NodeSocketObject": "Object", "NodeSocketObject": "Object",
"SLICESOCK_DirPath": "Folder",
"SLICESOCK_FilePath": "Filepath",
} }
if hasattr(bpy.types, "NodeSocket") and not hasattr(bpy.types.NodeSocket, "value"): if hasattr(bpy.types, "NodeSocket") and not hasattr(bpy.types.NodeSocket, "value"):
@ -169,7 +171,13 @@ class SliceNode(Node, AutoBlIdName):
is_atomic=is_atomic, is_atomic=is_atomic,
) )
def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = ""): def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = "", subtype: str = ""):
if subtype:
st = subtype.upper()
if st in ("DIR_PATH", "DIRECTORY", "DIR", "FOLDER"):
bl_idname = "SLICESOCK_DirPath"
elif st in ("FILE_PATH", "FILE", "FILENAME"):
bl_idname = "SLICESOCK_FilePath"
bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, 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 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 = self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
@ -186,7 +194,13 @@ class SliceNode(Node, AutoBlIdName):
pass pass
return sock return sock
def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = ""): def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = "", subtype: str = ""):
if subtype:
st = subtype.upper()
if st in ("DIR_PATH", "DIRECTORY", "DIR", "FOLDER"):
bl_idname = "SLICESOCK_DirPath"
elif st in ("FILE_PATH", "FILE", "FILENAME"):
bl_idname = "SLICESOCK_FilePath"
bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, 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 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 = self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name)

View file

@ -9,6 +9,8 @@ LIST_CAPABLE_SOCKET_TYPES = {
"SLICESOCK_Geometry", "SLICESOCK_Geometry",
"SLICESOCK_Intrinsics", "SLICESOCK_Intrinsics",
"SLICESOCK_Settings", "SLICESOCK_Settings",
"SLICESOCK_DirPath",
"SLICESOCK_FilePath",
"NodeSocketObject", "NodeSocketObject",
"NodeSocketString", "NodeSocketString",
"NodeSocketVector", "NodeSocketVector",
@ -237,7 +239,7 @@ def is_socket_vector_type(node: Any, socket_name: str) -> bool:
bl_id = getattr(sock, "bl_idname", "") bl_id = getattr(sock, "bl_idname", "")
if any(term in bl_id for term in ("Vector", "Rotation", "Color", "Euler")): if any(term in bl_id for term in ("Vector", "Rotation", "Color", "Euler")):
return True return True
if bl_id in ("NodeSocketFloat", "NodeSocketInt", "NodeSocketString", "NodeSocketBool", "SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Settings"): if bl_id in ("NodeSocketFloat", "NodeSocketInt", "NodeSocketString", "NodeSocketBool", "SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Settings", "SLICESOCK_DirPath", "SLICESOCK_FilePath"):
return False return False
return False return False

173
tests/test_path_sockets.py Normal file
View file

@ -0,0 +1,173 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.append(str(PROJECT_ROOT.parent))
# Ensure mock bpy is set up
from tests.test_lists import mock_bpy
from NodeSlicer.core.nodeslicer.sockets import SLICESOCK_DirPath, SLICESOCK_FilePath, InputNodeSocket
from NodeSlicer.infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input
from NodeSlicer.core.nodeslicer.nodes.output.gcode_to_file import SLICENODE_GcodeToFile
from NodeSlicer.core.nodeslicer.nodes.output.mesh_to_pdf import SLICENODE_MeshToPdf
from NodeSlicer.infra.blender.list_utils import LIST_CAPABLE_SOCKET_TYPES
class TestPathSockets(unittest.TestCase):
def test_socket_definitions(self):
"""Test that SLICESOCK_DirPath and SLICESOCK_FilePath are properly defined subclasses."""
self.assertTrue(issubclass(SLICESOCK_DirPath, InputNodeSocket))
self.assertTrue(issubclass(SLICESOCK_FilePath, InputNodeSocket))
dir_sock = SLICESOCK_DirPath()
file_sock = SLICESOCK_FilePath()
self.assertEqual(dir_sock.default, "")
self.assertEqual(file_sock.default, "")
self.assertEqual(dir_sock.value, "")
self.assertEqual(file_sock.value, "")
dir_sock.value = "/custom/directory/path"
file_sock.value = "/custom/file/path.gcode"
self.assertEqual(dir_sock.value, "/custom/directory/path")
self.assertEqual(file_sock.value, "/custom/file/path.gcode")
def test_list_capable_socket_types(self):
"""Ensure path sockets are recognized as valid socket types in list utilities."""
self.assertIn("SLICESOCK_DirPath", LIST_CAPABLE_SOCKET_TYPES)
self.assertIn("SLICESOCK_FilePath", LIST_CAPABLE_SOCKET_TYPES)
def test_add_input_subtype_mapping(self):
"""Test that add_input maps subtype='DIR_PATH' and subtype='FILE_PATH' to the appropriate socket."""
class DummyNode(SliceNode):
def __init__(self):
self.inputs = MagicMock()
self.outputs = MagicMock()
node = DummyNode()
node.add_input("NodeSocketString", "Folder", subtype="DIR_PATH")
node.inputs.new.assert_called_with("SLICESOCK_DirPath", "Folder", identifier="Folder")
node.add_input("NodeSocketString", "File", subtype="FILE_PATH")
node.inputs.new.assert_called_with("SLICESOCK_FilePath", "File", identifier="File")
# Case-insensitive and alias tests
node.add_input("NodeSocketString", "Dir", subtype="folder")
node.inputs.new.assert_called_with("SLICESOCK_DirPath", "Dir", identifier="Dir")
node.add_input("NodeSocketString", "Doc", subtype="file")
node.inputs.new.assert_called_with("SLICESOCK_FilePath", "Doc", identifier="Doc")
# Standard string without subtype remains NodeSocketString
node.add_input("NodeSocketString", "Name")
node.inputs.new.assert_called_with("NodeSocketString", "Name", identifier="Name")
# Direct socket type specification
node.add_input("SLICESOCK_DirPath", "TargetDir")
node.inputs.new.assert_called_with("SLICESOCK_DirPath", "TargetDir", identifier="TargetDir")
def test_add_output_subtype_mapping(self):
"""Test that add_output also supports subtype mapping."""
class DummyNode(SliceNode):
def __init__(self):
self.inputs = MagicMock()
self.outputs = MagicMock()
node = DummyNode()
node.add_output("NodeSocketString", "OutFolder", subtype="DIR_PATH")
node.outputs.new.assert_called_with("SLICESOCK_DirPath", "OutFolder", identifier="OutFolder")
node.add_output("NodeSocketString", "OutFile", subtype="FILE_PATH")
node.outputs.new.assert_called_with("SLICESOCK_FilePath", "OutFile", identifier="OutFile")
def test_nodes_use_dir_path_socket(self):
"""Verify that GcodeToFile and MeshToPdf configure the Folder socket with DIR_PATH subtype."""
class MockSocketCollection:
def __init__(self):
self.created = []
def new(self, bl_idname, name, identifier=""):
mock_sock = MagicMock()
mock_sock.bl_idname = bl_idname
mock_sock.name = name
mock_sock.identifier = identifier or name
self.created.append((bl_idname, name, identifier, mock_sock))
return mock_sock
def __iter__(self):
return iter([item[3] for item in self.created])
def get(self, name):
for bl_id, n, ident, s in self.created:
if n == name:
return s
return None
class InspectGcodeToFile(SLICENODE_GcodeToFile):
def __init__(self):
self.inputs = MockSocketCollection()
self.outputs = MockSocketCollection()
self._data = {}
class InspectMeshToPdf(SLICENODE_MeshToPdf):
def __init__(self):
self.inputs = MockSocketCollection()
self.outputs = MockSocketCollection()
self._data = {}
gcode_node = InspectGcodeToFile()
gcode_node.init(None)
folder_in_gcode = next((bl_id for bl_id, name, _, _ in gcode_node.inputs.created if name == "Folder"), None)
self.assertEqual(folder_in_gcode, "SLICESOCK_DirPath")
pdf_node = InspectMeshToPdf()
pdf_node.init(None)
folder_in_pdf = next((bl_id for bl_id, name, _, _ in pdf_node.inputs.created if name == "Folder"), None)
self.assertEqual(folder_in_pdf, "SLICESOCK_DirPath")
def test_resolve_input_with_path_socket(self):
"""Verify resolve_input behavior for unlinked and linked path sockets."""
class DummySocket:
def __init__(self, name, bl_idname, value=""):
self.name = name
self.bl_idname = bl_idname
self.identifier = name
self.value = value
self.default = ""
self.is_linked = False
self.links = []
self.node = MagicMock()
sock = DummySocket("Folder", "SLICESOCK_DirPath", value="")
# Empty value returns default empty string
self.assertEqual(resolve_input(sock), "")
# Set value
sock.value = "/path/to/folder"
self.assertEqual(resolve_input(sock), "/path/to/folder")
# Linked socket follows upstream
class UpstreamNode(SliceNode):
def evaluate_output(self, socket_name, **kwargs):
return "/upstream/linked/path"
upstream = UpstreamNode()
from_socket = MagicMock()
from_socket.node = upstream
from_socket.name = "String Out"
from_socket.identifier = "String Out"
mock_link = MagicMock()
mock_link.from_socket = from_socket
sock.is_linked = True
sock.links = [mock_link]
self.assertEqual(resolve_input(sock), "/upstream/linked/path")
if __name__ == "__main__":
unittest.main()