ref
This commit is contained in:
parent
f932939534
commit
c41380f17b
5 changed files with 273 additions and 102 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue