nodeslicer/tests/test_cudaslicer.py
2026-09-26 16:21:17 +03:00

288 lines
10 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)
if __name__ == "__main__":
unittest.main()