import os import tempfile import numpy as np import unittest from unittest.mock import MagicMock import sys from pathlib import Path # Add workspace directory to sys.path PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.append(str(PROJECT_ROOT.parent)) # Mock bpy and dependencies for unit testing class DummyNodeSocket: pass class DummyNode: pass class DummyObject: pass class DummyMesh: pass mock_types = MagicMock() mock_types.NodeSocket = DummyNodeSocket mock_types.Node = DummyNode mock_types.Object = DummyObject mock_types.Mesh = DummyMesh mock_bpy = MagicMock() mock_bpy.context.scene = MagicMock() mock_bpy.context.scene.unit_settings.scale_length = 0.001 mock_bpy.types = mock_types mock_bpy.path.abspath = lambda path: path.replace("//", "/abs/") sys.modules['bpy'] = mock_bpy sys.modules['bpy.types'] = mock_types sys.modules['bpy.props'] = MagicMock() sys.modules['bpy.utils'] = MagicMock() from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject from NodeSlicer.core.pdf_exporter import ( extract_unique_top_view_edges, extract_lineart_edges, export_wireframe_to_pdf, MM_TO_PT ) class TestPdfExporter(unittest.TestCase): def test_extract_edges_from_slicing_object(self): so = SlicingObject() # Create a simple triangle in 3D: (0,0,0), (10,0,0), (0,10,0) with normal (0,0,1) tri = np.array([ [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0] # Normal ], dtype=np.float64) so.mesh = np.array([tri]) edges = extract_unique_top_view_edges(so) self.assertEqual(len(edges), 3) def test_extract_edges_deduplication(self): so = SlicingObject() # Two triangles sharing an edge (10,0,0) - (0,10,0) tri1 = np.array([ [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0] ], dtype=np.float64) tri2 = np.array([ [10.0, 0.0, 0.0], [10.0, 10.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0] ], dtype=np.float64) so.mesh = np.array([tri1, tri2]) edges = extract_unique_top_view_edges(so) # Total edges: 3 + 3 - 1 shared = 5 unique edges self.assertEqual(len(edges), 5) def test_extract_edges_filters_vertical_lines(self): so = SlicingObject() # Vertical wall in top view (x, y remain constant, z changes) -> degenerate 2D line segment tri = np.array([ [0.0, 0.0, 0.0], [0.0, 0.0, 10.0], [10.0, 0.0, 0.0], [0.0, 1.0, 0.0] ], dtype=np.float64) so.mesh = np.array([tri]) edges = extract_unique_top_view_edges(so) # Vertical edge (0,0,0)-(0,0,10) becomes 2D length 0 and should be filtered out self.assertTrue(len(edges) < 3) def test_extract_lineart_edges(self): so = SlicingObject() # Create 2 triangles sharing an edge at 90 degree angle (crease) tri1 = np.array([ [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0] # Normal up ], dtype=np.float64) tri2 = np.array([ [10.0, 0.0, 0.0], [10.0, 0.0, 10.0], [0.0, 10.0, 0.0], [1.0, 0.0, 0.0] # Normal right (90 deg to tri1) ], dtype=np.float64) so.mesh = np.array([tri1, tri2]) # At max_angle_deg=30, the 90 deg crease edge should be marked lineart_30 = extract_lineart_edges(so, max_angle_deg=30.0) self.assertTrue(len(lineart_30) > 0) # At max_angle_deg=100, crease edge (90 deg) is below threshold, but boundary edges remain lineart_100 = extract_lineart_edges(so, max_angle_deg=100.0) self.assertTrue(len(lineart_100) > 0) def test_export_pdf_true_1_to_1_scale(self): edges_2d = np.array([ [[0.0, 0.0], [100.0, 0.0]], [[100.0, 0.0], [100.0, 100.0]], [[100.0, 100.0], [0.0, 100.0]], [[0.0, 100.0], [0.0, 0.0]] ], dtype=np.float64) margin_mm = 10.0 with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_true_scale.pdf") export_wireframe_to_pdf( edges_2d, pdf_path, margin_mm=margin_mm, stroke_color=(0.1, 0.2, 0.3), bg_color=(0.9, 0.95, 1.0) ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read() expected_w = (100.0 + 2 * margin_mm) * MM_TO_PT expected_h = (100.0 + 2 * margin_mm) * MM_TO_PT mediabox_str = f"/MediaBox [0 0 {expected_w:.2f} {expected_h:.2f}]".encode("utf-8") self.assertTrue(content.startswith(b"%PDF-1.4")) self.assertIn(mediabox_str, content) self.assertIn(b"0.900 0.950 1.000 rg", content) # Background color self.assertIn(b"0.100 0.200 0.300 RG", content) # Wireframe color self.assertIn(b"/GS25 gs", content) self.assertIn(b"/GS50 gs", content) self.assertIn(b"/GS100 gs", content) self.assertTrue(content.endswith(b"%%EOF\n")) def test_export_pdf_visualization_styles(self): so = SlicingObject() tri = np.array([ [0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [0.0, 100.0, 0.0], [0.0, 0.0, 1.0] ], dtype=np.float64) so.mesh = np.array([tri]) edges_2d = extract_unique_top_view_edges(so) for style in ['WIREFRAME', 'LINEART', 'OUTER_CONTOUR']: with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, f"test_{style}.pdf") export_wireframe_to_pdf( edges_2d, pdf_path, style=style, crease_angle=30.0, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read() self.assertTrue(content.startswith(b"%PDF-1.4")) self.assertTrue(content.endswith(b"%%EOF\n")) if __name__ == '__main__': unittest.main()