nodeslicer/tests/test_pdf_exporter.py
2026-07-29 19:50:20 +03:00

191 lines
6.5 KiB
Python

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()
minor_step = 1.0 # main_step 10.0 / 10
total_margin_mm = margin_mm + 2.0 * minor_step
expected_w = (100.0 + 2 * total_margin_mm) * MM_TO_PT
expected_h = (100.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_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()