352 lines
12 KiB
Python
352 lines
12 KiB
Python
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()
|
|
|