diff --git a/core/pdf_exporter/geometry.py b/core/pdf_exporter/geometry.py index 9f3eadb..8363991 100644 --- a/core/pdf_exporter/geometry.py +++ b/core/pdf_exporter/geometry.py @@ -220,18 +220,13 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra num_unique = len(unique_keys) + max_cat = np.full(num_unique, -1, dtype=int) + min_cat = np.full(num_unique, 999, dtype=int) + np.maximum.at(max_cat, inverse_indices, edge_category) + np.minimum.at(min_cat, inverse_indices, edge_category) + # Rule 1: Silhouette edge - adjacent faces have different categories - # (i.e., not all faces sharing this edge are in the same category) - categories_all = edge_category[inverse_indices] - rule1_unique = np.zeros(num_unique, dtype=bool) - - for edge_id in range(num_unique): - edge_cats = categories_all[inverse_indices == edge_id] - if len(edge_cats) < 2: - rule1_unique[edge_id] = False - else: - all_same = np.all(edge_cats == edge_cats[0]) - rule1_unique[edge_id] = not all_same + rule1_unique = (counts >= 2) & (max_cat != min_cat) # Rule 2: Boundary edge (owned by 1 face) rule2_unique = counts == 1 @@ -477,27 +472,19 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]: num_unique = len(unique_keys) - # For each unique edge, collect the categories of all adjacent faces - # An edge is a silhouette if it has faces from different categories - # (i.e., not all faces are in the same category) - - # Get categories for all edge instances - categories_all = edge_category[inverse_indices] - - # For each unique edge, check if all its face categories are the same - is_silhouette = np.zeros(num_unique, dtype=bool) - - for edge_id in range(num_unique): - edge_cats = categories_all[inverse_indices == edge_id] - # If this edge has only one face, it's a boundary, not silhouette - if len(edge_cats) < 2: - is_silhouette[edge_id] = False - else: - # Check if all categories are the same - all_same = np.all(edge_cats == edge_cats[0]) - is_silhouette[edge_id] = not all_same + max_cat = np.full(num_unique, -1, dtype=int) + min_cat = np.full(num_unique, 999, dtype=int) + np.maximum.at(max_cat, inverse_indices, edge_category) + np.minimum.at(min_cat, inverse_indices, edge_category) - return canonical_2d[first_indices[is_silhouette]] + # An edge is a silhouette/contour edge if: + # 1. It is owned by 1 face (boundary edge of open surface) + # OR + # 2. Adjacent faces belong to different normal.z orientation categories (e.g. front vs back, front vs vertical) + is_silhouette = (counts == 1) | (max_cat != min_cat) + + contour_edges = canonical_2d[first_indices[is_silhouette]] + return deduplicate_2d_edges(contour_edges) def extract_outer_contour_edges(mesh_data: Any) -> NDArray[np.float64]: diff --git a/core/pdf_exporter/writer.py b/core/pdf_exporter/writer.py index b13e0a4..515058a 100644 --- a/core/pdf_exporter/writer.py +++ b/core/pdf_exporter/writer.py @@ -126,8 +126,10 @@ def export_mesh_to_pdf( if edge_list: combined = np.concatenate(edge_list, axis=0) edges_to_draw = deduplicate_2d_edges(combined) - else: + elif draw_wireframe: edges_to_draw = edges_2d + else: + edges_to_draw = np.empty((0, 2, 2), dtype=np.float64) if len(edges_to_draw) > 0: min_x = float(edges_to_draw[:, :, 0].min()) diff --git a/tests/test_pdf_exporter.py b/tests/test_pdf_exporter.py index 04508d4..65d0c81 100644 --- a/tests/test_pdf_exporter.py +++ b/tests/test_pdf_exporter.py @@ -46,6 +46,7 @@ from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup, SlicingC from NodeSlicer.core.pdf_exporter import ( extract_unique_top_view_edges, extract_lineart_edges, + extract_outer_contour_edges, export_wireframe_to_pdf, export_mesh_to_pdf, MM_TO_PT @@ -129,6 +130,29 @@ class TestPdfExporter(unittest.TestCase): 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_export_pdf_true_1_to_1_scale(self): edges_2d = np.array([ [[0.0, 0.0], [100.0, 0.0]],