nodeslicer/tests/test_lists.py
2026-10-14 17:48:12 +03:00

1339 lines
53 KiB
Python

import sys
import unittest
from unittest.mock import MagicMock
from pathlib import Path
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.append(str(PROJECT_ROOT.parent))
# Ensure bpy mocks
class DummyObject:
def __init__(self, name="TestObj"):
self.name = name
class DummyMesh: pass
class DummyBase: pass
mock_types = MagicMock()
mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {})
mock_types.Node = type("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.Object = DummyObject
mock_types.Mesh = DummyMesh
mock_bpy = MagicMock()
mock_bpy.context.scene = MagicMock()
mock_bpy.context.scene.unit_settings.scale_length = 0.001
mock_bpy.types = mock_types
mock_bpy.path.abspath = lambda path: path.replace("//", "/abs/")
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.list_utils import (
is_list_data,
is_input_a_list,
is_vector_value,
prepare_indexed_inputs,
eval_element_wise_multivalue,
flatten_list,
eval_element_wise,
set_socket_shape,
is_socket_linked_to_list,
is_output_socket_a_list,
sync_node_list_sockets,
group_input_can_accept_list,
is_group_output_a_list,
sync_group_node_socket_shapes,
)
from NodeSlicer.core.slice import SlicePaths
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection, SlicingGroup
class TestListUtils(unittest.TestCase):
def test_is_list_data(self):
self.assertFalse(is_list_data(None))
self.assertFalse(is_list_data("a string"))
self.assertFalse(is_list_data(b"bytes"))
self.assertFalse(is_list_data(123))
self.assertFalse(is_list_data(12.34))
# SlicePaths subclasses list, but represents an atomic toolpath set, NOT a list of toolpaths
sp = SlicePaths()
self.assertFalse(is_list_data(sp))
# Normal lists and tuples
self.assertTrue(is_list_data([]))
self.assertTrue(is_list_data([1, 2, 3]))
self.assertTrue(is_list_data((1, 2)))
self.assertTrue(is_list_data([sp, sp]))
def test_flatten_list(self):
sp1 = SlicePaths()
sp2 = SlicePaths()
nested = [sp1, [sp2, None], [["gcode1"], "gcode2"]]
flattened = flatten_list(nested)
self.assertEqual(flattened, [sp1, sp2, "gcode1", "gcode2"])
def test_eval_element_wise(self):
def double(x):
return x * 2
self.assertEqual(eval_element_wise(double, 5), 10)
self.assertEqual(eval_element_wise(double, [1, 2, 3]), [2, 4, 6])
class DummySocket:
def __init__(self, name="Socket", bl_idname="NodeSocketFloat", display_shape="CIRCLE"):
self.name = name
self.identifier = name
self.bl_idname = bl_idname
self.display_shape = display_shape
self.is_linked = False
self.links = []
self.node = None
self.value = None
class DummyLink:
def __init__(self, from_socket, to_socket):
self.from_socket = from_socket
self.to_socket = to_socket
class DummySocketCollection(list):
def get(self, name, default=None):
for s in self:
if getattr(s, "name", "") == name:
return s
return default
from NodeSlicer.infra.blender.blender_slice_nodes_generic import SliceNode
class DummyNode(SliceNode):
def __init__(self, bl_idname="DummyNode", name="DummyNode"):
self.bl_idname = bl_idname
self.name = name
self.inputs = DummySocketCollection()
self.outputs = DummySocketCollection()
self.is_list_output = False
def as_pointer(self):
return id(self)
def add_in(self, sock):
sock.node = self
self.inputs.append(sock)
def add_out(self, sock):
sock.node = self
self.outputs.append(sock)
class TestSocketShapesAndLinks(unittest.TestCase):
def test_socket_linked_to_list(self):
upstream_node = DummyNode("SLICENODE_MeshesToList")
upstream_node.is_list_output = True
out_sock = DummySocket("Meshes", "SLICESOCK_Mesh", display_shape="LIST")
upstream_node.add_out(out_sock)
downstream_node = DummyNode("SLICENODE_SliceShells")
in_sock = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="CIRCLE")
downstream_node.add_in(in_sock)
# Before linking
self.assertFalse(is_socket_linked_to_list(in_sock))
self.assertTrue(is_output_socket_a_list(out_sock))
# Link them
link = DummyLink(out_sock, in_sock)
in_sock.is_linked = True
in_sock.links = [link]
self.assertTrue(is_socket_linked_to_list(in_sock))
def test_sync_node_list_sockets(self):
upstream_node = DummyNode("SLICENODE_MeshesToList")
out_sock = DummySocket("Meshes", "SLICESOCK_Mesh", display_shape="LIST")
upstream_node.add_out(out_sock)
node = DummyNode("SLICENODE_SliceShells")
in_sock = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="CIRCLE")
out_path = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE")
node.add_in(in_sock)
node.add_out(out_path)
link = DummyLink(out_sock, in_sock)
in_sock.is_linked = True
in_sock.links = [link]
mapping = [("Mesh", "Paths")]
sync_node_list_sockets(node, mapping)
self.assertEqual(in_sock.display_shape, "LIST")
self.assertEqual(out_path.display_shape, "LIST")
class TestNodeEvaluationsWithLists(unittest.TestCase):
def test_path_operations_with_first_input_list(self):
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOperations
node = SLICENODE_PathOperations()
node.operation = 'EXTEND'
p1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)])
p2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)])
q = SlicePaths([np.array([[10, 10, 0], [11, 11, 0]], dtype=np.float64)])
# Mock inputs
inp0 = DummySocket("Paths", "SLICESOCK_Paths")
inp0.is_linked = True
inp1 = DummySocket("Paths", "SLICESOCK_Paths")
inp1.is_linked = True
node.inputs = [inp0, inp1]
# resolve input mocking
def mock_resolve_input(sock, **kwargs):
if sock is inp0:
return [p1, p2] # List input
elif sock is inp1:
return q
return None
import NodeSlicer.infra.blender.blender_slice_nodes_generic as b_gen
original_resolve = b_gen.resolve_input
b_gen.resolve_input = mock_resolve_input
try:
res = node.evaluate_output("Paths")
self.assertIsInstance(res, list)
self.assertEqual(len(res), 2)
# res[0] is EXTEND of p1 and q
self.assertEqual(len(res[0]), 2)
# res[1] is EXTEND of p2 and q
self.assertEqual(len(res[1]), 2)
finally:
b_gen.resolve_input = original_resolve
def test_join_gcode_no_lists(self):
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
node = SLICENODE_JoinGcode()
node.resolve_multi_input = lambda label: ["G28", "G90", "M104 S200"]
res = node.evaluate_output("Gcode")
self.assertEqual(res, "G28\nG90\nM104 S200")
def test_join_gcode_one_list(self):
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
node = SLICENODE_JoinGcode()
node.resolve_multi_input = lambda label: ["START", ["LAYER1", "LAYER2", "LAYER3"], "END"]
res = node.evaluate_output("Gcode")
self.assertEqual(res, [
"START\nLAYER1\nEND",
"START\nLAYER2\nEND",
"START\nLAYER3\nEND",
])
def test_join_gcode_multiple_lists_zipped(self):
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
node = SLICENODE_JoinGcode()
node.resolve_multi_input = lambda label: [
"INIT",
["A1", "A2", "A3"],
"MID",
["B1", "B2", "B3"],
"FINAL",
]
res = node.evaluate_output("Gcode")
self.assertEqual(res, [
"INIT\nA1\nMID\nB1\nFINAL",
"INIT\nA2\nMID\nB2\nFINAL",
"INIT\nA3\nMID\nB3\nFINAL",
])
def test_join_gcode_mismatched_lengths_raises_error(self):
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
node = SLICENODE_JoinGcode()
node.resolve_multi_input = lambda label: [["A1", "A2"], ["B1", "B2", "B3"]]
with self.assertRaises(ValueError):
node.evaluate_output("Gcode")
def test_join_gcode_socket_shape_updates(self):
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_JoinGcode
node = SLICENODE_JoinGcode()
in0 = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE")
out0 = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE")
node.inputs = DummySocketCollection([in0])
node.outputs = DummySocketCollection([out0])
# Initially without list link
node.update()
self.assertEqual(out0.display_shape, "CIRCLE")
# Linked to list
upstream = DummyNode("SLICENODE_GcodesToList")
upstream.is_list_output = True
up_out = DummySocket("Gcode", "NodeSocketString", display_shape="LIST")
upstream.add_out(up_out)
link = DummyLink(up_out, in0)
in0.is_linked = True
in0.links = [link]
node.update()
self.assertEqual(out0.display_shape, "LIST")
def test_gcode_regex_replace_list(self):
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodeRegexReplace
node = SLICENODE_GcodeRegexReplace()
node.resolve = lambda field: {
"Gcode": ["G1 X10 Y10", "G1 X20 Y20"],
"Find": "G1",
"Replace": "G0"
}.get(field)
res = node.evaluate_output("Gcode")
self.assertEqual(res, ["G0 X10 Y10", "G0 X20 Y20"])
def test_list_joiners_evaluation(self):
from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_ObjectsToList
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_MeshesToList
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathsToList
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodesToList
# ObjectsToList
obj1 = DummyObject("Obj1")
obj2 = DummyObject("Obj2")
o_node = SLICENODE_ObjectsToList()
o_node.resolve_multi_input = lambda label: [[obj1], obj2]
self.assertEqual(o_node.evaluate_output("Objects"), [obj1, obj2])
# MeshesToList
sc1 = SlicingCollection([], "M1")
sc2 = SlicingCollection([], "M2")
m_node = SLICENODE_MeshesToList()
m_node.resolve_multi_input = lambda label: [sc1, [sc2]]
self.assertEqual(m_node.evaluate_output("Meshes"), [sc1, sc2])
# PathsToList
sp1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)])
sp2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)])
p_node = SLICENODE_PathsToList()
p_node.resolve_multi_input = lambda label: [sp1, [sp2]]
self.assertEqual(p_node.evaluate_output("Paths"), [sp1, sp2])
# GcodesToList
g_node = SLICENODE_GcodesToList()
g_node.resolve_multi_input = lambda label: [["G1 X1"], "G1 X2"]
self.assertEqual(g_node.evaluate_output("Gcode"), ["G1 X1", "G1 X2"])
def test_x_to_list_flattens_nested_and_incoming_lists(self):
from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_ObjectsToList
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_MeshesToList
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathsToList
from NodeSlicer.core.nodeslicer.nodes.gcode import SLICENODE_GcodesToList
# ObjectsToList with nested lists and single elements
obj1, obj2, obj3, obj4 = DummyObject("O1"), DummyObject("O2"), DummyObject("O3"), DummyObject("O4")
o_node = SLICENODE_ObjectsToList()
o_node.resolve_multi_input = lambda label: [[obj1, obj2], obj3, [obj4]]
self.assertEqual(o_node.evaluate_output("Objects"), [obj1, obj2, obj3, obj4])
# MeshesToList with nested lists
sc1 = SlicingCollection([], "M1")
sc2 = SlicingCollection([], "M2")
sc3 = SlicingCollection([], "M3")
m_node = SLICENODE_MeshesToList()
m_node.resolve_multi_input = lambda label: [[sc1, sc2], [sc3]]
self.assertEqual(m_node.evaluate_output("Meshes"), [sc1, sc2, sc3])
# PathsToList with list input and single input
sp1 = SlicePaths([np.array([[0, 0, 0], [1, 1, 0]], dtype=np.float64)])
sp2 = SlicePaths([np.array([[2, 2, 0], [3, 3, 0]], dtype=np.float64)])
sp3 = SlicePaths([np.array([[4, 4, 0], [5, 5, 0]], dtype=np.float64)])
p_node = SLICENODE_PathsToList()
p_node.resolve_multi_input = lambda label: [[sp1, sp2], sp3]
self.assertEqual(p_node.evaluate_output("Paths"), [sp1, sp2, sp3])
# GcodesToList with list input and single input
g_node = SLICENODE_GcodesToList()
g_node.resolve_multi_input = lambda label: [["G1 X1", "G1 X2"], "G1 X3", ["G1 X4", "G1 X5"]]
self.assertEqual(g_node.evaluate_output("Gcode"), ["G1 X1", "G1 X2", "G1 X3", "G1 X4", "G1 X5"])
def test_group_to_list(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GroupToList
sc1 = SlicingCollection([], "PartA")
sc2 = SlicingCollection([], "PartB")
group = SlicingGroup([])
group.collections = {"PartA": sc1, "PartB": sc2}
node = SLICENODE_GroupToList()
self.assertEqual(node.always_list_outputs, ["Collections"])
# 1. Normal conversion: SlicingGroup -> list[SlicingCollection]
node.resolve = lambda label: group
res = node.evaluate_output("Collections")
self.assertIsInstance(res, list)
self.assertEqual(len(res), 2)
self.assertEqual(res, [sc1, sc2])
# 2. None input -> empty list
node.resolve = lambda label: None
self.assertEqual(node.evaluate_output("Collections"), [])
# 3. Single collection passed directly -> [sc1]
node.resolve = lambda label: sc1
self.assertEqual(node.evaluate_output("Collections"), [sc1])
# 4. List of groups -> flattened list of collections
group2 = SlicingGroup([])
sc3 = SlicingCollection([], "PartC")
group2.collections = {"PartC": sc3}
node.resolve = lambda label: [group, group2]
res_multi = node.evaluate_output("Collections")
self.assertEqual(res_multi, [sc1, sc2, sc3])
def test_list_to_group(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_ListToGroup
sc1 = SlicingCollection([], "PartA")
sc2 = SlicingCollection([], "PartB")
node = SLICENODE_ListToGroup()
self.assertEqual(node.always_list_inputs, ["Collections"])
# 1. Normal conversion: list[SlicingCollection] -> SlicingGroup
node.resolve = lambda label: [sc1, sc2]
res = node.evaluate_output("Group")
self.assertIsInstance(res, SlicingGroup)
self.assertEqual(len(res.collections), 2)
self.assertIs(res.collections.get("PartA"), sc1)
self.assertIs(res.collections.get("PartB"), sc2)
# 2. None input -> empty SlicingGroup
node.resolve = lambda label: None
res_empty = node.evaluate_output("Group")
self.assertIsInstance(res_empty, SlicingGroup)
self.assertEqual(len(res_empty.collections), 0)
# 3. Single collection -> SlicingGroup with 1 collection
node.resolve = lambda label: sc1
res_single = node.evaluate_output("Group")
self.assertIsInstance(res_single, SlicingGroup)
self.assertEqual(len(res_single.collections), 1)
self.assertIs(res_single.collections.get("PartA"), sc1)
# 4. Duplicate collection names are handled without overwriting
dup1 = SlicingCollection([], "Part")
dup2 = SlicingCollection([], "Part")
node.resolve = lambda label: [dup1, dup2]
res_dup = node.evaluate_output("Group")
self.assertEqual(len(res_dup.collections), 2)
self.assertIn("Part", res_dup.collections)
self.assertIn("Part_1", res_dup.collections)
def test_group_to_list_and_list_to_group_roundtrip(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GroupToList, SLICENODE_ListToGroup
sc1 = SlicingCollection([], "Part1")
sc1.transforms = [{"type": "INIT", "translation": (1.0, 2.0, 3.0), "rotation": (0.0, 0.0, 0.0), "scale": (1.0, 1.0, 1.0)}]
sc2 = SlicingCollection([], "Part2")
orig_group = SlicingGroup([])
orig_group.collections = {"Part1": sc1, "Part2": sc2}
orig_group.transforms = list(sc1.transforms)
g2l = SLICENODE_GroupToList()
g2l.resolve = lambda label: orig_group
colls_list = g2l.evaluate_output("Collections")
self.assertIsInstance(colls_list, list)
self.assertEqual(len(colls_list), 2)
l2g = SLICENODE_ListToGroup()
l2g.resolve = lambda label: colls_list
recovered_group = l2g.evaluate_output("Group")
self.assertIsInstance(recovered_group, SlicingGroup)
self.assertEqual(len(recovered_group.collections), 2)
self.assertEqual(set(recovered_group.collections.keys()), {"Part1", "Part2"})
self.assertEqual(recovered_group.transforms, orig_group.transforms)
def test_join_mesh_flattens_lists(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_JoinMesh
node = SLICENODE_JoinMesh()
so1 = SlicingObject()
so1.name = "part1"
so1.mesh = np.ones((1, 4, 3), dtype=np.float64)
sc1 = SlicingCollection([], "Coll1")
sc1.objects = [so1]
so2 = SlicingObject()
so2.name = "part2"
so2.mesh = np.ones((2, 4, 3), dtype=np.float64)
sc2 = SlicingCollection([], "Coll2")
sc2.objects = [so2]
so3 = SlicingObject()
so3.name = "part3"
so3.mesh = np.ones((3, 4, 3), dtype=np.float64)
sc3 = SlicingCollection([], "Coll3")
sc3.objects = [so3]
# Mock inputs delivering [ [sc1, sc2], sc3 ]
mock_in1 = DummySocket("Mesh", "SLICESOCK_Mesh")
mock_in1.is_linked = True
mock_in2 = DummySocket("Mesh", "SLICESOCK_Mesh")
mock_in2.is_linked = True
node.inputs = [mock_in1, mock_in2]
import NodeSlicer.infra.blender.blender_slice_nodes_generic as b_gen
original_resolve = b_gen.resolve_input
b_gen.resolve_input = lambda sock, **kw: [sc1, sc2] if sock is mock_in1 else sc3
try:
res = node.evaluate_output("Mesh")
self.assertIsInstance(res, SlicingCollection)
self.assertEqual(len(res.objects), 3)
self.assertEqual({o.name for o in res.objects}, {"part1", "part2", "part3"})
finally:
b_gen.resolve_input = original_resolve
def test_drop_geometry_list_evaluation(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry
node = SLICENODE_DropGeometry()
so1 = SlicingObject()
so1.name = "part1"
so1.mesh = np.array([[[0, 0, 5], [1, 0, 5], [0, 1, 5], [0, 0, 1]]], dtype=np.float64)
sc1 = SlicingCollection([], "M1")
sc1.objects = [so1]
so2 = SlicingObject()
so2.name = "part2"
so2.mesh = np.array([[[0, 0, 2], [1, 0, 2], [0, 1, 2], [0, 0, 1]]], dtype=np.float64)
sc2 = SlicingCollection([], "M2")
sc2.objects = [so2]
node.resolve = lambda field: [sc1, sc2] if field == "Mesh" else 0.0
res_mesh = node.evaluate_output("Mesh")
res_trans = node.evaluate_output("Translation")
self.assertIsInstance(res_mesh, list)
self.assertEqual(len(res_mesh), 2)
self.assertIsInstance(res_trans, list)
self.assertEqual(res_trans, [(0.0, 0.0, -5.0), (0.0, 0.0, -2.0)])
def test_start_print_rejects_list_in_evaluate(self):
from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_StartPrint
node = SLICENODE_StartPrint()
node.resolve = lambda field: ["G1 X0", "G1 X10"] if field == "Gcode" else None
op = MagicMock()
res = node.evaluate(operator=op)
self.assertEqual(res, 'error')
op.report.assert_called_with({'ERROR'}, "Start Print does not accept a list of Gcode")
class DummyInterfaceItem:
def __init__(self, name, identifier, in_out="INPUT", socket_type="SLICESOCK_Paths"):
self.name = name
self.identifier = identifier
self.in_out = in_out
self.socket_type = socket_type
self.item_type = 'SOCKET'
class DummyInterface:
def __init__(self, items):
self.items_tree = items
class DummyGroupTree:
def __init__(self, items=None, nodes=None):
self.interface = DummyInterface(items or [])
self.nodes = nodes or []
class DummyGroupNode(DummyNode):
def __init__(self, name="Group", tree=None):
super().__init__("SLICENODE_Group")
self.name = name
self.slicer_tree = tree
class TestGroupNodeListShapes(unittest.TestCase):
def test_group_input_can_accept_list_detection(self):
item_paths = DummyInterfaceItem("Paths", "sock_paths", in_out="INPUT", socket_type="SLICESOCK_Paths")
item_float = DummyInterfaceItem("FloatVal", "sock_float", in_out="INPUT", socket_type="NodeSocketFloat")
gin = DummyNode("NodeGroupInput")
gin_out_paths = DummySocket("Paths", "SLICESOCK_Paths")
gin_out_paths.identifier = "sock_paths"
gin_out_float = DummySocket("FloatVal", "NodeSocketFloat")
gin_out_float.identifier = "sock_float"
gin.add_out(gin_out_paths)
gin.add_out(gin_out_float)
target_node = DummyNode("SLICENODE_BufferShells")
target_node.list_socket_mapping = [("Paths", "Paths")]
target_in_paths = DummySocket("Paths", "SLICESOCK_Paths")
target_node.add_in(target_in_paths)
link = DummyLink(gin_out_paths, target_in_paths)
gin_out_paths.is_linked = True
gin_out_paths.links = [link]
tree = DummyGroupTree([item_paths, item_float], [gin, target_node])
# Paths input can accept list because target is BufferShells
self.assertTrue(group_input_can_accept_list(tree, item_paths))
# FloatVal cannot accept list because socket type is Float
self.assertFalse(group_input_can_accept_list(tree, item_float))
def test_group_input_connected_to_forbidden_node_rejects_list(self):
item_gcode = DummyInterfaceItem("Gcode", "sock_gcode", in_out="INPUT", socket_type="NodeSocketString")
gin = DummyNode("NodeGroupInput")
gin_out_gcode = DummySocket("Gcode", "NodeSocketString")
gin_out_gcode.identifier = "sock_gcode"
gin.add_out(gin_out_gcode)
sp_node = DummyNode("SLICENODE_StartPrint")
sp_in = DummySocket("Gcode", "NodeSocketString")
sp_node.add_in(sp_in)
link = DummyLink(gin_out_gcode, sp_in)
gin_out_gcode.is_linked = True
gin_out_gcode.links = [link]
tree = DummyGroupTree([item_gcode], [gin, sp_node])
# StartPrint forbids lists, so group input must NOT accept list
self.assertFalse(group_input_can_accept_list(tree, item_gcode))
def test_group_output_inherent_list(self):
item_out = DummyInterfaceItem("Paths", "sock_out_paths", in_out="OUTPUT", socket_type="SLICESOCK_Paths")
gout = DummyNode("NodeGroupOutput")
gout_in = DummySocket("Paths", "SLICESOCK_Paths")
gout_in.identifier = "sock_out_paths"
gout.add_in(gout_in)
list_joiner = DummyNode("SLICENODE_PathsToList")
list_joiner.is_list_output = True
lj_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
list_joiner.add_out(lj_out)
link = DummyLink(lj_out, gout_in)
gout_in.is_linked = True
gout_in.links = [link]
tree = DummyGroupTree([item_out], [gout, list_joiner])
# Output fed from PathsToList is always a list
self.assertTrue(is_group_output_a_list(None, tree, item_out))
def test_group_output_element_wise_propagation_and_sync(self):
item_in = DummyInterfaceItem("Paths", "sock_in", in_out="INPUT", socket_type="SLICESOCK_Paths")
item_out = DummyInterfaceItem("Paths", "sock_out", in_out="OUTPUT", socket_type="SLICESOCK_Paths")
gin = DummyNode("NodeGroupInput")
gin_out = DummySocket("Paths", "SLICESOCK_Paths")
gin_out.identifier = "sock_in"
gin.add_out(gin_out)
buf_node = DummyNode("SLICENODE_BufferShells")
buf_node.list_socket_mapping = [("Paths", "Paths")]
buf_in = DummySocket("Paths", "SLICESOCK_Paths")
buf_out = DummySocket("Paths", "SLICESOCK_Paths")
buf_node.add_in(buf_in)
buf_node.add_out(buf_out)
gout = DummyNode("NodeGroupOutput")
gout_in = DummySocket("Paths", "SLICESOCK_Paths")
gout_in.identifier = "sock_out"
gout.add_in(gout_in)
link1 = DummyLink(gin_out, buf_in)
gin_out.is_linked = True
gin_out.links = [link1]
buf_in.is_linked = True
buf_in.links = [link1]
link2 = DummyLink(buf_out, gout_in)
buf_out.is_linked = True
buf_out.links = [link2]
gout_in.is_linked = True
gout_in.links = [link2]
tree = DummyGroupTree([item_in, item_out], [gin, buf_node, gout])
grp_node = DummyGroupNode("MyGroup", tree)
grp_in = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE")
grp_in.identifier = "sock_in"
grp_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="CIRCLE")
grp_out.identifier = "sock_out"
grp_node.add_in(grp_in)
grp_node.add_out(grp_out)
# Before external link:
# Input should be LIST (because list input is possible)
# Output should be CIRCLE (because no list is fed)
sync_group_node_socket_shapes(grp_node)
self.assertEqual(grp_in.display_shape, "LIST")
self.assertEqual(grp_out.display_shape, "CIRCLE")
# Now link a list to the group's input externally:
up_node = DummyNode("SLICENODE_PathsToList")
up_node.is_list_output = True
up_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
up_node.add_out(up_out)
ext_link = DummyLink(up_out, grp_in)
grp_in.is_linked = True
grp_in.links = [ext_link]
# After external list link:
# Output should now become LIST!
sync_group_node_socket_shapes(grp_node)
self.assertEqual(grp_in.display_shape, "LIST")
self.assertEqual(grp_out.display_shape, "LIST")
class TestImplicitGetMesh(unittest.TestCase):
def test_resolve_input_implicit_get_mesh_single_object(self):
from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache
parent_obj = DummyObject("ParentCube")
child_obj1 = DummyObject("Child1")
child_obj2 = DummyObject("Child2")
parent_obj.children = [child_obj1, child_obj2]
# Source node outputting parent_obj
src_node = DummyNode("SLICENODE_InputObject")
src_node.name = "InputObject"
src_out = DummySocket("Object", "NodeSocketObject")
src_out.node = src_node
src_node.outputs = DummySocketCollection([src_out])
src_node.evaluate_output = lambda socket_name, **kw: parent_obj
# Target node with SLICESOCK_Mesh input
dst_node = DummyNode("SLICENODE_SliceShells")
dst_in = DummySocket("Mesh", "SLICESOCK_Mesh")
dst_in.node = dst_node
dst_node.inputs = DummySocketCollection([dst_in])
# Link them
dst_in.is_linked = True
link = DummyLink(src_out, dst_in)
dst_in.links = [link]
with execution_cache() as cache:
result = resolve_input(dst_in, cache=cache)
self.assertIsInstance(result, SlicingCollection)
self.assertEqual(result.name, "ParentCube")
def test_resolve_input_implicit_get_mesh_object_list(self):
from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache
obj1 = DummyObject("Obj1")
obj2 = DummyObject("Obj2")
src_node = DummyNode("SLICENODE_ObjectsToList")
src_node.name = "ObjectsToList"
src_out = DummySocket("Objects", "NodeSocketObject", display_shape="LIST")
src_out.node = src_node
src_node.outputs = DummySocketCollection([src_out])
src_node.evaluate_output = lambda socket_name, **kw: [obj1, obj2]
dst_node = DummyNode("SLICENODE_SliceShells")
dst_in = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="LIST")
dst_in.node = dst_node
dst_node.inputs = DummySocketCollection([dst_in])
dst_in.is_linked = True
dst_in.links = [DummyLink(src_out, dst_in)]
with execution_cache() as cache:
result = resolve_input(dst_in, cache=cache)
self.assertIsInstance(result, list)
self.assertEqual(len(result), 2)
self.assertIsInstance(result[0], SlicingCollection)
self.assertIsInstance(result[1], SlicingCollection)
self.assertEqual(result[0].name, "Obj1")
self.assertEqual(result[1].name, "Obj2")
def test_resolve_input_already_mesh_is_not_reconverted(self):
from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache
sc = SlicingCollection([], "ExistingMesh")
src_node = DummyNode("SLICENODE_GetMesh")
src_node.name = "GetMesh"
src_out = DummySocket("Mesh", "SLICESOCK_Mesh")
src_out.node = src_node
src_node.outputs = DummySocketCollection([src_out])
src_node.evaluate_output = lambda socket_name, **kw: sc
dst_node = DummyNode("SLICENODE_SliceShells")
dst_in = DummySocket("Mesh", "SLICESOCK_Mesh")
dst_in.node = dst_node
dst_node.inputs = DummySocketCollection([dst_in])
dst_in.is_linked = True
dst_in.links = [DummyLink(src_out, dst_in)]
with execution_cache() as cache:
result = resolve_input(dst_in, cache=cache)
self.assertIs(result, sc)
def test_resolve_input_object_to_object_socket_not_converted(self):
from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache
obj = DummyObject("MyObj")
src_node = DummyNode("SLICENODE_InputObject")
src_node.name = "InputObject"
src_out = DummySocket("Object", "NodeSocketObject")
src_out.node = src_node
src_node.outputs = DummySocketCollection([src_out])
src_node.evaluate_output = lambda socket_name, **kw: obj
dst_node = DummyNode("SLICENODE_ObjectsToList")
dst_in = DummySocket("Object", "NodeSocketObject")
dst_in.node = dst_node
dst_node.inputs = DummySocketCollection([dst_in])
dst_in.is_linked = True
dst_in.links = [DummyLink(src_out, dst_in)]
with execution_cache() as cache:
result = resolve_input(dst_in, cache=cache)
self.assertIs(result, obj)
def test_implicit_get_mesh_caching(self):
from NodeSlicer.infra.blender.blender_slice_nodes_generic import resolve_input, execution_cache
obj = DummyObject("SharedObj")
src_node = DummyNode("SLICENODE_InputObject")
src_node.name = "InputObject"
src_out = DummySocket("Object", "NodeSocketObject")
src_out.node = src_node
src_node.outputs = DummySocketCollection([src_out])
src_node.evaluate_output = lambda socket_name, **kw: obj
dst_node1 = DummyNode("SLICENODE_SliceShells")
dst_in1 = DummySocket("Mesh", "SLICESOCK_Mesh")
dst_in1.node = dst_node1
dst_node1.inputs = DummySocketCollection([dst_in1])
dst_in1.is_linked = True
dst_in1.links = [DummyLink(src_out, dst_in1)]
dst_node2 = DummyNode("SLICENODE_JoinMesh")
dst_in2 = DummySocket("Mesh", "SLICESOCK_Mesh")
dst_in2.node = dst_node2
dst_node2.inputs = DummySocketCollection([dst_in2])
dst_in2.is_linked = True
dst_in2.links = [DummyLink(src_out, dst_in2)]
with execution_cache() as cache:
res1 = resolve_input(dst_in1, cache=cache)
res2 = resolve_input(dst_in2, cache=cache)
self.assertIs(res1, res2)
def test_explicit_get_mesh_include_children(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GetMesh
parent_obj = DummyObject("Parent")
child_obj = DummyObject("Child")
parent_obj.children = [child_obj]
node_with = SLICENODE_GetMesh()
node_with.include_children = True
node_with.resolve = lambda label: parent_obj
res_with = node_with.evaluate_output("Mesh")
self.assertIsInstance(res_with, SlicingCollection)
node_without = SLICENODE_GetMesh()
node_without.include_children = False
node_without.resolve = lambda label: parent_obj
res_without = node_without.evaluate_output("Mesh")
self.assertIsInstance(res_without, SlicingCollection)
def test_implicit_get_mesh_passes_all_children_to_slicing_collection(self):
from unittest.mock import patch
from NodeSlicer.infra.blender.blender_slice_nodes_generic import object_to_mesh
parent_obj = DummyObject("Parent")
c1 = DummyObject("C1")
c2 = DummyObject("C2")
gc1 = DummyObject("GC1")
c2.children = [gc1]
parent_obj.children = [c1, c2]
with patch.object(SlicingCollection, "__init__", return_value=None) as mock_init:
object_to_mesh(parent_obj, include_children=True)
self.assertTrue(mock_init.called)
family, name = mock_init.call_args[0]
self.assertEqual(name, "Parent")
self.assertEqual(set(family), {parent_obj, c1, c2, gc1})
class TestGeneralizedListArchitecture(unittest.TestCase):
def test_prepare_indexed_inputs_matching_length(self):
main = ["A", "B", "C"]
sec1 = [1, 2, 3]
sec2 = ["x", "y", "z"]
is_list, items, sec_lists = prepare_indexed_inputs(main, sec1, sec2, input_names=["sec1", "sec2"])
self.assertTrue(is_list)
self.assertEqual(items, ["A", "B", "C"])
self.assertEqual(sec_lists[0], [1, 2, 3])
self.assertEqual(sec_lists[1], ["x", "y", "z"])
def test_prepare_indexed_inputs_broadcast_single_value(self):
main = ["A", "B", "C"]
sec1 = 42
sec2 = ["single"]
is_list, items, sec_lists = prepare_indexed_inputs(main, sec1, sec2, input_names=["sec1", "sec2"])
self.assertTrue(is_list)
self.assertEqual(items, ["A", "B", "C"])
self.assertEqual(sec_lists[0], [42, 42, 42])
self.assertEqual(sec_lists[1], ["single", "single", "single"])
def test_prepare_indexed_inputs_mismatched_length_raises(self):
main = ["A", "B", "C"]
sec = [1, 2]
with self.assertRaises(ValueError) as ctx:
prepare_indexed_inputs(main, sec, input_names=["my_sec"])
self.assertIn("my_sec", str(ctx.exception))
self.assertIn("3 items", str(ctx.exception))
self.assertIn("2 items", str(ctx.exception))
def test_prepare_indexed_inputs_single_main_with_single_sec(self):
main = "A"
sec1 = 42
sec2 = [99]
is_list, items, sec_lists = prepare_indexed_inputs(main, sec1, sec2, input_names=["sec1", "sec2"])
self.assertFalse(is_list)
self.assertEqual(items, ["A"])
self.assertEqual(sec_lists[0], [42])
self.assertEqual(sec_lists[1], [99])
def test_prepare_indexed_inputs_single_main_with_multi_sec_raises(self):
main = "A"
sec = [1, 2]
with self.assertRaises(ValueError) as ctx:
prepare_indexed_inputs(main, sec, input_names=["my_sec"])
self.assertIn("single item", str(ctx.exception))
self.assertIn("2 items", str(ctx.exception))
def test_vector_input_distinction(self):
class MockBpyPropArray:
def __init__(self, items):
self._items = list(items)
def __iter__(self):
return iter(self._items)
def __len__(self):
return len(self._items)
def __getitem__(self, idx):
return self._items[idx]
MockBpyPropArray.__name__ = "bpy_prop_array"
# Single vector variants (tuple, list, np.ndarray, MockBpyPropArray)
vec_tuple = (1.0, 2.0, 3.0)
vec_list = [1.0, 2.0, 3.0]
vec_np = np.array([1.0, 2.0, 3.0])
vec_bpy = MockBpyPropArray([1.0, 2.0, 3.0])
for v in (vec_tuple, vec_list, vec_np, vec_bpy):
self.assertTrue(is_vector_value(v), f"Failed for {type(v).__name__}")
self.assertFalse(is_input_a_list(v, is_vector=True), f"Failed for {type(v).__name__}")
# List of vectors IS a list
list_of_vecs = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)]
self.assertFalse(is_vector_value(list_of_vecs))
self.assertTrue(is_input_a_list(list_of_vecs, is_vector=True))
# Broadcasting single vector across main list
main = ["geom1", "geom2"]
for v in (vec_tuple, vec_list, vec_np, vec_bpy):
is_list, items, (t_list,) = prepare_indexed_inputs(main, v, is_vector=[True], input_names=["Translation"])
self.assertTrue(is_list)
self.assertEqual(len(t_list), 2)
self.assertEqual(list(t_list[0]), [1.0, 2.0, 3.0])
self.assertEqual(list(t_list[1]), [1.0, 2.0, 3.0])
# Single main geometry with single atomic vector must NOT be treated as a list of 3 items
single_main = "geom1"
for v in (vec_tuple, vec_list, vec_np, vec_bpy):
is_list, items, (t_list,) = prepare_indexed_inputs(single_main, v, is_vector=[True], input_names=["Translation"])
self.assertFalse(is_list)
self.assertEqual(len(t_list), 1)
self.assertEqual(list(t_list[0]), [1.0, 2.0, 3.0])
# Matching list of vectors index-per-index
is_list, items, (t_list,) = prepare_indexed_inputs(main, list_of_vecs, is_vector=[True], input_names=["Translation"])
self.assertTrue(is_list)
self.assertEqual(t_list, [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)])
def test_transform_geometry_atomic_vector_inputs(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry
class MockBpyPropArray:
def __init__(self, items):
self._items = list(items)
def __iter__(self):
return iter(self._items)
def __len__(self):
return len(self._items)
def __getitem__(self, idx):
return self._items[idx]
MockBpyPropArray.__name__ = "bpy_prop_array"
so1 = SlicingObject()
so1.name = "Part1"
so1.mesh = np.zeros((1, 4, 3), dtype=np.float64)
c1 = SlicingCollection([so1], "Col1")
so2 = SlicingObject()
so2.name = "Part2"
so2.mesh = np.zeros((1, 4, 3), dtype=np.float64)
c2 = SlicingCollection([so2], "Col2")
tf_node = SLICENODE_TransformGeometry()
# 1. Single geometry + atomic tuple translation
tf_node.resolve = lambda label: {
"Geometry": c1,
"Translation": (10.0, 20.0, 30.0),
"Rotation": (0.0, 0.0, 0.0),
"Scale": (1.0, 1.0, 1.0),
}.get(label)
res = tf_node.evaluate_output("Geometry")
self.assertNotIsInstance(res, list)
self.assertIsInstance(res, SlicingCollection)
self.assertEqual(res.transforms[-1]["translation"], (10.0, 20.0, 30.0))
# 2. Single geometry + atomic list translation [10, 20, 30] (NOT a list of 3 items!)
tf_node.resolve = lambda label: {
"Geometry": c1,
"Translation": [10.0, 20.0, 30.0],
"Rotation": [0.0, 0.0, 0.0],
"Scale": [1.0, 1.0, 1.0],
}.get(label)
res = tf_node.evaluate_output("Geometry")
self.assertNotIsInstance(res, list)
self.assertIsInstance(res, SlicingCollection)
self.assertEqual(tuple(res.transforms[-1]["translation"]), (10.0, 20.0, 30.0))
# 3. Single geometry + mock bpy_prop_array (unlinked socket default value in Blender)
tf_node.resolve = lambda label: {
"Geometry": c1,
"Translation": MockBpyPropArray([10.0, 20.0, 30.0]),
"Rotation": MockBpyPropArray([0.0, 0.0, 0.0]),
"Scale": MockBpyPropArray([1.0, 1.0, 1.0]),
}.get(label)
res = tf_node.evaluate_output("Geometry")
self.assertNotIsInstance(res, list)
self.assertIsInstance(res, SlicingCollection)
self.assertEqual(tuple(res.transforms[-1]["translation"]), (10.0, 20.0, 30.0))
# 4. Multiple geometries + atomic translation (broadcasted)
tf_node.resolve = lambda label: {
"Geometry": [c1, c2],
"Translation": (10.0, 20.0, 30.0),
"Rotation": (0.0, 0.0, 0.0),
"Scale": (1.0, 1.0, 1.0),
}.get(label)
res_multi = tf_node.evaluate_output("Geometry")
self.assertIsInstance(res_multi, list)
self.assertEqual(len(res_multi), 2)
self.assertEqual(res_multi[0].transforms[-1]["translation"], (10.0, 20.0, 30.0))
self.assertEqual(res_multi[1].transforms[-1]["translation"], (10.0, 20.0, 30.0))
# 5. Multiple geometries + list of 2 translations
tf_node.resolve = lambda label: {
"Geometry": [c1, c2],
"Translation": [(10.0, 0.0, 0.0), (20.0, 0.0, 0.0)],
"Rotation": (0.0, 0.0, 0.0),
"Scale": (1.0, 1.0, 1.0),
}.get(label)
res_matched = tf_node.evaluate_output("Geometry")
self.assertIsInstance(res_matched, list)
self.assertEqual(len(res_matched), 2)
self.assertEqual(res_matched[0].transforms[-1]["translation"], (10.0, 0.0, 0.0))
self.assertEqual(res_matched[1].transforms[-1]["translation"], (20.0, 0.0, 0.0))
# 6. Single geometry + list of 2 translations -> raises ValueError
tf_node.resolve = lambda label: {
"Geometry": c1,
"Translation": [(10.0, 0.0, 0.0), (20.0, 0.0, 0.0)],
"Rotation": (0.0, 0.0, 0.0),
"Scale": (1.0, 1.0, 1.0),
}.get(label)
with self.assertRaises(ValueError):
tf_node.evaluate_output("Geometry")
def test_path_nodes_atomic_vector_inputs(self):
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOffset, SLICENODE_RotatePath
class MockBpyPropArray:
def __init__(self, items):
self._items = list(items)
def __iter__(self):
return iter(self._items)
def __len__(self):
return len(self._items)
def __getitem__(self, idx):
return self._items[idx]
MockBpyPropArray.__name__ = "bpy_prop_array"
p1 = SlicePaths([np.array([[0, 0, 0], [10, 0, 0]], dtype=np.int32)])
# Path Offset with atomic (10, 0, 0) as tuple, list, and bpy_prop_array
node_offset = SLICENODE_PathOffset()
for off in ((10.0, 0.0, 0.0), [10.0, 0.0, 0.0], MockBpyPropArray([10.0, 0.0, 0.0])):
node_offset.resolve = lambda label, o=off: {
"Paths": p1,
"Offset": o,
}.get(label)
res = node_offset.evaluate_output("Paths")
self.assertIsInstance(res, SlicePaths)
self.assertEqual(len(res), 1)
# Rotate Path with atomic center
node_rot = SLICENODE_RotatePath()
for cent in ((5.0, 5.0, 0.0), [5.0, 5.0, 0.0], MockBpyPropArray([5.0, 5.0, 0.0])):
node_rot.resolve = lambda label, c=cent: {
"Paths": p1,
"Angle": 45.0,
"Center": c,
}.get(label)
res = node_rot.evaluate_output("Paths")
self.assertIsInstance(res, SlicePaths)
self.assertEqual(len(res), 1)
def test_eval_element_wise_multivalue(self):
op = lambda item, a, b: f"{item}_{a}_{b}"
res = eval_element_wise_multivalue(op, ["x", "y"], [1, 2], "const")
self.assertEqual(res, ["x_1_const", "y_2_const"])
single_res = eval_element_wise_multivalue(op, "x", [1], "const")
self.assertEqual(single_res, "x_1_const")
def test_math_node_list_broadcasting_and_matching(self):
from NodeSlicer.core.nodeslicer.nodes.math import SLICENODE_FloatMath
node = SLICENODE_FloatMath()
in0 = DummySocket("Value", "NodeSocketFloat")
in1 = DummySocket("Value", "NodeSocketFloat")
node.inputs = DummySocketCollection([in0, in1])
node.operation = 'ADD'
# Case 1: A is list, B is scalar
node.resolve = lambda label: [1.0, 2.0, 3.0] if label == node.inputs[0] else 10.0
self.assertEqual(node.evaluate_output("Value"), [11.0, 12.0, 13.0])
# Case 2: A is scalar, B is list
node.resolve = lambda label: 10.0 if label == node.inputs[0] else [1.0, 2.0, 3.0]
self.assertEqual(node.evaluate_output("Value"), [11.0, 12.0, 13.0])
# Case 3: A and B are lists of equal length
node.resolve = lambda label: [1.0, 2.0, 3.0] if label == node.inputs[0] else [10.0, 20.0, 30.0]
self.assertEqual(node.evaluate_output("Value"), [11.0, 22.0, 33.0])
# Case 4: Mismatched list lengths raise ValueError
node.resolve = lambda label: [1.0, 2.0] if label == node.inputs[0] else [10.0, 20.0, 30.0]
with self.assertRaises(ValueError):
node.evaluate_output("Value")
def test_transform_geometry_with_get_transforms_list(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GetTransforms, SLICENODE_TransformGeometry
# Create two mock slicing collections with transforms
c1 = SlicingCollection([], "Part1")
c1.transforms = [{"type": "INIT", "translation": (10.0, 0.0, 0.0), "rotation": (0.0, 0.0, 0.0), "scale": (1.0, 1.0, 1.0)}]
c2 = SlicingCollection([], "Part2")
c2.transforms = [{"type": "INIT", "translation": (20.0, 0.0, 0.0), "rotation": (0.0, 0.0, 0.0), "scale": (2.0, 2.0, 2.0)}]
# Get Transforms on list of geometries
get_t_node = SLICENODE_GetTransforms()
get_t_node.resolve = lambda label: [c1, c2]
t_list = get_t_node.evaluate_output("Translation")
r_list = get_t_node.evaluate_output("Rotation")
s_list = get_t_node.evaluate_output("Scale")
self.assertEqual(len(t_list), 2)
self.assertEqual(len(r_list), 2)
self.assertEqual(len(s_list), 2)
self.assertEqual(t_list[0], (10.0, 0.0, 0.0))
self.assertEqual(t_list[1], (20.0, 0.0, 0.0))
# Pass those into Transform Geometry
tf_node = SLICENODE_TransformGeometry()
tf_node.resolve = lambda label: {
"Geometry": [c1, c2],
"Translation": t_list,
"Rotation": r_list,
"Scale": s_list,
}.get(label)
res = tf_node.evaluate_output("Geometry")
self.assertIsInstance(res, list)
self.assertEqual(len(res), 2)
# Mismatched translation list length raises ValueError
tf_node.resolve = lambda label: {
"Geometry": [c1, c2],
"Translation": [(1.0, 0.0, 0.0)], # length 1 broadcasts
"Rotation": (0.0, 0.0, 0.0),
"Scale": (1.0, 1.0, 1.0),
}.get(label)
res_broadcast = tf_node.evaluate_output("Geometry")
self.assertEqual(len(res_broadcast), 2)
tf_node.resolve = lambda label: {
"Geometry": [c1, c2],
"Translation": [(1.0, 0.0, 0.0), (2.0, 0.0, 0.0), (3.0, 0.0, 0.0)], # length 3 != 2
"Rotation": (0.0, 0.0, 0.0),
"Scale": (1.0, 1.0, 1.0),
}.get(label)
with self.assertRaises(ValueError):
tf_node.evaluate_output("Geometry")
def test_drop_geometry_list_matching(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry
so1 = SlicingObject()
so1.name = "Part1"
so1.mesh = np.zeros((1, 4, 3), dtype=np.float64)
so1.mesh[:, :3, 2] = 5.0
so2 = SlicingObject()
so2.name = "Part2"
so2.mesh = np.zeros((1, 4, 3), dtype=np.float64)
so2.mesh[:, :3, 2] = 10.0
c1 = SlicingCollection([], "Part1")
c1.objects = [so1]
c2 = SlicingCollection([], "Part2")
c2.objects = [so2]
node = SLICENODE_DropGeometry()
# Case 1: list of meshes + list of Z matching 1-to-1
node.resolve = lambda label: [c1, c2] if label == "Mesh" else [0.0, 2.0]
res = node.evaluate_output("Mesh")
self.assertIsInstance(res, list)
self.assertEqual(len(res), 2)
trans = node.evaluate_output("Translation")
self.assertEqual(trans, [(0.0, 0.0, -5.0), (0.0, 0.0, -8.0)])
# Case 2: list of meshes + single Z (broadcast)
node.resolve = lambda label: [c1, c2] if label == "Mesh" else 1.0
trans_broadcast = node.evaluate_output("Translation")
self.assertEqual(trans_broadcast, [(0.0, 0.0, -4.0), (0.0, 0.0, -9.0)])
# Case 3: list of meshes + list of 1 Z (broadcast)
node.resolve = lambda label: [c1, c2] if label == "Mesh" else [1.0]
trans_broadcast_list1 = node.evaluate_output("Translation")
self.assertEqual(trans_broadcast_list1, [(0.0, 0.0, -4.0), (0.0, 0.0, -9.0)])
# Case 4: length mismatch raises ValueError
node.resolve = lambda label: [c1, c2] if label == "Mesh" else [1.0, 2.0, 3.0]
with self.assertRaises(ValueError):
node.evaluate_output("Mesh")
# Case 5: single mesh + list of multiple Z raises ValueError
node.resolve = lambda label: c1 if label == "Mesh" else [1.0, 2.0]
with self.assertRaises(ValueError):
node.evaluate_output("Mesh")
def test_rename_mesh_list_matching(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_RenameMesh
c1 = SlicingCollection([], "Old1")
c2 = SlicingCollection([], "Old2")
node = SLICENODE_RenameMesh()
# Case 1: list of meshes + list of names matching 1-to-1
node.resolve = lambda label: [c1, c2] if label == "Geometry" else ["New1", "New2"]
res = node.evaluate_output("Geometry")
self.assertEqual([r.name for r in res], ["New1", "New2"])
# Case 2: list of meshes + single name (broadcast)
node.resolve = lambda label: [c1, c2] if label == "Geometry" else "SameName"
res = node.evaluate_output("Geometry")
self.assertEqual([r.name for r in res], ["SameName", "SameName"])
# Case 3: length mismatch raises ValueError
node.resolve = lambda label: [c1, c2] if label == "Geometry" else ["A", "B", "C"]
with self.assertRaises(ValueError):
node.evaluate_output("Geometry")
# Case 4: single mesh + multiple names raises ValueError
node.resolve = lambda label: c1 if label == "Geometry" else ["A", "B"]
with self.assertRaises(ValueError):
node.evaluate_output("Geometry")
def test_path_operations_list_matching(self):
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_PathOperations
p1 = SlicePaths([np.array([[0, 0, 0], [10, 0, 0]], dtype=np.int32)])
p2 = SlicePaths([np.array([[0, 10, 0], [10, 10, 0]], dtype=np.int32)])
p3 = SlicePaths([np.array([[0, 20, 0], [10, 20, 0]], dtype=np.int32)])
node = SLICENODE_PathOperations()
in0 = DummySocket("Paths", "SLICESOCK_Paths")
in1 = DummySocket("Paths", "SLICESOCK_Paths")
node.inputs = DummySocketCollection([in0, in1])
in0.is_linked = True
in1.is_linked = True
from unittest.mock import patch
# Case 1: list of Paths A + list of Paths B matching 1-to-1
with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[[p1, p2], [p3, p3]]):
res = node.evaluate_output("Paths")
self.assertIsInstance(res, list)
self.assertEqual(len(res), 2)
# Case 2: list of Paths A + single Path B (broadcast)
with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[[p1, p2], p3]):
res = node.evaluate_output("Paths")
self.assertIsInstance(res, list)
self.assertEqual(len(res), 2)
# Case 3: length mismatch raises ValueError
with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[[p1, p2], [p3, p3, p3]]):
with self.assertRaises(ValueError):
node.evaluate_output("Paths")
# Case 4: single Path A + multiple Paths B raises ValueError
with patch("NodeSlicer.infra.blender.blender_slice_nodes_generic.resolve_input", side_effect=[p1, [p2, p3]]):
with self.assertRaises(ValueError):
node.evaluate_output("Paths")
if __name__ == '__main__':
unittest.main()