nodeslicer/tests/test_simplify.py
2026-10-09 17:18:23 +03:00

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()