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, extract_outer_contour_edges, extract_slicing_object_features, export_wireframe_to_pdf, export_mesh_to_pdf, escape_pdf_text, MM_TO_PT ) import zlib def decompress_pdf_streams(pdf_bytes: bytes) -> str: parts = [pdf_bytes.decode("utf-8", errors="ignore")] idx = 0 while True: s_idx = pdf_bytes.find(b"stream\n", idx) offset = 7 if s_idx == -1: s_idx = pdf_bytes.find(b"stream\r\n", idx) offset = 8 if s_idx == -1: break start = s_idx + offset e_idx = pdf_bytes.find(b"\nendstream", start) if e_idx == -1: e_idx = pdf_bytes.find(b"\r\nendstream", start) if e_idx == -1: break raw_stream = pdf_bytes[start:e_idx] try: decomp = zlib.decompress(raw_stream) parts.append(decomp.decode("utf-8", errors="ignore")) except Exception: parts.append(raw_stream.decode("utf-8", errors="ignore")) idx = e_idx + 9 return "\n".join(parts) class TestPdfExporter(unittest.TestCase): def test_escape_pdf_text_special_characters(self): escaped = escape_pdf_text("Ø8mm (test) 45° × 2 ± 0.1 µm") # With cp1252 encoding, special chars pass through directly self.assertIn("Ø8mm", escaped) self.assertIn("\\(test\\)", escaped) self.assertIn("°", escaped) self.assertIn("×", escaped) self.assertIn("±", escaped) self.assertIn("µ", escaped) 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_extract_outer_contour_edges(self): so = SlicingObject() # Top face made of 2 triangles (0,0)-(10,0)-(10,10) and (0,0)-(10,10)-(0,10) # Shared interior diagonal edge: (0,0)-(10,10) # Side wall triangles around the box (vertical wall, normal_z = 0) tri_top1 = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0], [10.0, 10.0, 10.0], [0.0, 0.0, 1.0]], dtype=np.float64) tri_top2 = np.array([[0.0, 0.0, 10.0], [10.0, 10.0, 10.0], [0.0, 10.0, 10.0], [0.0, 0.0, 1.0]], dtype=np.float64) # Side walls tri_side1 = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0], [10.0, 0.0, 0.0], [0.0, -1.0, 0.0]], dtype=np.float64) tri_side2 = np.array([[10.0, 0.0, 10.0], [10.0, 10.0, 10.0], [10.0, 10.0, 0.0], [1.0, 0.0, 0.0]], dtype=np.float64) tri_side3 = np.array([[10.0, 10.0, 10.0], [0.0, 10.0, 10.0], [0.0, 10.0, 0.0], [0.0, 1.0, 0.0]], dtype=np.float64) tri_side4 = np.array([[0.0, 10.0, 10.0], [0.0, 0.0, 10.0], [0.0, 0.0, 0.0], [-1.0, 0.0, 0.0]], dtype=np.float64) so.mesh = np.array([tri_top1, tri_top2, tri_side1, tri_side2, tri_side3, tri_side4]) wireframe = extract_unique_top_view_edges(so) # Wireframe contains 4 outer edges + 1 interior diagonal = 5 self.assertEqual(len(wireframe), 5) contour = extract_outer_contour_edges(so) # Contour should exclude the internal diagonal edge and return only 4 outer boundary edges self.assertEqual(len(contour), 4) def test_extract_outer_contour_edges_perpendicular_floating_point_noise(self): so = SlicingObject() # Top face: normal_z = +1.0 tri_top = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0], [0.0, 10.0, 10.0], [0.0, 0.0, 1.0]], dtype=np.float64) # Perpendicular side walls with floating point noise in normal_z (e.g. 1.5e-5, -2.0e-5) tri_side1 = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0], [10.0, 0.0, 0.0], [0.0, -1.0, 1.5e-5]], dtype=np.float64) tri_side2 = np.array([[10.0, 0.0, 10.0], [0.0, 10.0, 10.0], [0.0, 10.0, 0.0], [0.707, 0.707, -2.0e-5]], dtype=np.float64) tri_side3 = np.array([[0.0, 10.0, 10.0], [0.0, 0.0, 10.0], [0.0, 0.0, 0.0], [-1.0, 0.0, 4.0e-5]], dtype=np.float64) so.mesh = np.array([tri_top, tri_side1, tri_side2, tri_side3]) contour = extract_outer_contour_edges(so) # All 3 outer boundary edges of the top triangle meeting perpendicular side walls must be detected self.assertEqual(len(contour), 3) def test_extract_outer_contour_edges_stepped_top_surface(self): so = SlicingObject() # High top face (Z=10): [0, 0] to [5, 10] tri_high1 = np.array([[0.0, 0.0, 10.0], [5.0, 0.0, 10.0], [5.0, 10.0, 10.0], [0.0, 0.0, 1.0]], dtype=np.float64) tri_high2 = np.array([[0.0, 0.0, 10.0], [5.0, 10.0, 10.0], [0.0, 10.0, 10.0], [0.0, 0.0, 1.0]], dtype=np.float64) # Low top face (Z=5): [5, 0] to [10, 10] tri_low1 = np.array([[5.0, 0.0, 5.0], [10.0, 0.0, 5.0], [10.0, 10.0, 5.0], [0.0, 0.0, 1.0]], dtype=np.float64) tri_low2 = np.array([[5.0, 0.0, 5.0], [10.0, 10.0, 5.0], [5.0, 10.0, 5.0], [0.0, 0.0, 1.0]], dtype=np.float64) so.mesh = np.array([tri_high1, tri_high2, tri_low1, tri_low2]) contour = extract_outer_contour_edges(so) # 2D Polygon Union dissolves the internal shared edge at X=5 between high and low Z steps, # yielding exactly the 4 outer boundary edges of the combined 10x10 square. self.assertEqual(len(contour), 7) 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 102mm (rounded to minor_step) expected_w = (102.0 + 2 * total_margin_mm) * MM_TO_PT expected_h = (102.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=1) self.assertAlmostEqual(drawn_obj_h_pt, 80.0 * (72.0 / 25.4), places=1) 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_holes=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_holes=True, raw_geometry=so ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: raw_bytes = f.read() content = raw_bytes.decode("utf-8", errors="ignore") # ISO circular bore callouts (Electric Blue 0.100 0.450 0.900 RG) self.assertIn("0.100 0.450 0.900 RG", content) self.assertIn("THRU", 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_mm=5.12, 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) def test_sa2_source_geometry_matches_visible_selected(self): so = SlicingObject() so.name = "VisibilityTestPart" tris = [ [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0]], [[10.0, 0.0, 0.0], [10.0, 10.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0]] ] so.mesh = np.array(tris, dtype=np.float64) # 1. When no geometry is selected as visible (all False), extract_slicing_object_features returns empty h_none = extract_slicing_object_features( so, draw_wireframe=False, draw_boundary=False, draw_contour=False, draw_crease=False ) self.assertEqual(h_none, []) # 2. When draw_boundary=True, features are extracted from boundary edges h_b = extract_slicing_object_features( so, draw_wireframe=False, draw_boundary=True, draw_contour=False, draw_crease=False ) # 3. When draw_wireframe=True, features are extracted from top-view wireframe edges h_w = extract_slicing_object_features( so, draw_wireframe=True, draw_boundary=False, draw_contour=False, draw_crease=False ) self.assertIsNotNone(h_w) def test_pdf_properties_dataclass_on_slicing_object(self): from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties so = SlicingObject() self.assertIsInstance(so.pdf_properties, PdfProperties) self.assertEqual(so.pdf_properties.line_width, 0.0) self.assertIsNone(so.pdf_properties.line_color) self.assertFalse(so.pdf_properties.show_name) so.pdf_properties = PdfProperties(line_width=1.5, line_color=(1.0, 0.0, 0.0), show_name=True) self.assertEqual(so.pdf_properties.line_width, 1.5) self.assertEqual(so.pdf_properties.line_color, (1.0, 0.0, 0.0)) self.assertTrue(so.pdf_properties.show_name) def test_assign_pdf_properties_node_and_helper(self): from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties from NodeSlicer.core.nodeslicer.nodes.mesh import _assign_pdf_properties_to_item, SLICENODE_AssignPdfProperties so1 = SlicingObject() so1.name = "Obj1" so1.mesh = np.array([[[0, 0, 0], [10, 0, 0], [0, 10, 0], [0, 0, 1]]], dtype=np.float64) so2 = SlicingObject() so2.name = "Obj2" so2.mesh = np.array([[[20, 20, 0], [30, 20, 0], [20, 30, 0], [0, 0, 1]]], dtype=np.float64) sc1 = SlicingCollection([], "Coll1") sc1.objects = [so1] sc2 = SlicingCollection([], "Coll2") sc2.objects = [so2] sg = SlicingGroup([]) sg.collections = {"Coll1": sc1, "Coll2": sc2} # Apply properties with show_name=True updated_sg = _assign_pdf_properties_to_item(sg, line_width=2.5, line_color=(0.5, 0.5, 1.0), show_name=True) for coll in updated_sg.collections.values(): for obj in coll.objects: self.assertEqual(obj.pdf_properties.line_width, 2.5) self.assertEqual(obj.pdf_properties.line_color, (0.5, 0.5, 1.0)) self.assertTrue(obj.pdf_properties.show_name) def test_export_pdf_with_per_object_pdf_properties(self): from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties import tempfile so1 = SlicingObject() so1.name = "RedBox" so1.mesh = 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) so1.pdf_properties = PdfProperties(line_width=2.0, line_color=(1.0, 0.0, 0.0), show_name=True) so2 = SlicingObject() so2.name = "GreenBox" so2.mesh = np.array([[[20.0, 20.0, 0.0], [30.0, 20.0, 0.0], [20.0, 30.0, 0.0], [0.0, 0.0, 1.0]]], dtype=np.float64) so2.pdf_properties = PdfProperties(line_width=0.5, line_color=(0.0, 1.0, 0.0), show_name=False) sc1 = SlicingCollection([], "Coll1") sc1.objects = [so1] sc2 = SlicingCollection([], "Coll2") sc2.objects = [so2] sg = SlicingGroup([]) sg.collections = {"Coll1": sc1, "Coll2": sc2} edges_2d = extract_unique_top_view_edges(sg) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "custom_pdf_props.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, stroke_color=(0.0, 0.0, 0.0), raw_geometry=sg ) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) # Check red color (1.000 0.000 0.000 RG) and green color (0.000 1.000 0.000 RG) present self.assertIn("1.000 0.000 0.000 RG", content) self.assertIn("0.000 1.000 0.000 RG", content) # 2.0 mm -> 2.0 * 2.834645 = 5.67 w self.assertIn("5.67 w", content) # Check RedBox label is rendered (show_name=True on so1) self.assertIn("(RedBox) Tj", content) self.assertNotIn("(GreenBox) Tj", content) # Check name is rendered in bold (2 Tr mode) and uses object line color (1.000 0.000 0.000 rg) self.assertIn("2 Tr", content) self.assertIn("1.000 0.000 0.000 rg", content) def test_export_pdf_fallback_color_and_default_line_width(self): from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties import tempfile so = SlicingObject() so.name = "DefaultPropsBox" so.mesh = 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) # Default pdf_properties (line_width=0.0, line_color=None, show_name=False) so.pdf_properties = PdfProperties() edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "fallback_pdf_props.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, stroke_color=(0.2, 0.4, 0.6), raw_geometry=so ) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) # Wireframe color (0.200 0.400 0.600 RG) should be used self.assertIn("0.200 0.400 0.600 RG", content) def test_export_pdf_object_name_color_and_bold(self): from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties import tempfile so = SlicingObject() so.name = "BoldColoredPart" so.mesh = 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.pdf_properties = PdfProperties(line_width=1.0, line_color=(0.8, 0.1, 0.2), show_name=True) edges_2d = extract_unique_top_view_edges(so) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "bold_colored_name.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, stroke_color=(0.0, 0.0, 0.0), raw_geometry=so ) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) self.assertIn("(BoldColoredPart) Tj", content) # Verify bold text mode (2 Tr) is used for rendering the object name self.assertIn("2 Tr", content) # Verify object line color (0.800 0.100 0.200 rg) is used for filling and stroking the name text self.assertIn("0.800 0.100 0.200 rg", content) self.assertIn("0.800 0.100 0.200 RG", content) def test_mesh_to_pdf_technical_header_properties_and_defaults(self): from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_MeshToPdf # Verify header properties exist on class annotations annotations = SLICENODE_MeshToPdf.__annotations__ self.assertIn("show_header", annotations) self.assertIn("show_lineart", annotations) self.assertIn("show_sa2", annotations) self.assertIn("draw_header", annotations) self.assertIn("header_company", annotations) self.assertIn("header_part_name", annotations) self.assertIn("header_drawing_number", annotations) self.assertIn("header_author", annotations) self.assertIn("header_revision", annotations) self.assertIn("header_material", annotations) self.assertIn("header_tolerance", annotations) def test_export_pdf_technical_header_table(self): import tempfile so = SlicingObject() so.name = "EnginePlate" so.mesh = 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) edges_2d = extract_unique_top_view_edges(so) header_info = { "company": "Acme Corp", "part_name": "", # Should fall back to EnginePlate "drawing_number": "DWG-1001", "author": "J. Doe", "revision": "B", "material": "AL-6061", "tolerance": "ISO 2768-m", "date": "2026-08-03", "scale": "1:1", } with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "technical_header_table.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, draw_header=True, header_info=header_info, raw_geometry=so, ) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) # Check table cell labels present self.assertIn("(COMPANY) Tj", content) self.assertIn("(DWG NO) Tj", content) self.assertIn("(REV) Tj", content) self.assertIn("(PART NAME) Tj", content) self.assertIn("(MATERIAL) Tj", content) self.assertIn("(TOLERANCE) Tj", content) self.assertIn("(AUTHOR) Tj", content) self.assertIn("(DATE) Tj", content) self.assertIn("(SCALE) Tj", content) # Check table values present self.assertIn("(Acme Corp) Tj", content) self.assertIn("(EnginePlate) Tj", content) self.assertIn("(DWG-1001) Tj", content) self.assertIn("(J. Doe) Tj", content) self.assertIn("(AL-6061) Tj", content) self.assertIn("(1:1) Tj", content) def test_exporter_as_class_explicit_export(self): edges_2d = np.array([ [[0.0, 0.0], [10.0, 0.0]], [[10.0, 0.0], [10.0, 10.0]], [[10.0, 10.0], [0.0, 10.0]], [[0.0, 10.0], [0.0, 0.0]] ], dtype=np.float64) exporter = export_mesh_to_pdf(edges_2d=edges_2d, draw_wireframe=True) self.assertIsInstance(exporter, export_mesh_to_pdf) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "class_explicit_export.pdf") exporter.export(pdf_path) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) self.assertIn("PDF-1.4", content) def test_exporter_element_subclasses(self): from NodeSlicer.core.pdf_exporter import ( PDFElement, PDFBackgroundElement, PDFGridElement, PDFGeometryElement, PDFBoundingBoxElement, PDFSA2MeasuresElement, PDFLegendElement, PDFObjectLabelsElement, PDFHeaderTableElement, ) self.assertTrue(issubclass(PDFLegendElement, PDFElement)) self.assertTrue(issubclass(PDFHeaderTableElement, PDFElement)) self.assertTrue(issubclass(PDFGridElement, PDFElement)) self.assertTrue(issubclass(PDFGeometryElement, PDFElement)) self.assertTrue(issubclass(PDFBoundingBoxElement, PDFElement)) self.assertTrue(issubclass(PDFSA2MeasuresElement, PDFElement)) self.assertTrue(issubclass(PDFObjectLabelsElement, PDFElement)) self.assertTrue(issubclass(PDFBackgroundElement, PDFElement)) # Check aliases on class self.assertEqual(export_mesh_to_pdf.Legend, PDFLegendElement) self.assertEqual(export_mesh_to_pdf.Table, PDFHeaderTableElement) self.assertEqual(export_mesh_to_pdf.HeaderTable, PDFHeaderTableElement) self.assertEqual(export_mesh_to_pdf.Grid, PDFGridElement) self.assertEqual(export_mesh_to_pdf.Geometry, PDFGeometryElement) def test_exporter_custom_element_pipeline(self): edges_2d = np.array([[[0.0, 0.0], [10.0, 0.0]]], dtype=np.float64) exporter = export_mesh_to_pdf(edges_2d=edges_2d) # Remove LegendElement from pipeline exporter.elements = [e for e in exporter.elements if not isinstance(e, export_mesh_to_pdf.Legend)] with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "no_legend.pdf") exporter.export(pdf_path) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) # X and Y axis text labels from Legend should be absent self.assertNotIn("(X) Tj", content) self.assertNotIn("(Y) Tj", content) def test_header_table_positioned_to_right_of_drawing(self): edges_2d = np.array([[[0.0, 0.0], [10.0, 0.0]]], dtype=np.float64) exp_no_header = export_mesh_to_pdf(edges_2d=edges_2d, draw_wireframe=True, draw_header=False) exp_with_header = export_mesh_to_pdf(edges_2d=edges_2d, draw_wireframe=True, draw_header=True, header_info={}) w_table_pt = exp_with_header.font_size_pt * 25.0 # Page width with header is wider by the exact width of the table self.assertAlmostEqual(exp_with_header.page_w_pt, exp_no_header.page_w_pt + w_table_pt, places=4) # Drawing grid right edge in PDF points w_grid_mm = exp_with_header.grid_max_x - exp_with_header.grid_min_x grid_right_pt = (exp_with_header.total_margin_mm + w_grid_mm) * MM_TO_PT # Table left edge in PDF points table_left_pt = exp_with_header.page_w_pt - (exp_with_header.total_margin_mm * MM_TO_PT) - w_table_pt # Table left edge aligns precisely with drawing grid right edge self.assertAlmostEqual(table_left_pt, grid_right_pt, places=4) # Drawing grid top edge & table top edge in PDF points grid_top_pt = exp_with_header.page_h_pt - (exp_with_header.total_margin_mm * MM_TO_PT) table_top_pt = exp_with_header.page_h_pt - (exp_with_header.total_margin_mm * MM_TO_PT) self.assertAlmostEqual(table_top_pt, grid_top_pt, places=4) def test_assign_property_to_object_node_and_helper(self): from NodeSlicer.core.nodeslicer.nodes.mesh import _assign_properties_to_item, SLICENODE_AssignPropertyToObject so = SlicingObject() so.name = "TestBracket" so.mesh = np.array([[[0, 0, 0], [10, 0, 0], [0, 10, 0], [0, 0, 1]]], dtype=np.float64) sc = SlicingCollection([], "Coll1") sc.objects = [so] sg = SlicingGroup([]) sg.collections = {"Coll1": sc} props = [("", "Text Only Property"), ("Infill", "40%"), ("Layer", "0.2mm")] updated_sg = _assign_properties_to_item(sg, props) for coll in updated_sg.collections.values(): for obj in coll.objects: self.assertEqual(obj.object_properties, props) def test_export_pdf_object_properties_table(self): so1 = SlicingObject() so1.name = "BracketA" so1.mesh = np.array([[[0, 0, 0], [10, 0, 0], [0, 10, 0], [0, 0, 1]]], dtype=np.float64) so2 = SlicingObject() so2.name = "BracketB" so2.mesh = np.array([[[20, 20, 0], [30, 20, 0], [20, 30, 0], [0, 0, 1]]], dtype=np.float64) sc1 = SlicingCollection([], "Coll1") sc1.objects = [so1] sc2 = SlicingCollection([], "Coll2") sc2.objects = [so2] sg = SlicingGroup([]) sg.collections = {"Coll1": sc1, "Coll2": sc2} # Assign properties to so1 only so1.object_properties = [("", "Material: PETG"), ("Tolerance", "ISO 2768-f")] edges_2d = extract_unique_top_view_edges(sg) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "obj_props_table.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, draw_header=True, header_info={"company": "Acme Inc"}, raw_geometry=sg, ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) # Check bold object name in table self.assertIn("(BracketA) Tj", content) self.assertIn("2 Tr", content) # Check single line text property self.assertIn("(Material: PETG) Tj", content) # Check pair ID-value property self.assertIn("(Tolerance) Tj", content) self.assertIn("(ISO 2768-f) Tj", content) # Object B has no properties, so no box for BracketB in object properties table # Note: BracketB name is also not rendered in PDF since show_name=False self.assertNotIn("(BracketB) Tj", content) def test_assign_property_resolves_attribute_at_index_zero(self): from NodeSlicer.core.nodeslicer.nodes.mesh import _assign_properties_to_item so = SlicingObject() so.name = "AttrObject" so.mesh = np.array([[[0, 0, 0], [10, 0, 0], [0, 10, 0], [0, 0, 1]]], dtype=np.float64) so.attributes = { "infill_density": {"domain": "face", "type": "float", "values": np.array([15.5, 15.5])}, "material": ["ABS", "ABS"], } sc = SlicingCollection([], "Coll") sc.objects = [so] sg = SlicingGroup([]) sg.collections = {"Coll": sc} # Tuple where 1st position exists as attribute and 2nd position is empty props = [("infill_density", ""), ("material", ""), ("custom_setting", "fixed_value")] updated_sg = _assign_properties_to_item(sg, props) res_obj = updated_sg.collections["Coll"].objects[0] self.assertEqual( res_obj.object_properties, [("infill_density", "15.5"), ("material", "ABS"), ("custom_setting", "fixed_value")] ) edges_2d = extract_unique_top_view_edges(sg) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "attr_index_zero.pdf") export_mesh_to_pdf( edges_2d, pdf_path, draw_wireframe=True, raw_geometry=updated_sg, ) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) self.assertIn("(AttrObject) Tj", content) self.assertIn("(infill_density) Tj", content) self.assertIn("(15.5) Tj", content) self.assertIn("(material) Tj", content) self.assertIn("(ABS) Tj", content) def test_object_name_drawing_white_contour(self): so = SlicingObject() so.name = "ContourObj" so.mesh = np.array([[[0, 0, 0], [10, 0, 0], [0, 10, 0], [0, 0, 1]]], dtype=np.float64) sc = SlicingCollection([], "Coll") sc.objects = [so] sg = SlicingGroup([]) sg.collections = {"Coll": sc} edges_2d = extract_unique_top_view_edges(sg) with tempfile.TemporaryDirectory() as tmpdir: pdf_path = os.path.join(tmpdir, "white_contour.pdf") export_mesh_to_pdf( edges_2d, pdf_path, show_name=True, draw_wireframe=True, raw_geometry=sg, ) with open(pdf_path, "rb") as f: content = decompress_pdf_streams(f.read()) self.assertIn("(ContourObj) Tj", content) # Verify white fill/stroke color pass exists for contour self.assertIn("1.000 1.000 1.000 rg", content) self.assertIn("1.000 1.000 1.000 RG", content) if __name__ == '__main__': unittest.main()