import unittest import numpy as np import sys from pathlib import Path from unittest.mock import MagicMock PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.append(str(PROJECT_ROOT.parent)) # Setup dummy bpy 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.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() from NodeSlicer.core.slice import SlicePaths, is_closed_path class TestSimplifyPaths(unittest.TestCase): def test_colinear_points_elimination(self): # A straight line segment with 50 redundant colinear points from (0,0,0) to (100,0,0) xs = np.linspace(0, 100, 50) ys = np.zeros_like(xs) zs = np.zeros_like(xs) line = np.column_stack([xs, ys, zs]) sp = SlicePaths([line]) res = sp.simplify(0.01) self.assertEqual(len(res), 1) self.assertEqual(len(res[0]), 2) np.testing.assert_allclose(res[0][0], [0.0, 0.0, 0.0]) np.testing.assert_allclose(res[0][-1], [100.0, 0.0, 0.0]) def test_closed_square_with_subdivisions(self): # 100 points on each edge of a 20x20 square (400 points total) edge1 = np.column_stack([np.linspace(-10, 10, 100), np.full(100, -10.0), np.zeros(100)]) edge2 = np.column_stack([np.full(100, 10.0), np.linspace(-10, 10, 100), np.zeros(100)]) edge3 = np.column_stack([np.linspace(10, -10, 100), np.full(100, 10.0), np.zeros(100)]) edge4 = np.column_stack([np.full(100, -10.0), np.linspace(10, -10, 100), np.zeros(100)]) square_dense = np.concatenate([edge1[:-1], edge2[:-1], edge3[:-1], edge4], axis=0) self.assertTrue(is_closed_path(square_dense)) sp = SlicePaths([square_dense]) res = sp.simplify(0.01, preserve_topology=True) self.assertEqual(len(res), 1) self.assertTrue(is_closed_path(res[0])) # A clean 20x20 square should reduce to 5 vertices (4 corners + loop closure) self.assertEqual(len(res[0]), 5) np.testing.assert_allclose(res[0][0], res[0][-1]) def test_closed_loop_topology_preservation(self): # Small circle with diameter 0.02mm, tolerance 0.05mm # If preserve_topology=True, it should not collapse into < 4 vertices (stays a valid triangle loop) theta = np.linspace(0, 2 * np.pi, 20) r = 0.01 circle = np.column_stack([r * np.cos(theta), r * np.sin(theta), np.zeros(20)]) circle[-1] = circle[0] sp = SlicePaths([circle]) res = sp.simplify(0.05, preserve_topology=True) self.assertEqual(len(res), 1) self.assertTrue(is_closed_path(res[0])) self.assertGreaterEqual(len(res[0]), 4) def test_attribute_transfer(self): # Path with extrusion_rate and feedrate attributes xs = np.linspace(0, 100, 50) ys = np.zeros_like(xs) zs = np.zeros_like(xs) line = np.column_stack([xs, ys, zs]) ext = [np.linspace(1.0, 2.0, 50)] feed = [np.linspace(1000.0, 2000.0, 50)] sp = SlicePaths([line], extrusion_rate=ext, feedrate=feed) res = sp.simplify(0.01) self.assertEqual(len(res), 1) self.assertEqual(len(res[0]), 2) self.assertEqual(len(res.extrusion_rate[0]), 2) self.assertEqual(len(res.feedrate[0]), 2) np.testing.assert_allclose(res.extrusion_rate[0], [1.0, 2.0], atol=1e-3) np.testing.assert_allclose(res.feedrate[0], [1000.0, 2000.0], atol=1e-3) def test_batch_performance(self): # 200 layers, 5 paths per layer = 1,000 paths (300,000 vertices) paths = [] for z in np.linspace(0, 40, 200): for r in [5.0, 10.0, 15.0, 20.0, 25.0]: theta = np.linspace(0, 2 * np.pi, 300) x = r * np.cos(theta) + np.random.normal(0, 0.005, 300) y = r * np.sin(theta) + np.random.normal(0, 0.005, 300) x[-1] = x[0] y[-1] = y[0] paths.append(np.column_stack([x, y, np.full(300, z)])) sp = SlicePaths(paths) import time t0 = time.perf_counter() res = sp.simplify(0.05) duration = time.perf_counter() - t0 self.assertEqual(len(res), 1000) self.assertLess(duration, 0.1, f"Batch simplify of 300,000 points took {duration:.4f}s (must be < 0.1s)") if __name__ == '__main__': unittest.main()