From c41380f17b0920bc462a61ef124749b8c4b6d1bc Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 29 Jul 2026 20:12:35 +0300 Subject: [PATCH] ref --- core/nodeslicer/nodes/output.py | 47 ++--- core/nodeslicer/tree.py | 2 +- core/pdf_exporter.py | 300 ++++++++++++++++++++++++-------- doc/NODES.md | 9 +- tests/test_pdf_exporter.py | 17 +- 5 files changed, 273 insertions(+), 102 deletions(-) diff --git a/core/nodeslicer/nodes/output.py b/core/nodeslicer/nodes/output.py index 9bb3cef..56a0e1f 100644 --- a/core/nodeslicer/nodes/output.py +++ b/core/nodeslicer/nodes/output.py @@ -7,7 +7,7 @@ import tempfile import re import json -from ..bpy_ns_types import NfStringProperty, NfFloatProperty, NfEnumProperty, NfFloatVectorProperty +from ..bpy_ns_types import NfStringProperty, NfFloatProperty, NfEnumProperty, NfFloatVectorProperty, NfBoolProperty from ....registry import register_class from .base import ExecSliceNode, SlicePaths, SliceNode, SlicingCollection, SlicingGroup @@ -353,23 +353,18 @@ class SLICENODE_AirtableSubmit(ExecSliceNode_DatabaseSubmit): @register_class -class SLICENODE_WireframeToPdf(ExecSliceNode): - bl_label = "Wireframe to PDF" +class SLICENODE_MeshToPdf(ExecSliceNode): + bl_label = "Mesh to PDF" button_label = "Generate PDF" - style: NfEnumProperty( - name="Style", - items=[ - ('WIREFRAME', "Wireframe", "All projected 3D wireframe edges"), - ('LINEART', "Lineart", "Silhouette, boundary, and crease edges above threshold angle"), - ('OUTER_CONTOUR', "Outer Contour", "Silhouette outline boundary"), - ], - default='WIREFRAME' - ) # pyright: ignore[reportInvalidTypeForm] + draw_wireframe: NfBoolProperty(name="Wireframe", default=True) # pyright: ignore[reportInvalidTypeForm] + draw_boundary: NfBoolProperty(name="Boundary (1 Face)", default=False) # pyright: ignore[reportInvalidTypeForm] + draw_contour: NfBoolProperty(name="Contour", default=False) # pyright: ignore[reportInvalidTypeForm] + draw_crease: NfBoolProperty(name="Crease Angle", default=False) # pyright: ignore[reportInvalidTypeForm] + crease_angle: NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0) # pyright: ignore[reportInvalidTypeForm] - crease_angle: NfFloatProperty(name="Crease Angle (°)", default=30.0, min=0.0, max=180.0) # pyright: ignore[reportInvalidTypeForm] - line_width: NfFloatProperty(name="Line Width (mm)", default=0.5, min=.0) # pyright: ignore[reportInvalidTypeForm] - margin: NfFloatProperty(name="Margin (mm)", default=10.0, min=.0) # pyright: ignore[reportInvalidTypeForm] + line_width: NfFloatProperty(name="Line Width (mm)", default=0.5, min=0.01, max=50.0) # pyright: ignore[reportInvalidTypeForm] + margin: NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0) # pyright: ignore[reportInvalidTypeForm] bg_color: NfFloatVectorProperty( name="Background", @@ -396,8 +391,11 @@ class SLICENODE_WireframeToPdf(ExecSliceNode): def draw_buttons(self, context, layout): super().draw_buttons(context, layout) - layout.prop(self, "style") - if self.style == 'LINEART': + layout.prop(self, "draw_wireframe") + layout.prop(self, "draw_boundary") + layout.prop(self, "draw_contour") + layout.prop(self, "draw_crease") + if self.draw_crease: layout.prop(self, "crease_angle") layout.prop(self, "line_width") layout.prop(self, "margin") @@ -405,7 +403,7 @@ class SLICENODE_WireframeToPdf(ExecSliceNode): layout.prop(self, "wire_color") def evaluate(self, operator=None): - from ...pdf_exporter import extract_unique_top_view_edges, export_wireframe_to_pdf + from ...pdf_exporter import extract_unique_top_view_edges, export_mesh_to_pdf geometries: list[Any] = [g for g in self.resolve_multi_input('Geometry') if g is not None] folder: str = self.resolve("Folder") or "" @@ -448,10 +446,13 @@ class SLICENODE_WireframeToPdf(ExecSliceNode): if operator: operator.report({'WARNING'}, f"Geometry for {os.path.basename(filepath)} has no valid 2D top-view wireframe edges") try: - export_wireframe_to_pdf( + export_mesh_to_pdf( edges_2d=edges_2d, filepath=filepath, - style=self.style, + draw_wireframe=self.draw_wireframe, + draw_boundary=self.draw_boundary, + draw_contour=self.draw_contour, + draw_crease=self.draw_crease, crease_angle=self.crease_angle, margin_mm=self.margin, line_width_mm=self.line_width, @@ -459,7 +460,7 @@ class SLICENODE_WireframeToPdf(ExecSliceNode): bg_color=(float(self.bg_color[0]), float(self.bg_color[1]), float(self.bg_color[2])), raw_geometry=geom ) - if operator: operator.report({'INFO'}, f"Wireframe PDF saved to {filepath}") + if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}") except Exception as e: if operator: operator.report({'ERROR'}, f"PDF Generation Error: {str(e)}") return 'error' @@ -467,5 +468,9 @@ class SLICENODE_WireframeToPdf(ExecSliceNode): return 'ok' +# Alias for backward compatibility +SLICENODE_WireframeToPdf = SLICENODE_MeshToPdf + + diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index 47c329f..783ef97 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -561,7 +561,7 @@ class SLICENODE_MT_ActionIO(Menu): layout = self.layout assert layout layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile" - layout.operator("node.add_node", text="Wireframe to PDF").type = "SLICENODE_WireframeToPdf" + layout.operator("node.add_node", text="Mesh to PDF").type = "SLICENODE_MeshToPdf" layout.operator("node.add_node", text="Gcode to Text Block").type = "SLICENODE_GcodeToTextBlock" layout.operator("node.add_node", text="Start Print").type = "SLICENODE_StartPrint" layout.separator() diff --git a/core/pdf_exporter.py b/core/pdf_exporter.py index eeaa729..b056e7c 100644 --- a/core/pdf_exporter.py +++ b/core/pdf_exporter.py @@ -10,6 +10,26 @@ from shapely.geometry import Polygon, MultiPolygon MM_TO_PT = 72.0 / 25.4 # 1 mm = 2.8346456692913386 PostScript points +def deduplicate_2d_edges(edges: NDArray[np.float64]) -> NDArray[np.float64]: + """ + Deduplicates 2D line segments regardless of endpoint orientation. + """ + if len(edges) == 0: + return edges + rounded = np.round(edges, decimals=5) + swap_mask = (rounded[:, 0, 0] > rounded[:, 1, 0]) | ( + (rounded[:, 0, 0] == rounded[:, 1, 0]) & (rounded[:, 0, 1] > rounded[:, 1, 1]) + ) + ordered = edges.copy() + ordered[swap_mask] = ordered[swap_mask][:, ::-1, :] + ordered_rounded = rounded.copy() + ordered_rounded[swap_mask] = ordered_rounded[swap_mask][:, ::-1, :] + + view = ordered_rounded.reshape(len(ordered_rounded), -1) + _, unique_indices = np.unique(view, axis=0, return_index=True) + return ordered[unique_indices] + + def extract_unique_top_view_edges(mesh_data: Any) -> NDArray[np.float64]: """ Extracts unique 2D line segments projected orthographically from top (+Z). @@ -67,19 +87,7 @@ def extract_unique_top_view_edges(mesh_data: Any) -> NDArray[np.float64]: if valid_edges.shape[0] == 0: return np.empty((0, 2, 2), dtype=np.float64) - # Deduplicate 2D edges regardless of endpoint orientation - rounded = np.round(valid_edges, decimals=5) - swap_mask = (rounded[:, 0, 0] > rounded[:, 1, 0]) | ( - (rounded[:, 0, 0] == rounded[:, 1, 0]) & (rounded[:, 0, 1] > rounded[:, 1, 1]) - ) - ordered = valid_edges.copy() - ordered[swap_mask] = ordered[swap_mask][:, ::-1, :] - ordered_rounded = rounded.copy() - ordered_rounded[swap_mask] = ordered_rounded[swap_mask][:, ::-1, :] - - view = ordered_rounded.reshape(len(ordered_rounded), -1) - _, unique_indices = np.unique(view, axis=0, return_index=True) - return ordered[unique_indices] + return deduplicate_2d_edges(valid_edges) def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArray[np.float64]: @@ -240,13 +248,13 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra return canonical_2d[marked_first_indices] -def extract_outer_contour_edges(mesh_data: Any) -> NDArray[np.float64]: +def extract_boundary_edges(mesh_data: Any) -> NDArray[np.float64]: """ - Extracts top-view outer boundary / silhouette contour line segments using Shapely 2D polygon union. + Extracts 2D edge segments owned by only 1 face (single-face boundary edges). """ from ..infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject - all_tris_2d = [] + all_tris_3d = [] def process_item(item: Any): if item is None: @@ -262,58 +270,182 @@ def extract_outer_contour_edges(mesh_data: Any) -> NDArray[np.float64]: process_item(so) elif isinstance(item, SlicingObject): if item.mesh.size > 0: - verts_3d = item.mesh[:, :3, :3] # (N, 3, 3) - verts_2d = verts_3d[:, :, :2] # (N, 3, 2) - all_tris_2d.append(verts_2d) + all_tris_3d.append(item.mesh[:, :3, :3]) process_item(mesh_data) - if not all_tris_2d: + if not all_tris_3d: return np.empty((0, 2, 2), dtype=np.float64) - tris_2d = np.concatenate(all_tris_2d, axis=0) - if len(tris_2d) == 0: + tris_3d = np.concatenate(all_tris_3d, axis=0) + if len(tris_3d) == 0: return np.empty((0, 2, 2), dtype=np.float64) - polys = [] - for tri in tris_2d: - p = Polygon(tri) - if p.is_valid and p.area > 1e-6: - polys.append(p) + verts_2d = tris_3d[:, :, :2] + e0_3d = tris_3d[:, [0, 1], :] + e1_3d = tris_3d[:, [1, 2], :] + e2_3d = tris_3d[:, [2, 0], :] + edges_3d = np.concatenate([e0_3d, e1_3d, e2_3d], axis=0) - if not polys: + e0_2d = verts_2d[:, [0, 1], :] + e1_2d = verts_2d[:, [1, 2], :] + e2_2d = verts_2d[:, [2, 0], :] + edges_2d = np.concatenate([e0_2d, e1_2d, e2_2d], axis=0) + + dists_sq_2d = np.sum((edges_2d[:, 0, :] - edges_2d[:, 1, :]) ** 2, axis=1) + valid_mask = dists_sq_2d >= 1e-8 + + edges_3d = edges_3d[valid_mask] + edges_2d = edges_2d[valid_mask] + + if len(edges_3d) == 0: return np.empty((0, 2, 2), dtype=np.float64) - try: - union_poly = shapely.union_all(polys) - except Exception: - return np.empty((0, 2, 2), dtype=np.float64) + rounded_3d = np.round(edges_3d, decimals=4) + pA = rounded_3d[:, 0, :] + pB = rounded_3d[:, 1, :] + swap_mask = (pA[:, 0] > pB[:, 0]) | ( + (pA[:, 0] == pB[:, 0]) & (pA[:, 1] > pB[:, 1]) + ) | ( + (pA[:, 0] == pB[:, 0]) & (pA[:, 1] == pB[:, 1]) & (pA[:, 2] > pB[:, 2]) + ) - boundary_edges = [] + canonical_3d = rounded_3d.copy() + canonical_3d[swap_mask] = canonical_3d[swap_mask][:, ::-1, :] - def process_geom(geom): - if geom.is_empty: + canonical_2d = edges_2d.copy() + canonical_2d[swap_mask] = canonical_2d[swap_mask][:, ::-1, :] + + flat_3d = canonical_3d.reshape(len(canonical_3d), 6) + view_bytes = np.ascontiguousarray(flat_3d).view(np.dtype((np.void, 48))).ravel() + unique_keys, first_indices, inverse_indices, counts = np.unique( + view_bytes, return_index=True, return_inverse=True, return_counts=True + ) + + boundary_mask = counts == 1 + return canonical_2d[first_indices[boundary_mask]] + + +def extract_crease_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArray[np.float64]: + """ + Extracts 2D edge segments owned by 2 or more faces where the 3D dihedral angle between face normals > max_angle_deg. + """ + return extract_lineart_edges(mesh_data, max_angle_deg=max_angle_deg) + + +def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]: + """ + Extracts top-view silhouette contour edges using the user's exact backfacing rule: + Checks 2D triangle orientation (CW vs CCW). + An edge is marked for drawing if it is shared by at least one Clockwise (back-facing) triangle + AND at least one Counter-Clockwise (front-facing) triangle in +Z top-view projection. + """ + from ..infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject + + all_tris_3d = [] + + def process_item(item: Any): + if item is None: return - if isinstance(geom, Polygon): - coords = np.array(geom.exterior.coords) - for i in range(len(coords) - 1): - boundary_edges.append([coords[i], coords[i + 1]]) - for interior in geom.interiors: - icoords = np.array(interior.coords) - for i in range(len(icoords) - 1): - boundary_edges.append([icoords[i], icoords[i + 1]]) - elif isinstance(geom, MultiPolygon): - for sub in geom.geoms: - process_geom(sub) - elif hasattr(geom, 'geoms'): - for sub in geom.geoms: - process_geom(sub) + elif isinstance(item, (list, tuple)): + for sub in item: + process_item(sub) + elif isinstance(item, SlicingGroup): + for coll in item.collections.values(): + process_item(coll) + elif isinstance(item, SlicingCollection): + for so in item.objects: + process_item(so) + elif isinstance(item, SlicingObject): + if item.mesh.size > 0: + all_tris_3d.append(item.mesh[:, :3, :3]) - process_geom(union_poly) - if not boundary_edges: + process_item(mesh_data) + + if not all_tris_3d: return np.empty((0, 2, 2), dtype=np.float64) - return np.array(boundary_edges, dtype=np.float64) + tris_3d = np.concatenate(all_tris_3d, axis=0) # (N, 3, 3) + num_tris = len(tris_3d) + + if num_tris == 0: + return np.empty((0, 2, 2), dtype=np.float64) + + # Top-view 2D vertices + verts_2d = tris_3d[:, :, :2] # (N, 3, 2) + + # 2D orientation per triangle (cross product of 2D edge vectors) + v0_2d = verts_2d[:, 0, :] + v1_2d = verts_2d[:, 1, :] + v2_2d = verts_2d[:, 2, :] + cross_2d = (v1_2d[:, 0] - v0_2d[:, 0]) * (v2_2d[:, 1] - v0_2d[:, 1]) - (v1_2d[:, 1] - v0_2d[:, 1]) * (v2_2d[:, 0] - v0_2d[:, 0]) + is_ccw = cross_2d > 0 + is_cw = cross_2d < 0 + + # Extract all 3 edges per triangle + e0_3d = tris_3d[:, [0, 1], :] + e1_3d = tris_3d[:, [1, 2], :] + e2_3d = tris_3d[:, [2, 0], :] + edges_3d = np.concatenate([e0_3d, e1_3d, e2_3d], axis=0) # (3N, 2, 3) + + e0_2d = verts_2d[:, [0, 1], :] + e1_2d = verts_2d[:, [1, 2], :] + e2_2d = verts_2d[:, [2, 0], :] + edges_2d = np.concatenate([e0_2d, e1_2d, e2_2d], axis=0) # (3N, 2, 2) + + edge_ccw = np.tile(is_ccw, 3) # (3N,) + edge_cw = np.tile(is_cw, 3) # (3N,) + + # Filter out near zero-length 2D edges + dists_sq_2d = np.sum((edges_2d[:, 0, :] - edges_2d[:, 1, :]) ** 2, axis=1) + valid_len_mask = dists_sq_2d >= 1e-8 + + edges_3d = edges_3d[valid_len_mask] + edges_2d = edges_2d[valid_len_mask] + edge_ccw = edge_ccw[valid_len_mask] + edge_cw = edge_cw[valid_len_mask] + + if len(edges_3d) == 0: + return np.empty((0, 2, 2), dtype=np.float64) + + # Canonical undirected edge sorting in 3D + rounded_3d = np.round(edges_3d, decimals=4) + pA = rounded_3d[:, 0, :] + pB = rounded_3d[:, 1, :] + swap_mask = (pA[:, 0] > pB[:, 0]) | ( + (pA[:, 0] == pB[:, 0]) & (pA[:, 1] > pB[:, 1]) + ) | ( + (pA[:, 0] == pB[:, 0]) & (pA[:, 1] == pB[:, 1]) & (pA[:, 2] > pB[:, 2]) + ) + + canonical_3d = rounded_3d.copy() + canonical_3d[swap_mask] = canonical_3d[swap_mask][:, ::-1, :] + + canonical_2d = edges_2d.copy() + canonical_2d[swap_mask] = canonical_2d[swap_mask][:, ::-1, :] + + # Unique 3D edge identification using NumPy structured view + flat_3d = canonical_3d.reshape(len(canonical_3d), 6) + view_bytes = np.ascontiguousarray(flat_3d).view(np.dtype((np.void, 48))).ravel() + unique_keys, first_indices, inverse_indices, counts = np.unique( + view_bytes, return_index=True, return_inverse=True, return_counts=True + ) + + num_unique = len(unique_keys) + + # Silhouette contour edge: owned by at least 1 CCW triangle AND at least 1 CW triangle + has_ccw = np.bincount(inverse_indices, weights=edge_ccw.astype(int), minlength=num_unique) > 0 + has_cw = np.bincount(inverse_indices, weights=edge_cw.astype(int), minlength=num_unique) > 0 + silhouette_mask = has_ccw & has_cw + + return canonical_2d[first_indices[silhouette_mask]] + + +def extract_outer_contour_edges(mesh_data: Any) -> NDArray[np.float64]: + """ + Extracts top-view contour edges using backfacing CW vs CCW triangle orientation. + """ + return extract_silhouette_contour_edges(mesh_data) def _format_pdf_lines(lines_arr: NDArray[np.float64]) -> list[str]: @@ -327,36 +459,57 @@ def _format_pdf_lines(lines_arr: NDArray[np.float64]) -> list[str]: return [f"{row[0]:.2f} {row[1]:.2f} m {row[2]:.2f} {row[3]:.2f} l" for row in flat] -def export_wireframe_to_pdf( +def export_mesh_to_pdf( edges_2d: NDArray[np.float64], filepath: str, - style: str = 'WIREFRAME', + draw_wireframe: bool = True, + draw_boundary: bool = False, + draw_contour: bool = False, + draw_crease: bool = False, crease_angle: float = 30.0, margin_mm: float = 10.0, line_width_mm: float = 0.5, stroke_color: tuple[float, float, float] = (0.0, 0.0, 0.0), bg_color: tuple[float, float, float] = (1.0, 1.0, 1.0), - raw_geometry: Any = None + raw_geometry: Any = None, + style: str | None = None # Backward compatibility ) -> None: """ Exports 2D vector geometry to a 1:1 true physical scale vector PDF file. - Styles: - - WIREFRAME: All projected 3D wireframe edges - - LINEART: Silhouette, boundary, and crease edges above threshold angle - - OUTER_CONTOUR: Silhouette boundary contour outline + Supports toggleable line layers: Wireframe, Boundary (1 face), Contour, Crease Angle. """ - if style == 'LINEART' and raw_geometry is not None: - lineart_edges = extract_lineart_edges(raw_geometry, max_angle_deg=crease_angle) - if len(lineart_edges) > 0: - edges_to_draw = lineart_edges - else: - edges_to_draw = edges_2d - elif style == 'OUTER_CONTOUR' and raw_geometry is not None: - contour_edges = extract_outer_contour_edges(raw_geometry) - if len(contour_edges) > 0: - edges_to_draw = contour_edges - else: - edges_to_draw = edges_2d + # Handle legacy style argument if provided + if style == 'LINEART': + draw_wireframe = False + draw_boundary = True + draw_crease = True + elif style == 'OUTER_CONTOUR': + draw_wireframe = False + draw_contour = True + + edge_list: list[NDArray[np.float64]] = [] + + if draw_wireframe and len(edges_2d) > 0: + edge_list.append(edges_2d) + + if draw_boundary and raw_geometry is not None: + b_edges = extract_boundary_edges(raw_geometry) + if len(b_edges) > 0: + edge_list.append(b_edges) + + if draw_contour and raw_geometry is not None: + c_edges = extract_outer_contour_edges(raw_geometry) + if len(c_edges) > 0: + edge_list.append(c_edges) + + if draw_crease and raw_geometry is not None: + cr_edges = extract_crease_edges(raw_geometry, max_angle_deg=crease_angle) + if len(cr_edges) > 0: + edge_list.append(cr_edges) + + if edge_list: + combined = np.concatenate(edge_list, axis=0) + edges_to_draw = deduplicate_2d_edges(combined) else: edges_to_draw = edges_2d @@ -412,7 +565,6 @@ def export_wireframe_to_pdf( w_grid_mm = grid_max_x - grid_min_x h_grid_mm = grid_max_y - grid_min_y - # Paper margin: user margin_mm + 2x smaller grid step in all directions total_margin_mm = margin_mm + 2.0 * minor_step w_total_mm = w_grid_mm + 2.0 * total_margin_mm @@ -629,3 +781,7 @@ def export_wireframe_to_pdf( f.write(b"startxref\n") f.write(f"{xref_offset}\n".encode("utf-8")) f.write(b"%%EOF\n") + + +# Alias for backward compatibility +export_wireframe_to_pdf = export_mesh_to_pdf diff --git a/doc/NODES.md b/doc/NODES.md index f437210..374693c 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -118,11 +118,14 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Load Gcode**: Reads a G-code file from disk. - **Gcode to File**: Writes G-code string to a file on disk. - **Automatic Binary Conversion**: If the G-code contains the comment `; binary_gcode = 1`, the node automatically converts the output to Prusa's binary G-code format (`.bgcode`) using MeatPack encoding and Heatshrink compression. -- **Wireframe to PDF**: Generates a 1:1 true-scale 2D top-view (+Z orthographic) vector PDF from a `SlicingGroup` or `SlicingCollection` with a custom grid and background color. +- **Mesh to PDF**: Generates a 1:1 true-scale 2D top-view (+Z orthographic) vector PDF from a `SlicingGroup` or `SlicingCollection` with a custom grid and background color. - **Geometry**: Input socket accepting a `SlicingGroup`, `SlicingCollection`, or `SlicingObject`. - **Folder**: Target folder or PDF file path on disk. - - **Style**: Visualization style (Wireframe, Lineart, Outer Contour). - - **Crease Angle (°)**: Maximum dihedral angle threshold (default 30°) for marking feature crease lines when in Lineart mode. + - **Wireframe**: Checkbox to render all projected 3D wireframe edges. + - **Boundary (1 Face)**: Checkbox to render open mesh boundary edges owned by only 1 face. + - **Contour**: Checkbox to render outer boundary silhouette contour. + - **Crease Angle**: Checkbox to render crease lines above specified dihedral angle. + - **Angle (°)**: Maximum dihedral angle threshold (default 30°) for marking feature crease lines when Crease Angle is enabled. - **Line Width (mm)**: Stroke width in millimeters for the exported lines. - **Margin (mm)**: Margin around the geometry grid in mm. - **Background**: Color picker for the canvas background. diff --git a/tests/test_pdf_exporter.py b/tests/test_pdf_exporter.py index 9a96d17..0305fe2 100644 --- a/tests/test_pdf_exporter.py +++ b/tests/test_pdf_exporter.py @@ -158,7 +158,7 @@ class TestPdfExporter(unittest.TestCase): self.assertIn(b"/GS100 gs", content) self.assertTrue(content.endswith(b"%%EOF\n")) - def test_export_pdf_visualization_styles(self): + def test_export_pdf_line_layers(self): so = SlicingObject() tri = np.array([ [0.0, 0.0, 0.0], @@ -170,15 +170,22 @@ class TestPdfExporter(unittest.TestCase): edges_2d = extract_unique_top_view_edges(so) - for style in ['WIREFRAME', 'LINEART', 'OUTER_CONTOUR']: + 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_{style}.pdf") + pdf_path = os.path.join(tmpdir, f"test_layers_{i}.pdf") export_wireframe_to_pdf( edges_2d, pdf_path, - style=style, crease_angle=30.0, - raw_geometry=so + raw_geometry=so, + **cfg ) self.assertTrue(os.path.exists(pdf_path)) with open(pdf_path, "rb") as f: