import sys import unittest from unittest.mock import MagicMock, patch import numpy as np from pathlib import Path # Add workspace directory to sys.path to resolve root package name (NodeSlicer) PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT.parent) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT.parent)) if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) # Define dummy classes to avoid metaclass conflicts class DummyNodeSocket: pass class DummyNode: pass class DummyObject: def __init__(self, name="TestObj"): self.name = name class DummyMesh: pass class DummyNodeSocketShader: pass class DummyNodeCustomGroup: pass class DummyNodeTree: pass class DummyOperator: pass class DummyMenu: pass class DummyPanel: pass class DummyPropertyGroup: pass mock_types = MagicMock() mock_types.NodeSocket = DummyNodeSocket mock_types.Node = DummyNode mock_types.Object = DummyObject mock_types.Mesh = DummyMesh mock_types.NodeSocketShader = DummyNodeSocketShader mock_types.NodeCustomGroup = DummyNodeCustomGroup mock_types.NodeTree = DummyNodeTree mock_types.Operator = DummyOperator mock_types.Menu = DummyMenu mock_types.Panel = DummyPanel mock_types.PropertyGroup = DummyPropertyGroup mock_bpy = MagicMock() mock_bpy.context.scene = MagicMock() mock_bpy.context.scene.unit_settings.scale_length = 0.001 mock_bpy.types = mock_types mock_previews = MagicMock() mock_previews.ImagePreviewCollection = MagicMock 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'] = mock_previews sys.modules['bpy.app'] = MagicMock() sys.modules['bpy.app.handlers'] = MagicMock() # Import cudaslicer loader and NodeSlicer modules from NodeSlicer.core.cudaslicer_loader import import_cudaslicer from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject from NodeSlicer.core.slice import SlicePaths from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_CudaSliceMesh class TestCudaSlicer(unittest.TestCase): def setUp(self): self.cudaslicer = import_cudaslicer() def test_cudaslicer_loader(self): self.assertIsNotNone(self.cudaslicer, "cudaslicer loader should successfully import cudaslicer") self.assertTrue(self.cudaslicer.is_available(), "cudaslicer backend should be available") def test_cudaslicer_mesh_slicing(self): # Create a simple 10x10x10 cube tris = np.array([ [[0,0,0], [10,0,0], [10,10,0]], [[0,0,0], [10,10,0], [0,10,0]], [[0,0,10], [10,10,10], [10,0,10]], [[0,0,10], [0,10,10], [10,10,10]], [[0,0,0], [0,10,0], [0,10,10]], [[0,0,0], [0,10,10], [0,0,10]], [[10,0,0], [10,10,10], [10,10,0]], [[10,0,0], [10,0,10], [10,10,10]], [[0,0,0], [10,0,10], [10,0,0]], [[0,0,0], [0,0,10], [10,0,10]], [[0,10,0], [10,10,0], [10,10,10]], [[0,10,0], [10,10,10], [0,10,10]], ], dtype=np.float32) layers = self.cudaslicer.slice_mesh_and_offset( mesh_or_triangles=tris, layer_height=1.0, first_layer_height=0.5, perimeters_count=2, layer_width=0.45, pitch_mm=0.1, pad_mm=1.5, backend="auto" ) self.assertGreater(len(layers), 0, "Should generate sliced layers") self.assertAlmostEqual(layers[0].z, 0.5, places=4) self.assertGreater(len(layers[0].perimeters), 0) def test_cuda_slice_mesh_node_evaluation(self): # Create dummy SlicingObject so = SlicingObject() so.name = "TestCube" # Build (12, 4, 3) mesh representation tris = np.array([ [[0,0,0], [10,0,0], [10,10,0]], [[0,0,0], [10,10,0], [0,10,0]], [[0,0,10], [10,10,10], [10,0,10]], [[0,0,10], [0,10,10], [10,10,10]], [[0,0,0], [0,10,0], [0,10,10]], [[0,0,0], [0,10,10], [0,0,10]], [[10,0,0], [10,10,10], [10,10,0]], [[10,0,0], [10,0,10], [10,10,10]], [[0,0,0], [10,0,10], [10,0,0]], [[0,0,0], [0,0,10], [10,0,10]], [[0,10,0], [10,10,0], [10,10,10]], [[0,10,0], [10,10,10], [0,10,10]], ], dtype=np.float64) normals = np.zeros((12, 1, 3), dtype=np.float64) so.mesh = np.concatenate([tris, normals], axis=1) node = SLICENODE_CudaSliceMesh() node.name = "CUDA_Slice_Mesh" # Mock resolve method resolve_map = { "Mesh": so, "Layer Height": 1.0, "First Layer Height": 0.5, "Perimeters Count": 2, "Layer Width": 0.45, "Pitch": 0.10, "Pad": 1.5, "Rotation": (0.0, 0.0, 0.0), } node.resolve = lambda key: resolve_map.get(key) node.backend = "AUTO" cache = {} perimeters_paths = node.evaluate_output("Perimeters", cache=cache) self.assertIsInstance(perimeters_paths, SlicePaths) self.assertGreater(len(perimeters_paths), 0) self.assertEqual(perimeters_paths.layer_height, 1.0) self.assertEqual(perimeters_paths.first_layer_height, 0.5) # First layer paths should have adjusted extrusion rate z_first = 0.5 for i, p in enumerate(perimeters_paths): if len(p) > 0 and abs(p[0, 2] - z_first) < 1e-4: self.assertTrue(np.allclose(perimeters_paths.extrusion_rate[i], 0.5)) # Test infill contours evaluation (should reuse cache) infill_paths = node.evaluate_output("Infill Contours", cache=cache) self.assertIsInstance(infill_paths, SlicePaths) self.assertGreater(len(infill_paths), 0) def test_cuda_slice_mesh_with_rotation(self): so = SlicingObject() so.name = "RotatedCube" tris = np.array([ [[0,0,0], [10,0,0], [10,10,0]], [[0,0,0], [10,10,0], [0,10,0]], [[0,0,10], [10,10,10], [10,0,10]], [[0,0,10], [0,10,10], [10,10,10]], [[0,0,0], [0,10,0], [0,10,10]], [[0,0,0], [0,10,10], [0,0,10]], [[10,0,0], [10,10,10], [10,10,0]], [[10,0,0], [10,0,10], [10,10,10]], [[0,0,0], [10,0,10], [10,0,0]], [[0,0,0], [0,0,10], [10,0,10]], [[0,10,0], [10,10,0], [10,10,10]], [[0,10,0], [10,10,10], [0,10,10]], ], dtype=np.float64) normals = np.zeros((12, 1, 3), dtype=np.float64) so.mesh = np.concatenate([tris, normals], axis=1) node = SLICENODE_CudaSliceMesh() resolve_map = { "Mesh": so, "Layer Height": 1.0, "First Layer Height": 1.0, "Perimeters Count": 1, "Layer Width": 0.45, "Pitch": 0.10, "Pad": 1.5, "Rotation": (0.0, 0.0, float(np.radians(45.0))), } node.resolve = lambda key: resolve_map.get(key) node.backend = "AUTO" paths = node.evaluate_output("Perimeters") self.assertIsInstance(paths, SlicePaths) self.assertGreater(len(paths), 0) def test_cuda_slice_mesh_backends(self): so = SlicingObject() tris = np.array([ [[0,0,0], [10,0,0], [10,10,0]], [[0,0,0], [10,10,0], [0,10,0]], [[0,0,10], [10,10,10], [10,0,10]], [[0,0,10], [0,10,10], [10,10,10]], [[0,0,0], [0,10,0], [0,10,10]], [[0,0,0], [0,10,10], [0,0,10]], [[10,0,0], [10,10,10], [10,10,0]], [[10,0,0], [10,0,10], [10,10,10]], [[0,0,0], [10,0,10], [10,0,0]], [[0,0,0], [0,0,10], [10,0,10]], [[0,10,0], [10,10,0], [10,10,10]], [[0,10,0], [10,10,10], [0,10,10]], ], dtype=np.float64) so.mesh = np.concatenate([tris, np.zeros((12, 1, 3))], axis=1) node = SLICENODE_CudaSliceMesh() resolve_map = { "Mesh": so, "Layer Height": 1.0, "Perimeters Count": 1, "Layer Width": 0.45, "Pitch": 0.10, "Pad": 1.5, } node.resolve = lambda key: resolve_map.get(key) for b in ["AUTO", "CPU"]: node.backend = b paths = node.evaluate_output("Perimeters") self.assertGreater(len(paths), 0, f"Backend {b} should produce paths") def test_cuda_slice_mesh_empty(self): node = SLICENODE_CudaSliceMesh() node.resolve = lambda key: None paths = node.evaluate_output("Perimeters") self.assertIsInstance(paths, SlicePaths) self.assertEqual(len(paths), 0) def test_cuda_slice_mesh_evaluation_cleaned(self): so = SlicingObject() so.name = "TestCube" tris = np.array([ [[0,0,0], [10,0,0], [10,10,0]], [[0,0,0], [10,10,0], [0,10,0]], [[0,0,10], [10,10,10], [10,0,10]], [[0,0,10], [0,10,10], [10,10,10]], [[0,0,0], [0,10,0], [0,10,10]], [[0,0,0], [0,10,10], [0,0,10]], [[10,0,0], [10,10,10], [10,10,0]], [[10,0,0], [10,0,10], [10,10,10]], [[0,0,0], [10,0,10], [10,0,0]], [[0,0,0], [0,0,10], [10,0,10]], [[0,10,0], [10,10,0], [10,10,10]], [[0,10,0], [10,10,10], [0,10,10]], ], dtype=np.float64) so.mesh = np.concatenate([tris, np.zeros((12, 1, 3))], axis=1) node = SLICENODE_CudaSliceMesh() resolve_map = { "Mesh": so, "Layer Height": 1.0, "First Layer Height": 0.5, "Perimeters Count": 2, "Layer Width": 0.45, "Pitch": 0.10, "Pad": 1.5, "Rotation": (0.0, 0.0, 0.0), } node.resolve = lambda key: resolve_map.get(key) node.backend = "AUTO" cache = {} perim_paths = node.evaluate_output("Perimeters", cache=cache) infill_contours = node.evaluate_output("Infill Contours", cache=cache) self.assertIsInstance(perim_paths, SlicePaths) self.assertIsInstance(infill_contours, SlicePaths) self.assertGreater(len(perim_paths), 0) self.assertGreater(len(infill_contours), 0) def test_cuda_slice_mesh_timing_in_label(self): node = SLICENODE_CudaSliceMesh() node.name = "CUDA_Slice_Mesh" # Initially no execution time recorded -> default label self.assertEqual(node.draw_label(), "CUDA Slice Mesh") # Test simulated timing values and label formatting node.execution_time_ms = 10.2 self.assertEqual(node.draw_label(), "CUDA Slice Mesh (10ms)") node.execution_time_ms = 35.8 self.assertEqual(node.draw_label(), "CUDA Slice Mesh (36ms)") node.execution_time_ms = 4.2 self.assertEqual(node.draw_label(), "CUDA Slice Mesh (4.2ms)") node.execution_time_ms = 0.35 self.assertEqual(node.draw_label(), "CUDA Slice Mesh (0.35ms)") # Custom user label support node.label = "My Benchy Slicer" node.execution_time_ms = 12.0 self.assertEqual(node.draw_label(), "My Benchy Slicer (12ms)") # After real evaluation, timing should be automatically measured and formatted node.label = "" so = SlicingObject() so.name = "TestCube" tris = np.array([ [[0,0,0], [10,0,0], [10,10,0]], [[0,0,0], [10,10,0], [0,10,0]], [[0,0,10], [10,10,10], [10,0,10]], [[0,0,10], [0,10,10], [10,10,10]], [[0,0,0], [0,10,0], [0,10,10]], [[0,0,0], [0,10,10], [0,0,10]], [[10,0,0], [10,10,10], [10,10,0]], [[10,0,0], [10,10,0], [10,10,10]], [[0,0,0], [10,0,10], [10,0,0]], [[0,0,0], [0,0,10], [10,0,10]], [[0,10,0], [10,10,0], [10,10,10]], [[0,10,0], [10,10,10], [0,10,10]], ], dtype=np.float64) so.mesh = np.concatenate([tris, np.zeros((12, 1, 3))], axis=1) resolve_map = { "Mesh": so, "Layer Height": 1.0, "First Layer Height": 0.5, "Perimeters Count": 2, "Layer Width": 0.45, "Pitch": 0.10, "Pad": 1.5, "Rotation": (0.0, 0.0, 0.0), } node.resolve = lambda key: resolve_map.get(key) node.backend = "AUTO" node.evaluate_output("Perimeters") self.assertGreater(node.execution_time_ms, 0.0) label = node.draw_label() self.assertTrue(label.startswith("CUDA Slice Mesh (") and label.endswith("ms)"), f"Label was: {label}") if __name__ == "__main__": unittest.main()