import numpy as np import unittest from unittest.mock import MagicMock import sys from pathlib import Path # Add workspace directory to sys.path to resolve root package name (NodeSlicer) PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.append(str(PROJECT_ROOT.parent)) # Define dummy classes to avoid metaclass conflicts class DummyNodeSocket: pass class DummyNode: pass class DummyObject: pass 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 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() # Import the actual functions to test from NodeSlicer.core.slice import segments_to_slice, link_segments class TestSlice(unittest.TestCase): def test_link_segments_closed(self): # Closed triangle: A(0,0,10) -> B(10,0,10) -> C(0,10,10) -> A(0,0,10) segments = np.array([ [[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]], [[10.0, 0.0, 10.0], [0.0, 10.0, 10.0]], [[0.0, 10.0, 10.0], [0.0, 0.0, 10.0]] ]) linked = link_segments(segments) self.assertEqual(len(linked), 1) path = linked[0] # Should start and end at the same point (A) self.assertTrue(np.allclose(path[0], path[-1])) # Verify segments_to_slice closes it sliced = segments_to_slice(segments) self.assertEqual(len(sliced), 1) sliced_path = sliced[0] self.assertTrue(np.allclose(sliced_path[0], sliced_path[-1])) def test_link_segments_open(self): # Open path: A(0,0,10) -> B(10,0,10) -> C(0,10,10) (no C->A segment) segments = np.array([ [[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]], [[10.0, 0.0, 10.0], [0.0, 10.0, 10.0]] ]) linked = link_segments(segments) self.assertEqual(len(linked), 1) path = linked[0] # Should NOT start and end at the same point self.assertFalse(np.allclose(path[0], path[-1])) # Verify segments_to_slice does NOT close it sliced = segments_to_slice(segments) self.assertEqual(len(sliced), 1) sliced_path = sliced[0] self.assertFalse(np.allclose(sliced_path[0], sliced_path[-1]), f"Path was incorrectly closed: {sliced_path}") def test_buffer_curve_basic(self): # Open path (line segment from (0,0,10) to (10,0,10)) path = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Buffer curve with distance 1.0 buffered = slice_paths.buffer_curve(1.0, cap_style='flat') # Output should be a SlicePaths instance self.assertIsInstance(buffered, SlicePaths) self.assertEqual(len(buffered), 1) # Check that the path is now closed (buffered area boundary) buffered_path = buffered[0] self.assertTrue(np.allclose(buffered_path[0], buffered_path[-1])) # Flat cap with distance 1.0 should result in a rectangle: # width = 10 (from 0 to 10), height = 2 (from -1 to 1) -> area = 20 # Let's compute 2D polygon area of the resulting path xs = buffered_path[:, 0] ys = buffered_path[:, 1] # Shoelace formula for area area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) self.assertAlmostEqual(area, 20.0, places=3) # Check Z coordinate is preserved self.assertTrue(np.allclose(buffered_path[:, 2], 10.0)) def test_buffer_curve_cap_styles(self): path = np.array([[0.0, 0.0, 5.0], [10.0, 0.0, 5.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Round cap buffered_round = slice_paths.buffer_curve(1.0, cap_style='round') # Flat cap buffered_flat = slice_paths.buffer_curve(1.0, cap_style='flat') # Square cap buffered_square = slice_paths.buffer_curve(1.0, cap_style='square') # Area comparison: flat (20.0) < round (20.0 + pi) < square (24.0) def get_area(p): xs, ys = p[:, 0], p[:, 1] return 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) area_flat = get_area(buffered_flat[0]) area_round = get_area(buffered_round[0]) area_square = get_area(buffered_square[0]) self.assertAlmostEqual(area_flat, 20.0, places=3) self.assertAlmostEqual(area_square, 24.0, places=3) self.assertGreater(area_round, area_flat) self.assertLess(area_round, area_square) def test_buffer_curve_single_sided(self): path = np.array([[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) buffered = slice_paths.buffer_curve(1.0, cap_style='flat', single_sided=True) # For a single sided flat cap buffer, the rectangle area is distance * length = 1.0 * 10.0 = 10.0 xs, ys = buffered[0][:, 0], buffered[0][:, 1] area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) self.assertAlmostEqual(area, 10.0, places=3) def test_offset_curve_basic(self): # Open path (line segment from (0,0,10) to (10,0,10)) path = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Offset curve by 1.0 (should shift it to (0, 1, 10) -> (10, 1, 10)) offset = slice_paths.offset_curve(1.0) self.assertIsInstance(offset, SlicePaths) self.assertEqual(len(offset), 1) offset_path = offset[0] # Should remain an open path (first and last coordinate not equal) self.assertFalse(np.allclose(offset_path[0], offset_path[-1])) # Verify the shifted coordinates self.assertTrue(np.allclose(offset_path[:, 1], 1.0)) self.assertTrue(np.allclose(offset_path[:, 2], 10.0)) self.assertTrue(np.allclose(offset_path[0, 0], 0.0)) self.assertTrue(np.allclose(offset_path[1, 0], 10.0)) def test_offset_curve_closed_loop(self): # Closed square path: (0,0) -> (0,10) -> (10,10) -> (10,0) -> (0,0) at Z=5.0 path = np.array([ [0.0, 0.0, 5.0], [0.0, 10.0, 5.0], [10.0, 10.0, 5.0], [10.0, 0.0, 5.0], [0.0, 0.0, 5.0] ]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Offset curve by +1.0 # Positive distance in a clockwise loop should shift it inward (to a square of size 8.0) offset_in = slice_paths.offset_curve(1.0, join_style='mitre') self.assertIsInstance(offset_in, SlicePaths) self.assertEqual(len(offset_in), 1) offset_path = offset_in[0] # Should remain perfectly closed (first and last coordinate equal) self.assertTrue(np.allclose(offset_path[0], offset_path[-1]), f"Offset path was not closed: {offset_path}") # Calculate the area of the offset path xs, ys = offset_path[:, 0], offset_path[:, 1] area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) self.assertAlmostEqual(area, 64.0, places=3) # Check Z coordinate is preserved self.assertTrue(np.allclose(offset_path[:, 2], 5.0)) if __name__ == '__main__': unittest.main()