144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
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()
|