nodeslicer/tests/test_slice.py
2026-07-02 17:58:55 +03:00

248 lines
9.6 KiB
Python

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))
def test_get_infill_mask(self):
# Two overlapping closed squares on the same layer Z=3.0
# Square 1: (0,0) to (10,10)
sq1 = np.array([
[0.0, 0.0, 3.0],
[0.0, 10.0, 3.0],
[10.0, 10.0, 3.0],
[10.0, 0.0, 3.0],
[0.0, 0.0, 3.0]
])
# Square 2: (5,5) to (15,15) - overlaps from (5,5) to (10,10)
sq2 = np.array([
[5.0, 5.0, 3.0],
[5.0, 15.0, 3.0],
[15.0, 15.0, 3.0],
[15.0, 5.0, 3.0],
[5.0, 5.0, 3.0]
])
from NodeSlicer.core.slice import SlicePaths
# Both in group 0
slice_paths = SlicePaths([sq1, sq2], groups=[0, 0])
mask = slice_paths.get_infill_mask()
self.assertIsInstance(mask, SlicePaths)
self.assertEqual(len(mask), 1) # Intersection of two squares should be one square (5,5) to (10,10)
mask_path = mask[0]
self.assertTrue(np.allclose(mask_path[0], mask_path[-1]))
# Area of intersection should be 5 * 5 = 25
xs, ys = mask_path[:, 0], mask_path[:, 1]
area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1)))
self.assertAlmostEqual(area, 25.0, places=3)
self.assertTrue(np.allclose(mask_path[:, 2], 3.0))
if __name__ == '__main__':
unittest.main()