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, 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_export_pdf_true_1_to_1_scale(self): # Setup 100mm x 100mm 2D square 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 # Main grid step for 100mm is 10mm. Bounds: [0, 100], [0, 100]. # Total width = 100 + 2*10 = 120mm. Expected pt = 120 * MM_TO_PT = 340.16 pt. 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")) if __name__ == '__main__': unittest.main()