1208 lines
48 KiB
Python
1208 lines
48 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
|
|
from NodeSlicer.core.nodeslicer.tree import SLICETREE_NodeTree
|
|
|
|
|
|
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 TestTreeValidLinkRules(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tree = SLICETREE_NodeTree()
|
|
|
|
def test_valid_link_start_print_rejects_list(self):
|
|
list_out = DummySocket("Gcode", "NodeSocketString", display_shape="LIST")
|
|
start_print = DummyNode("SLICENODE_StartPrint")
|
|
sp_in = DummySocket("Gcode", "NodeSocketString")
|
|
start_print.add_in(sp_in)
|
|
|
|
# Connection should be rejected
|
|
self.assertFalse(self.tree.valid_link(list_out, sp_in))
|
|
|
|
# Standard non-list connection should be allowed
|
|
normal_out = DummySocket("Gcode", "NodeSocketString", display_shape="CIRCLE")
|
|
self.assertTrue(self.tree.valid_link(normal_out, sp_in))
|
|
|
|
def test_valid_link_printer_list_rejects_list(self):
|
|
list_out = DummySocket("Gcode", "NodeSocketString", display_shape="LIST")
|
|
printer_list = DummyNode("SLICENODE_PrinterList")
|
|
pl_in = DummySocket("Printer1", "NodeSocketString")
|
|
printer_list.add_in(pl_in)
|
|
|
|
self.assertFalse(self.tree.valid_link(list_out, pl_in))
|
|
|
|
def test_valid_link_path_operations_secondary_inputs_accept_list(self):
|
|
list_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
|
|
path_ops = DummyNode("SLICENODE_PathOperations")
|
|
p0 = DummySocket("Paths", "SLICESOCK_Paths")
|
|
p1 = DummySocket("Paths", "SLICESOCK_Paths")
|
|
path_ops.add_in(p0)
|
|
path_ops.add_in(p1)
|
|
|
|
# Primary input p0 accepts list
|
|
self.assertTrue(self.tree.valid_link(list_out, p0))
|
|
# Secondary input p1 also accepts list (matched index per index)
|
|
self.assertTrue(self.tree.valid_link(list_out, p1))
|
|
|
|
def test_valid_link_seam_placement_cutting_mesh_accepts_list(self):
|
|
list_mesh_out = DummySocket("Mesh", "SLICESOCK_Mesh", display_shape="LIST")
|
|
seam_node = DummyNode("SLICENODE_SeamPlacement")
|
|
p_in = DummySocket("Paths", "SLICESOCK_Paths")
|
|
m_in = DummySocket("Mesh", "SLICESOCK_Mesh")
|
|
seam_node.add_in(p_in)
|
|
seam_node.add_in(m_in)
|
|
|
|
# Secondary cutting mesh accepts list (matched index per index)
|
|
self.assertTrue(self.tree.valid_link(list_mesh_out, m_in))
|
|
|
|
|
|
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_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")
|
|
|
|
def test_valid_link_with_group_nodes(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])
|
|
|
|
grp_node = DummyGroupNode("Group", tree)
|
|
grp_in_paths = DummySocket("Paths", "SLICESOCK_Paths")
|
|
grp_in_paths.identifier = "sock_paths"
|
|
grp_in_float = DummySocket("FloatVal", "NodeSocketFloat")
|
|
grp_in_float.identifier = "sock_float"
|
|
grp_node.add_in(grp_in_paths)
|
|
grp_node.add_in(grp_in_float)
|
|
|
|
list_out = DummySocket("Paths", "SLICESOCK_Paths", display_shape="LIST")
|
|
validator = SLICETREE_NodeTree()
|
|
|
|
# Linking list to Paths group input (which accepts list): allowed
|
|
self.assertTrue(validator.valid_link(list_out, grp_in_paths))
|
|
|
|
# Linking list to FloatVal group input: rejected
|
|
self.assertFalse(validator.valid_link(list_out, grp_in_float))
|
|
|
|
|
|
class TestImplicitGetMesh(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tree = SLICETREE_NodeTree()
|
|
|
|
def test_valid_link_object_to_mesh(self):
|
|
obj_out = DummySocket("Object", "NodeSocketObject")
|
|
obj_out.is_output = True
|
|
mesh_in = DummySocket("Mesh", "SLICESOCK_Mesh")
|
|
mesh_in.is_output = False
|
|
mesh_in.node = DummyNode("SLICENODE_SliceShells")
|
|
mesh_in.node.list_socket_mapping = [("Mesh", "Paths")]
|
|
|
|
# Object output to Mesh input is valid
|
|
self.assertTrue(self.tree.valid_link(obj_out, mesh_in))
|
|
|
|
# Reverse: Mesh output to Object input is NOT valid
|
|
mesh_out = DummySocket("Mesh", "SLICESOCK_Mesh")
|
|
mesh_out.is_output = True
|
|
obj_in = DummySocket("Object", "NodeSocketObject")
|
|
obj_in.is_output = False
|
|
self.assertFalse(self.tree.valid_link(mesh_out, obj_in))
|
|
|
|
def test_valid_link_object_list_to_mesh(self):
|
|
list_obj_out = DummySocket("Objects", "NodeSocketObject", display_shape="LIST")
|
|
list_obj_out.is_output = True
|
|
mesh_in = DummySocket("Mesh", "SLICESOCK_Mesh")
|
|
mesh_in.is_output = False
|
|
mesh_in.node = DummyNode("SLICENODE_SliceShells")
|
|
mesh_in.node.list_socket_mapping = [("Mesh", "Paths")]
|
|
|
|
# Target accepts lists -> allowed
|
|
self.assertTrue(self.tree.valid_link(list_obj_out, mesh_in))
|
|
|
|
# SeamPlacement cutting mesh accepts lists
|
|
seam_mesh_in = DummySocket("Mesh", "SLICESOCK_Mesh")
|
|
seam_mesh_in.is_output = False
|
|
seam_mesh_in.node = DummyNode("SLICENODE_SeamPlacement")
|
|
self.assertTrue(self.tree.valid_link(list_obj_out, seam_mesh_in))
|
|
|
|
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):
|
|
# Single vector is not a list
|
|
single_vec = (1.0, 2.0, 3.0)
|
|
self.assertTrue(is_vector_value(single_vec))
|
|
self.assertFalse(is_input_a_list(single_vec, is_vector=True))
|
|
|
|
# List of vectors IS a list
|
|
vec_list = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)]
|
|
self.assertFalse(is_vector_value(vec_list))
|
|
self.assertTrue(is_input_a_list(vec_list, is_vector=True))
|
|
|
|
# Broadcasting single vector across main list
|
|
main = ["geom1", "geom2"]
|
|
is_list, items, (t_list,) = prepare_indexed_inputs(main, single_vec, is_vector=[True], input_names=["Translation"])
|
|
self.assertTrue(is_list)
|
|
self.assertEqual(t_list, [(1.0, 2.0, 3.0), (1.0, 2.0, 3.0)])
|
|
|
|
# Matching list of vectors index-per-index
|
|
is_list, items, (t_list,) = prepare_indexed_inputs(main, vec_list, 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_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()
|