nodeslicer/tests/test_lists.py

933 lines
36 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,
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_reject_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 rejects list
self.assertFalse(self.tree.valid_link(list_out, p1))
def test_valid_link_seam_placement_cutting_mesh_rejects_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 rejects list
self.assertFalse(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 rejects lists -> rejected
seam_mesh_in = DummySocket("Mesh", "SLICESOCK_Mesh")
seam_mesh_in.is_output = False
seam_mesh_in.node = DummyNode("SLICENODE_SeamPlacement")
self.assertFalse(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})
if __name__ == '__main__':
unittest.main()