This commit is contained in:
Nicolas Predella 2026-08-25 14:47:46 +03:00
parent 7228613ae4
commit b67a8d9755
6 changed files with 195 additions and 17 deletions

View file

@ -184,8 +184,11 @@ class SLICENODE_JoinGcode(SliceNodeJoiner):
from ....infra.blender.blender_slice_nodes_generic import resolve_input from ....infra.blender.blender_slice_nodes_generic import resolve_input
gcode_input: list[str] = [] gcode_input: list[str] = []
for input in self.inputs: for input in self.inputs:
gc: str = str(resolve_input(input)) val = resolve_input(input)
gcode_input += [gc] or [""] if val is not None:
gc = str(val)
if gc:
gcode_input.append(gc)
return "\n".join(gcode_input) return "\n".join(gcode_input)
@register_class @register_class

View file

@ -590,12 +590,10 @@ class SLICENODE_TransformGeometry(SliceNode):
self.add_output("SLICESOCK_Geometry", "Geometry") self.add_output("SLICESOCK_Geometry", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name not in ("Geometry", "Mesh"): if socket_name not in ("Geometry"):
return None return None
geom_data: Any = self.resolve("Geometry") geom_data: Any = self.resolve("Geometry")
if geom_data is None:
geom_data = self.resolve("Mesh")
if geom_data is None: if geom_data is None:
return None return None

View file

@ -19,14 +19,7 @@ class LabelNodeSocket(SliceNodeSocket):
class InputNodeSocket(SliceNodeSocket): class InputNodeSocket(SliceNodeSocket):
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 node and isinstance(node, bpy.types.NodeCustomGroup): if self.is_output or self.is_linked:
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:
layout.label(text=text or self.name) layout.label(text=text or self.name)
else: else:
self._draw_prop(text, layout) self._draw_prop(text, layout)

View file

@ -150,6 +150,11 @@ def depsgraph_update_handler(scene, depsgraph):
node._update_outputs() node._update_outputs()
except Exception: except Exception:
pass pass
if hasattr(node, "_update_multi_groups"):
try:
node._update_multi_groups()
except Exception:
pass
@register_class @register_class

View file

@ -64,6 +64,39 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName):
from ...registry import get_label_by_name, register_class from ...registry import get_label_by_name, register_class
class SliceNode(Node, AutoBlIdName): 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: def update(self) -> None:
self._update_multi_groups() 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_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 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() ins = get_ins()
@ -426,13 +465,13 @@ def resolve_input(
if not socket.is_linked: if not socket.is_linked:
try: try:
val = getattr(socket, "default_value", None) val = getattr(socket, "value", None)
if val is not None: return val if val is not None and val != "": return val
except Exception: except Exception:
pass pass
try: try:
val = getattr(socket, "value", None) val = getattr(socket, "default_value", None)
if val is not None: return val if val is not None: return val
except Exception: except Exception:
pass pass

140
tests/test_multi_groups.py Normal file
View file

@ -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()