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 class DummyBase: pass mock_types = MagicMock() mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {}) mock_types.Node = type("Node", (DummyBase,), {}) mock_types.NodeCustomGroup = type("NodeCustomGroup", (DummyBase,), {}) mock_types.NodeTree = type("NodeTree", (DummyBase,), {}) mock_types.PropertyGroup = type("PropertyGroup", (DummyBase,), {}) mock_types.Operator = type("Operator", (DummyBase,), {}) mock_types.Panel = type("Panel", (DummyBase,), {}) 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() sys.modules['bpy.utils.previews'] = MagicMock() sys.modules['bpy.app'] = MagicMock() sys.modules['bpy.app.handlers'] = 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, export_mesh_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() minor_step = 1.0 # main_step 10.0 / 10 total_margin_mm = margin_mm + 2.0 * minor_step # 100mm geometry padded by 1 minor_step clearance expands grid to 120mm expected_w = (120.0 + 2 * total_margin_mm) * MM_TO_PT expected_h = (120.0 + 2 * total_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_line_layers(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) configs = [ {"draw_wireframe": True, "draw_contour": False, "draw_crease": False}, {"draw_wireframe": False, "draw_contour": True, "draw_crease": False}, {"draw_wireframe": False, "draw_contour": False, "draw_crease": True}, {"draw_wireframe": True, "draw_contour": True, "draw_crease": True}, ] for i, cfg in enumerate(configs): with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, f"test_layers_{i}.pdf") export_wireframe_to_pdf( edges_2d, pdf_path, crease_angle=30.0, raw_geometry=so, **cfg ) 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")) self.assertIn(b"(X) Tj", content) self.assertIn(b"(Y) Tj", content) def test_export_pdf_show_name(self): so = SlicingObject() so.name = "MyTestPart" tri = np.array([ [0.0, 0.0, 0.0], [50.0, 0.0, 0.0], [0.0, 50.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) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_show_name.pdf") export_mesh_to_pdf( edges_2d, pdf_path, show_name=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read() self.assertIn(b"(MyTestPart) Tj", content) def test_export_pdf_show_bbox_size(self): so = SlicingObject() so.name = "GearPart" tri = np.array([ [10.0, 20.0, 0.0], [130.0, 20.0, 0.0], [10.0, 250.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) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_show_bbox.pdf") export_mesh_to_pdf( edges_2d, pdf_path, show_name=True, show_bbox_size=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read() # Width = 120mm, Height = 230mm -> Line 1: (GearPart) Tj, Line 2: (\(X: 120mm Y: 230mm\)) Tj self.assertIn(b"(GearPart) Tj", content) self.assertIn(b"(\\(X: 120mm Y: 230mm\\)) Tj", content) def test_export_pdf_draw_bbox(self): so = SlicingObject() so.name = "BBoxPart" tri = np.array([ [0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [0.0, 50.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) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_draw_bbox.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_bbox=True, bbox_color=(0.0, 0.0, 1.0), raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read() # BBox color blue: 0.000 0.000 1.000 RG self.assertIn(b"0.000 0.000 1.000 RG", content) def test_pdf_object_physical_size_matches_3d_mesh(self): """ Verifies that the physical size of an object exported to PDF (in PostScript points pt) matches its 3D mesh dimensions (in mm) * (72.0 / 25.4) with 100% mathematical precision. """ import re mesh_w_mm = 120.0 mesh_h_mm = 80.0 min_x_3d, min_y_3d = 25.0, -15.0 max_x_3d, max_y_3d = min_x_3d + mesh_w_mm, min_y_3d + mesh_h_mm so = SlicingObject() so.name = "ScaledPart" tri1 = np.array([ [min_x_3d, min_y_3d, 0.0], [max_x_3d, min_y_3d, 0.0], [min_x_3d, max_y_3d, 0.0], [0.0, 0.0, 1.0] ], dtype=np.float64) tri2 = np.array([ [max_x_3d, min_y_3d, 0.0], [max_x_3d, max_y_3d, 0.0], [min_x_3d, max_y_3d, 0.0], [0.0, 0.0, 1.0] ], dtype=np.float64) so.mesh = np.array([tri1, tri2]) edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_physical_scale.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, margin_mm=15.0, line_width_mm=0.5, raw_geometry=so ) with open(pdf_path, "rb") as f: pdf_text = f.read().decode("utf-8", errors="ignore") mediabox_match = re.search(r"/MediaBox\s+\[0\s+0\s+(\d+\.\d+)\s+(\d+\.\d+)\]", pdf_text) self.assertIsNotNone(mediabox_match) page_w_pt = float(mediabox_match.group(1)) page_h_pt = float(mediabox_match.group(2)) # Verify MediaBox exact conversion: 1 pt = 1/72 inch = 25.4/72 mm page_w_mm = page_w_pt / MM_TO_PT page_h_mm = page_h_pt / MM_TO_PT self.assertAlmostEqual(page_w_mm * MM_TO_PT, page_w_pt, places=4) self.assertAlmostEqual(page_h_mm * MM_TO_PT, page_h_pt, places=4) # Object physical size in PDF points: object_w_pt = mesh_w_mm * MM_TO_PT object_h_pt = mesh_h_mm * MM_TO_PT # 120 mm -> 340.15748 pt, 80 mm -> 226.77165 pt self.assertAlmostEqual(object_w_pt, 120.0 * (72.0 / 25.4), places=5) self.assertAlmostEqual(object_h_pt, 80.0 * (72.0 / 25.4), places=5) # Extract drawn object line coordinates under /GS100 gs from the PDF stream gs100_idx = pdf_text.find("/GS100 gs") self.assertNotEqual(gs100_idx, -1) gs100_section = pdf_text[gs100_idx:] line_matches = re.findall(r"(\d+\.\d+)\s+(\d+\.\d+)\s+m\s+(\d+\.\d+)\s+(\d+\.\d+)\s+l", gs100_section) self.assertTrue(len(line_matches) >= 4) obj_x, obj_y = [], [] for x0, y0, x1, y1 in line_matches[:4]: obj_x.extend([float(x0), float(x1)]) obj_y.extend([float(y0), float(y1)]) drawn_obj_w_pt = max(obj_x) - min(obj_x) drawn_obj_h_pt = max(obj_y) - min(obj_y) # Assert that actual drawn line span in PDF points EXACTLY equals 3D mesh dimensions self.assertAlmostEqual(drawn_obj_w_pt, 120.0 * (72.0 / 25.4), places=2) self.assertAlmostEqual(drawn_obj_h_pt, 80.0 * (72.0 / 25.4), places=2) def test_complex_slicing_group_pdf_exporter_end_to_end(self): """ End-to-end test on a complex multi-object SlicingGroup containing 3D mechanical components (chamfered block + hollow prism), testing all PDF layers simultaneously. """ import re # Object 1: 3D Chamfered Block so1 = SlicingObject() so1.name = "BaseBracket" # Triangles forming a 3D block with a chamfered face tris1 = [ # Bottom face [[10.0, 20.0, 0.0], [110.0, 20.0, 0.0], [10.0, 70.0, 0.0], [0.0, 0.0, -1.0]], [[110.0, 20.0, 0.0], [110.0, 70.0, 0.0], [10.0, 70.0, 0.0], [0.0, 0.0, -1.0]], # Top face [[10.0, 20.0, 30.0], [10.0, 70.0, 30.0], [90.0, 20.0, 30.0], [0.0, 0.0, 1.0]], [[90.0, 20.0, 30.0], [10.0, 70.0, 30.0], [90.0, 70.0, 30.0], [0.0, 0.0, 1.0]], # Chamfer face (45 deg angle) [[90.0, 20.0, 30.0], [90.0, 70.0, 30.0], [110.0, 20.0, 0.0], [0.707, 0.0, 0.707]], [[110.0, 20.0, 0.0], [90.0, 70.0, 30.0], [110.0, 70.0, 0.0], [0.707, 0.0, 0.707]], ] so1.mesh = np.array(tris1, dtype=np.float64) # Object 2: 3D Hollow Octagonal Prism so2 = SlicingObject() so2.name = "HollowPipe" # Construct an 8-sided hollow tube in 3D angles = np.linspace(0, 2 * np.pi, 8, endpoint=False) r_out = 30.0 r_in = 15.0 cx, cy = 180.0, 45.0 tube_tris = [] for i in range(8): a1, a2 = angles[i], angles[(i + 1) % 8] x1_o, y1_o = cx + r_out * np.cos(a1), cy + r_out * np.sin(a1) x2_o, y2_o = cx + r_out * np.cos(a2), cy + r_out * np.sin(a2) x1_i, y1_i = cx + r_in * np.cos(a1), cy + r_in * np.sin(a1) x2_i, y2_i = cx + r_in * np.cos(a2), cy + r_in * np.sin(a2) # Top ring face tube_tris.append([[x1_o, y1_o, 50.0], [x2_o, y2_o, 50.0], [x1_i, y1_i, 50.0], [0.0, 0.0, 1.0]]) tube_tris.append([[x2_o, y2_o, 50.0], [x2_i, y2_i, 50.0], [x1_i, y1_i, 50.0], [0.0, 0.0, 1.0]]) # Outer wall face nx, ny = np.cos((a1 + a2) / 2), np.sin((a1 + a2) / 2) tube_tris.append([[x1_o, y1_o, 0.0], [x2_o, y2_o, 0.0], [x1_o, y1_o, 50.0], [nx, ny, 0.0]]) tube_tris.append([[x2_o, y2_o, 0.0], [x2_o, y2_o, 50.0], [x1_o, y1_o, 50.0], [nx, ny, 0.0]]) so2.mesh = np.array(tube_tris, dtype=np.float64) # Assemble into SlicingGroup with 2 collections sc1 = SlicingCollection([], "BracketColl") sc1.objects = [so1] sc2 = SlicingCollection([], "PipeColl") sc2.objects = [so2] sg = SlicingGroup([]) sg.collections = {"BracketColl": sc1, "PipeColl": sc2} # Extract 2D wireframe edges from SlicingGroup edges_2d = extract_unique_top_view_edges(sg) self.assertTrue(len(edges_2d) > 0) # Export to PDF with ALL checkboxes enabled with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "complex_slicing_group.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, draw_boundary=True, draw_contour=True, draw_crease=True, show_name=True, crease_angle=30.0, margin_mm=12.5, line_width_mm=0.8, stroke_color=(0.1, 0.2, 0.4), bg_color=(0.95, 0.98, 1.0), raw_geometry=sg ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: pdf_bytes = f.read() pdf_text = pdf_bytes.decode("utf-8", errors="ignore") # 1. Check PDF header & footer self.assertTrue(pdf_bytes.startswith(b"%PDF-1.4")) self.assertIn(b"%%EOF\n", pdf_bytes) # 2. Check MediaBox mediabox_match = re.search(r"/MediaBox\s+\[0\s+0\s+(\d+\.\d+)\s+(\d+\.\d+)\]", pdf_text) self.assertIsNotNone(mediabox_match) w_pt, h_pt = float(mediabox_match.group(1)), float(mediabox_match.group(2)) self.assertTrue(w_pt > 0 and h_pt > 0) # 3. Check ExtGState opacities (/GS25, /GS50, /GS100) self.assertIn("/GS25 5 0 R", pdf_text) self.assertIn("/GS50 6 0 R", pdf_text) self.assertIn("/GS100 7 0 R", pdf_text) # 4. Check colors self.assertIn("0.950 0.980 1.000 rg", pdf_text) # BG color self.assertIn("0.100 0.200 0.400 RG", pdf_text) # Stroke color # 5. Check object name rendering for both objects self.assertIn("(BaseBracket) Tj", pdf_text) self.assertIn("(HollowPipe) Tj", pdf_text) # 6. Check size legend tick labels self.assertIn("(10) Tj", pdf_text) self.assertIn("(100 mm) Tj", pdf_text) def test_export_pdf_with_settings_attributes(self): so = SlicingObject() so.name = "AttributedPart" 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] ], dtype=np.float64) so.mesh = np.array([tri]) so.attributes = { "infill_density": {"domain": "face", "type": "float", "values": np.array([20.0, 20.0])}, "layer_height": {"domain": "face", "type": "float", "values": np.array([0.15, 0.15])}, "material": ["PLA", "PLA"] } edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_settings_attr.pdf") export_mesh_to_pdf( edges_2d, pdf_path, show_name=True, show_bbox_size=True, raw_geometry=so, settings={"infill_density": "%", "layer_height": " mm", "material": ""} ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read().decode("utf-8", errors="ignore") self.assertIn("(AttributedPart) Tj", content) self.assertIn("(infill_density: 20%) Tj", content) self.assertIn("(layer_height: 0.15 mm) Tj", content) self.assertIn("(material: PLA) Tj", content) def test_export_pdf_draw_sa2_measures(self): so = SlicingObject() so.name = "SA2Part" tri = np.array([ [0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [0.0, 50.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) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_sa2_measures.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_sa2_measures=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read().decode("utf-8", errors="ignore") # Check that SA2 crimson red color stroke and text measurements (100mm, 50mm) are generated self.assertIn("0.850 0.150 0.150 RG", content) self.assertIn("(100mm) Tj", content) self.assertIn("(50mm) Tj", content) def test_export_pdf_3d_solid_bore_detection(self): num_sides = 16 angles = np.linspace(0, 2 * np.pi, num_sides, endpoint=False) outer_top = np.column_stack([50 * np.cos(angles), 50 * np.sin(angles), np.full(num_sides, 20.0)]) outer_bot = np.column_stack([50 * np.cos(angles), 50 * np.sin(angles), np.full(num_sides, 0.0)]) inner_top = np.column_stack([15 * np.cos(angles), 15 * np.sin(angles), np.full(num_sides, 20.0)]) inner_bot = np.column_stack([15 * np.cos(angles), 15 * np.sin(angles), np.full(num_sides, 0.0)]) tris = [] for i in range(num_sides): i_next = (i + 1) % num_sides tris.append([outer_top[i], outer_top[i_next], inner_top[i], [0, 0, 1]]) tris.append([outer_top[i_next], inner_top[i_next], inner_top[i], [0, 0, 1]]) tris.append([outer_bot[i], inner_bot[i], outer_bot[i_next], [0, 0, -1]]) tris.append([outer_bot[i_next], inner_bot[i], inner_bot[i_next], [0, 0, -1]]) n_out = [np.cos(angles[i]), np.sin(angles[i]), 0] tris.append([outer_bot[i], outer_top[i], outer_top[i_next], n_out]) tris.append([outer_bot[i], outer_top[i_next], outer_bot[i_next], n_out]) n_in = [-np.cos(angles[i]), -np.sin(angles[i]), 0] tris.append([inner_bot[i], inner_top[i_next], inner_top[i], n_in]) tris.append([inner_bot[i], inner_bot[i_next], inner_top[i_next], n_in]) so = SlicingObject() so.name = "SolidBoreCylinder" so.mesh = np.array(tris, dtype=np.float64) edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_solid_bore.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_sa2_measures=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read().decode("utf-8", errors="ignore") # ISO circular callouts (Electric Blue 0.10,0.45,0.90 for Ø or Amber Orange 0.90,0.45,0.10 for R) self.assertTrue("0.100 0.450 0.900 RG" in content or "0.900 0.450 0.100 RG" in content) def test_export_pdf_iso_arc_and_chamfer_detection(self): # Create mesh with a chamfer (10mm) and an arc cutout (R15mm) t = np.linspace(0, np.pi/2, 8) arc_pts = np.column_stack([15 - 15*np.cos(t), 15 - 15*np.sin(t)]) pts = [ [0, 15], [0, 60], [90, 60], # Chamfer start [100, 50], # Chamfer end [100, 0], [15, 0] ] polygon_pts = pts[:5] + list(arc_pts[::-1]) # Triangulate top face & extruded 3D mesh tris = [] for i in range(len(polygon_pts)): i_next = (i + 1) % len(polygon_pts) p1_2d = polygon_pts[i] p2_2d = polygon_pts[i_next] p1_top = [p1_2d[0], p1_2d[1], 10.0] p2_top = [p2_2d[0], p2_2d[1], 10.0] p1_bot = [p1_2d[0], p1_2d[1], 0.0] p2_bot = [p2_2d[0], p2_2d[1], 0.0] # Side wall tris tris.append([p1_bot, p1_top, p2_top, [0, 0, 1]]) tris.append([p1_bot, p2_top, p2_bot, [0, 0, 1]]) so = SlicingObject() so.name = "ArcChamferPart" so.mesh = np.array(tris, dtype=np.float64) edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_arc_chamfer.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_sa2_measures=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read().decode("utf-8", errors="ignore") # Verify ISO R (radius amber 0.900 0.450 0.100) and ISO C (chamfer purple 0.600 0.200 0.850) self.assertIn("0.900 0.450 0.100 RG", content) self.assertIn("(R15", content) self.assertIn("0.600 0.200 0.850 RG", content) self.assertIn("(C10mm) Tj", content) def test_export_pdf_font_size(self): so = SlicingObject() so.name = "CustomFontPart" so.mesh = np.array([ [[0, 0, 0], [50, 0, 0], [50, 50, 0]], [[0, 0, 0], [50, 50, 0], [0, 50, 0]] ], dtype=np.float64) edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_custom_font.pdf") export_mesh_to_pdf( edges_2d, pdf_path, font_size_pt=14.5, show_name=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read().decode("utf-8", errors="ignore") self.assertIn("/F1 14.5 Tf", content) def test_export_pdf_league_mono_font_embedded(self): so = SlicingObject() so.name = "LeagueMonoTestPart" so.mesh = np.array([ [[0, 0, 0], [10, 0, 0], [10, 10, 0]], [[0, 0, 0], [10, 10, 0], [0, 10, 0]] ], dtype=np.float64) edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "test_league_mono.pdf") export_mesh_to_pdf( edges_2d, pdf_path, show_name=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = f.read().decode("utf-8", errors="ignore") self.assertIn("LeagueMono-Regular", content) self.assertIn("/Subtype /OpenType", content) if __name__ == '__main__': unittest.main()