From b67a8d9755f0aed62060114f29706f444c14a2d3 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Tue, 25 Aug 2026 14:47:46 +0300 Subject: [PATCH] up --- core/nodeslicer/nodes/gcode.py | 7 +- core/nodeslicer/nodes/mesh.py | 4 +- core/nodeslicer/sockets.py | 9 +- core/nodeslicer/tree.py | 5 + infra/blender/blender_slice_nodes_generic.py | 47 ++++++- tests/test_multi_groups.py | 140 +++++++++++++++++++ 6 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 tests/test_multi_groups.py diff --git a/core/nodeslicer/nodes/gcode.py b/core/nodeslicer/nodes/gcode.py index 83a3298..2d0a343 100644 --- a/core/nodeslicer/nodes/gcode.py +++ b/core/nodeslicer/nodes/gcode.py @@ -184,8 +184,11 @@ class SLICENODE_JoinGcode(SliceNodeJoiner): from ....infra.blender.blender_slice_nodes_generic import resolve_input gcode_input: list[str] = [] for input in self.inputs: - gc: str = str(resolve_input(input)) - gcode_input += [gc] or [""] + val = resolve_input(input) + if val is not None: + gc = str(val) + if gc: + gcode_input.append(gc) return "\n".join(gcode_input) @register_class diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index cfc1320..2ae35d7 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -590,12 +590,10 @@ class SLICENODE_TransformGeometry(SliceNode): 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 diff --git a/core/nodeslicer/sockets.py b/core/nodeslicer/sockets.py index fda0b47..d72fbd7 100644 --- a/core/nodeslicer/sockets.py +++ b/core/nodeslicer/sockets.py @@ -19,14 +19,7 @@ class LabelNodeSocket(SliceNodeSocket): class InputNodeSocket(SliceNodeSocket): def draw(self, context: Any, layout: Any, node: Any, text: str | None): assert layout - if node and isinstance(node, bpy.types.NodeCustomGroup): - if self.is_output or self.is_linked: - layout.label(text=text or self.name) - else: - self._draw_prop(text, layout) - return - - if not self.is_output and self.is_linked: + if self.is_output or self.is_linked: layout.label(text=text or self.name) else: self._draw_prop(text, layout) diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index db3077b..1298377 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -150,6 +150,11 @@ def depsgraph_update_handler(scene, depsgraph): node._update_outputs() except Exception: pass + if hasattr(node, "_update_multi_groups"): + try: + node._update_multi_groups() + except Exception: + pass @register_class diff --git a/infra/blender/blender_slice_nodes_generic.py b/infra/blender/blender_slice_nodes_generic.py index eae9c1c..13db522 100644 --- a/infra/blender/blender_slice_nodes_generic.py +++ b/infra/blender/blender_slice_nodes_generic.py @@ -64,6 +64,39 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName): from ...registry import get_label_by_name, register_class class SliceNode(Node, AutoBlIdName): + def get(self, key: str, default: Any = None) -> Any: + try: + return super().get(key, default) # type: ignore + except (AttributeError, TypeError): + pass + if hasattr(self, "_data") and isinstance(self._data, dict): + return self._data.get(key, default) + return getattr(self, key, default) + + def __setitem__(self, key: str, value: Any) -> None: + try: + super().__setitem__(key, value) # type: ignore + except (AttributeError, TypeError): + if not hasattr(self, "_data") or self._data is None: + self._data = {} + self._data[key] = value + + def __getitem__(self, key: str) -> Any: + try: + return super().__getitem__(key) # type: ignore + except (AttributeError, TypeError): + if hasattr(self, "_data") and isinstance(self._data, dict): + return self._data[key] + raise KeyError(key) + + def __contains__(self, key: str) -> bool: + try: + return super().__contains__(key) # type: ignore + except (AttributeError, TypeError): + if hasattr(self, "_data") and isinstance(self._data, dict): + return key in self._data + return False + def update(self) -> None: self._update_multi_groups() @@ -128,7 +161,13 @@ class SliceNode(Node, AutoBlIdName): def get_ins(): return [i for i in self.inputs if i.identifier.startswith(f"{group_id}_ms_in_")] def get_outs(): return [o for o in self.outputs if o.identifier.startswith(f"{group_id}_ms_out_")] - def is_active(s): return s.is_linked or bool(getattr(s, 'value', False)) + def is_active(s): + if getattr(s, 'is_linked', False): + return True + val = getattr(s, 'value', None) + if val is None or val == "": + val = getattr(s, 'default_value', None) + return bool(val) ins = get_ins() @@ -426,13 +465,13 @@ def resolve_input( if not socket.is_linked: try: - val = getattr(socket, "default_value", None) - if val is not None: return val + val = getattr(socket, "value", None) + if val is not None and val != "": return val except Exception: pass try: - val = getattr(socket, "value", None) + val = getattr(socket, "default_value", None) if val is not None: return val except Exception: pass diff --git a/tests/test_multi_groups.py b/tests/test_multi_groups.py new file mode 100644 index 0000000..0e64181 --- /dev/null +++ b/tests/test_multi_groups.py @@ -0,0 +1,140 @@ +import unittest +from unittest.mock import MagicMock +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.append(str(PROJECT_ROOT.parent)) + +class DummyBase: + def _get_data_dict(self): + if not hasattr(self, "_data") or self._data is None: + self._data = {} + return self._data + + def __getitem__(self, key): + return self._get_data_dict()[key] + + def __setitem__(self, key, value): + self._get_data_dict()[key] = value + + def __contains__(self, key): + return key in self._get_data_dict() + + def get(self, key, default=None): + return self._get_data_dict().get(key, default) + +mock_types = MagicMock() +mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {}) +mock_types.Node = DummyBase +mock_types.NodeCustomGroup = type("NodeCustomGroup", (DummyBase,), {}) +mock_types.NodeTree = type("NodeTree", (DummyBase,), {}) +mock_types.PropertyGroup = type("PropertyGroup", (DummyBase,), {}) +mock_types.Operator = type("Operator", (DummyBase,), {}) +mock_types.Panel = type("Panel", (DummyBase,), {}) +mock_types.Menu = type("Menu", (DummyBase,), {}) +mock_types.Object = type("Object", (DummyBase,), {}) +mock_types.Mesh = type("Mesh", (DummyBase,), {}) + +mock_bpy = MagicMock() +mock_bpy.types = mock_types + +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() + +# Socket and node testing mocks +class TestNodeSocket: + def __init__(self, name="", identifier="", node=None): + self.name = name + self.identifier = identifier or name + self.is_linked = False + self.links = [] + self.default = None + self.default_value = "" + self.value = "" + self.hide = False + self.display_shape = "" + self.node = node + +class TestSocketCollection(list): + def __init__(self, node=None): + super().__init__() + self.node = node + + def new(self, type_name, name, identifier=""): + sock = TestNodeSocket(name, identifier or name, node=self.node) + self.append(sock) + return sock + + def remove(self, sock): + if sock in self: + super().remove(sock) + + def get(self, name, default=None): + for item in self: + if item.name == name or item.identifier == name: + return item + return default + +from NodeSlicer.infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input +from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode + +class TestMultiGroupsManualValue(unittest.TestCase): + def test_join_gcode_multi_groups_manual_value(self): + node = SLICENODE_JoinGcode() + node.inputs = TestSocketCollection(node) + node.outputs = TestSocketCollection(node) + + node.add_output("NodeSocketString", "Gcode") + node.add_multi_socket_group("NodeSocketString", name="Gcode") + + # Initial state should have 1 input socket + self.assertEqual(len(node.inputs), 1) + + # Entering a manual value in socket 0 + node.inputs[0].value = "G1 X10 Y10 F1200" + node._update_multi_groups() + + # Should trigger adding socket 1 + self.assertEqual(len(node.inputs), 2) + + # Entering a manual value in socket 1 + node.inputs[1].value = "G1 X20 Y20" + node._update_multi_groups() + + # Should trigger adding socket 2 + self.assertEqual(len(node.inputs), 3) + + # Evaluate output should join manual values + joined = node.evaluate_output("Gcode") + self.assertEqual(joined, "G1 X10 Y10 F1200\nG1 X20 Y20") + + # Clearing socket 1 manual value + node.inputs[1].value = "" + node._update_multi_groups() + + # Trailing inactive sockets cleaned up + self.assertEqual(len(node.inputs), 2) + + def test_join_gcode_default_value_trigger(self): + node = SLICENODE_JoinGcode() + node.inputs = TestSocketCollection(node) + node.outputs = TestSocketCollection(node) + + node.add_output("NodeSocketString", "Gcode") + node.add_multi_socket_group("NodeSocketString", name="Gcode") + + # Set default_value on native socket + node.inputs[0].default_value = "M104 S200" + node._update_multi_groups() + + self.assertEqual(len(node.inputs), 2) + self.assertEqual(node.evaluate_output("Gcode"), "M104 S200") + +if __name__ == "__main__": + unittest.main()