This commit is contained in:
Nicolas Predella 2026-07-30 19:29:23 +03:00
parent c2e1bc50a5
commit c862e8976b
3 changed files with 45 additions and 32 deletions

View file

@ -220,18 +220,13 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
num_unique = len(unique_keys) 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 # Rule 1: Silhouette edge - adjacent faces have different categories
# (i.e., not all faces sharing this edge are in the same category) rule1_unique = (counts >= 2) & (max_cat != min_cat)
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
# Rule 2: Boundary edge (owned by 1 face) # Rule 2: Boundary edge (owned by 1 face)
rule2_unique = counts == 1 rule2_unique = counts == 1
@ -477,27 +472,19 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
num_unique = len(unique_keys) num_unique = len(unique_keys)
# For each unique edge, collect the categories of all adjacent faces max_cat = np.full(num_unique, -1, dtype=int)
# An edge is a silhouette if it has faces from different categories min_cat = np.full(num_unique, 999, dtype=int)
# (i.e., not all faces are in the same category) np.maximum.at(max_cat, inverse_indices, edge_category)
np.minimum.at(min_cat, inverse_indices, edge_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
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]: def extract_outer_contour_edges(mesh_data: Any) -> NDArray[np.float64]:

View file

@ -126,8 +126,10 @@ def export_mesh_to_pdf(
if edge_list: if edge_list:
combined = np.concatenate(edge_list, axis=0) combined = np.concatenate(edge_list, axis=0)
edges_to_draw = deduplicate_2d_edges(combined) edges_to_draw = deduplicate_2d_edges(combined)
else: elif draw_wireframe:
edges_to_draw = edges_2d edges_to_draw = edges_2d
else:
edges_to_draw = np.empty((0, 2, 2), dtype=np.float64)
if len(edges_to_draw) > 0: if len(edges_to_draw) > 0:
min_x = float(edges_to_draw[:, :, 0].min()) min_x = float(edges_to_draw[:, :, 0].min())

View file

@ -46,6 +46,7 @@ from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup, SlicingC
from NodeSlicer.core.pdf_exporter import ( from NodeSlicer.core.pdf_exporter import (
extract_unique_top_view_edges, extract_unique_top_view_edges,
extract_lineart_edges, extract_lineart_edges,
extract_outer_contour_edges,
export_wireframe_to_pdf, export_wireframe_to_pdf,
export_mesh_to_pdf, export_mesh_to_pdf,
MM_TO_PT MM_TO_PT
@ -129,6 +130,29 @@ class TestPdfExporter(unittest.TestCase):
lineart_100 = extract_lineart_edges(so, max_angle_deg=100.0) lineart_100 = extract_lineart_edges(so, max_angle_deg=100.0)
self.assertTrue(len(lineart_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): def test_export_pdf_true_1_to_1_scale(self):
edges_2d = np.array([ edges_2d = np.array([
[[0.0, 0.0], [100.0, 0.0]], [[0.0, 0.0], [100.0, 0.0]],