subtypes
This commit is contained in:
parent
51ac50fc22
commit
317b1ff45c
8 changed files with 226 additions and 7 deletions
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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="")
|
||||||
|
|
|
||||||
|
|
@ -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`:
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
173
tests/test_path_sockets.py
Normal 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()
|
||||||
Loading…
Add table
Reference in a new issue