189 lines
6.3 KiB
Python
189 lines
6.3 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()
|
|
|
|
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()
|