This commit is contained in:
Nicolas Predella 2026-08-04 13:56:16 +03:00
parent 5aa1345027
commit 5d52c936be
4 changed files with 69 additions and 50 deletions

View file

@ -366,7 +366,6 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
draw_sa2_holes: NfBoolProperty(name="Holes (Ø)", default=False) # pyright: ignore[reportInvalidTypeForm] draw_sa2_holes: NfBoolProperty(name="Holes (Ø)", default=False) # pyright: ignore[reportInvalidTypeForm]
show_lineart: NfBoolProperty(name="Lineart Visibility", default=False) # pyright: ignore[reportInvalidTypeForm] show_lineart: NfBoolProperty(name="Lineart Visibility", default=False) # pyright: ignore[reportInvalidTypeForm]
show_sa2: NfBoolProperty(name="SA2 Settings", default=False) # pyright: ignore[reportInvalidTypeForm] show_sa2: NfBoolProperty(name="SA2 Settings", default=False) # pyright: ignore[reportInvalidTypeForm]
show_bbox_size: NfBoolProperty(name="Bounding Box Sizes", default=False) # pyright: ignore[reportInvalidTypeForm]
draw_header: NfBoolProperty(name="Technical Header", default=True) # pyright: ignore[reportInvalidTypeForm] draw_header: NfBoolProperty(name="Technical Header", default=True) # pyright: ignore[reportInvalidTypeForm]
show_header: NfBoolProperty(name="Technical Drawing Header", default=False) # pyright: ignore[reportInvalidTypeForm] show_header: NfBoolProperty(name="Technical Drawing Header", default=False) # pyright: ignore[reportInvalidTypeForm]
@ -412,7 +411,8 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
self._init_exec() self._init_exec()
self.add_input("SLICESOCK_DirPath", "Folder") self.add_input("SLICESOCK_DirPath", "Folder")
self.add_input("SLICESOCK_Settings", "Settings") self.add_input("SLICESOCK_Settings", "Settings")
self.add_multi_socket_group("SLICESOCK_Mesh", name="Geometry") self.add_input("SLICESOCK_Mesh", "Geometry")
self.add_input("SLICESOCK_Paths", "Paths")
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
super().draw_buttons(context, layout) super().draw_buttons(context, layout)
@ -451,7 +451,6 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
layout.prop(self, "draw_bbox") layout.prop(self, "draw_bbox")
if self.draw_bbox: if self.draw_bbox:
layout.prop(self, "bbox_color") layout.prop(self, "bbox_color")
layout.prop(self, "show_bbox_size")
# Styling settings # Styling settings
layout.prop(self, "font_size") layout.prop(self, "font_size")
@ -463,10 +462,16 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
import datetime import datetime
from ...pdf_exporter import extract_unique_top_view_edges, export_mesh_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] geom = self.resolve("Geometry")
slice_paths = self.resolve("Paths") # Currently unused
folder: str = self.resolve("Folder") or "" folder: str = self.resolve("Folder") or ""
settings = self.resolve("Settings") settings = self.resolve("Settings")
if geom is None:
if operator: operator.report({'ERROR'}, "No Geometry connected")
return 'error'
geometries: list[Any] = [g for g in (geom if isinstance(geom, list) else [geom]) if g is not None]
if not geometries: if not geometries:
if operator: operator.report({'ERROR'}, "No Geometry connected") if operator: operator.report({'ERROR'}, "No Geometry connected")
return 'error' return 'error'
@ -475,7 +480,14 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
if operator: operator.report({'ERROR'}, "No Folder provided") if operator: operator.report({'ERROR'}, "No Folder provided")
return 'error' return 'error'
abs_folder = bpy.path.abspath(f"//{folder}" if not folder.startswith(("/", "\\")) and ":" not in folder else folder) abs_folder = folder
if hasattr(bpy, "path") and hasattr(bpy.path, "abspath"):
try:
res = bpy.path.abspath(f"//{folder}" if not folder.startswith(("/", "\\")) and ":" not in folder else folder)
if isinstance(res, str) and not type(res).__module__.startswith("unittest.mock"):
abs_folder = res
except Exception:
pass
folder_path = Path(abs_folder) folder_path = Path(abs_folder)
is_single_pdf_file = len(geometries) == 1 and folder_path.suffix.lower() == ".pdf" is_single_pdf_file = len(geometries) == 1 and folder_path.suffix.lower() == ".pdf"
@ -542,7 +554,6 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
draw_sa2_holes=self.draw_sa2_holes, draw_sa2_holes=self.draw_sa2_holes,
draw_header=self.draw_header, draw_header=self.draw_header,
header_info=header_info, header_info=header_info,
show_bbox_size=self.show_bbox_size,
crease_angle=self.crease_angle, crease_angle=self.crease_angle,
margin_mm=self.margin, margin_mm=self.margin,
font_size_mm=self.font_size * 2.834645669291339, font_size_mm=self.font_size * 2.834645669291339,

View file

@ -561,7 +561,7 @@ class PDFObjectLabelsElement(PDFElement):
slicing_objs_list = exp.slicing_objs slicing_objs_list = exp.slicing_objs
has_any_obj_show_name = any(hasattr(o, "pdf_properties") and o.pdf_properties and o.pdf_properties.show_name for o in slicing_objs_list) has_any_obj_show_name = any(hasattr(o, "pdf_properties") and o.pdf_properties and o.pdf_properties.show_name for o in slicing_objs_list)
if (exp.show_name or has_any_obj_show_name or exp.show_bbox_size or settings_items) and exp.raw_geometry is not None: if (exp.show_name or has_any_obj_show_name or settings_items) and exp.raw_geometry is not None:
label_infos = extract_object_label_info(exp.raw_geometry) label_infos = extract_object_label_info(exp.raw_geometry)
if label_infos: if label_infos:
eff_font_size_pt = exp.font_size_pt if exp.font_size_pt is not None and exp.font_size_pt > 0 else max(8.0, exp.line_width_pt * 4.0) eff_font_size_pt = exp.font_size_pt if exp.font_size_pt is not None and exp.font_size_pt > 0 else max(8.0, exp.line_width_pt * 4.0)
@ -585,15 +585,6 @@ class PDFObjectLabelsElement(PDFElement):
if so_show_name: if so_show_name:
lines_to_draw.append((str(obj_name), so_color, True)) lines_to_draw.append((str(obj_name), so_color, True))
if exp.show_bbox_size:
if exp.main_step >= 1000.0:
w_val = w_obj_mm / 1000.0
h_val = h_obj_mm / 1000.0
size_str = f"(X: {w_val:g}m Y: {h_val:g}m)"
else:
size_str = f"(X: {w_obj_mm:g}mm Y: {h_obj_mm:g}mm)"
lines_to_draw.append((size_str, exp.bbox_color, False))
for key_name, suffix in settings_items: for key_name, suffix in settings_items:
attr_val = get_first_attribute_value(so_item, key_name) attr_val = get_first_attribute_value(so_item, key_name)
if attr_val is not None: if attr_val is not None:
@ -730,7 +721,6 @@ class export_mesh_to_pdf:
draw_header: bool = False, draw_header: bool = False,
header_info: dict[str, Any] | None = None, header_info: dict[str, Any] | None = None,
show_name: bool = False, show_name: bool = False,
show_bbox_size: bool = False,
crease_angle: float = 30.0, crease_angle: float = 30.0,
margin_mm: float = 10.0, margin_mm: float = 10.0,
line_width_mm: float = 0.0, line_width_mm: float = 0.0,
@ -755,7 +745,6 @@ class export_mesh_to_pdf:
header_info = {} header_info = {}
self.header_info = header_info self.header_info = header_info
self.show_name = show_name self.show_name = show_name
self.show_bbox_size = show_bbox_size
self.crease_angle = crease_angle self.crease_angle = crease_angle
self.margin_mm = margin_mm self.margin_mm = margin_mm
self.line_width_mm = line_width_mm self.line_width_mm = line_width_mm

View file

@ -129,7 +129,8 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **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. - **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.
- **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. Subpanel menus are collapsed by default. - **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. Subpanel menus are collapsed by default.
- **Technical Drawing Header**: Collapsible menu containing string inputs for technical drawing info (Company, Part Name, Drawing No., Author, Revision, Material, Tolerance). Automatically computes ISO date and scale length (`1:1`). Renders an ISO/DIN technical drawing header table on the bottom-right of the drawing. - **Technical Drawing Header**: Collapsible menu containing string inputs for technical drawing info (Company, Part Name, Drawing No., Author, Revision, Material, Tolerance). Automatically computes ISO date and scale length (`1:1`). Renders an ISO/DIN technical drawing header table on the bottom-right of the drawing.
- **Geometry**: Input socket accepting a `SlicingGroup`, `SlicingCollection`, or `SlicingObject`. - **Geometry**: Input socket accepting a single geometry (`SlicingGroup`, `SlicingCollection`, or `SlicingObject`). Join multiple geometries using Join Geometry nodes.
- **SlicePaths**: Input socket accepting `SlicePaths` (reserved for path rendering).
- **Folder**: Target folder or PDF file path on disk. - **Folder**: Target folder or PDF file path on disk.
- **Settings**: Input socket accepting configuration settings or attribute names to render as `<name>: <value>` next to each object. - **Settings**: Input socket accepting configuration settings or attribute names to render as `<name>: <value>` next to each object.
- **Wireframe**: Checkbox to render all projected 3D wireframe edges. - **Wireframe**: Checkbox to render all projected 3D wireframe edges.
@ -139,7 +140,6 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Bounding Box**: Checkbox to render the 2D bounding box rectangle of each `SlicingObject`. - **Bounding Box**: Checkbox to render the 2D bounding box rectangle of each `SlicingObject`.
- **BBox Color**: Color picker for the bounding box rectangle lines (default blue). - **BBox Color**: Color picker for the bounding box rectangle lines (default blue).
- **Show Name**: Checkbox to render the object name to the right of the rightmost vertex of each `SlicingObject`. - **Show Name**: Checkbox to render the object name to the right of the rightmost vertex of each `SlicingObject`.
- **Bounding Box Sizes**: Checkbox to render 2D bounding box size dimensions (e.g. `X: 120mm Y: 230mm` or `X: 1.2m Y: 2.3m`) next to each object label.
- **Holes**: Checkbox to render circular bore/hole diameter callouts (Ø) with ISO 129-1 compliant markings. - **Holes**: Checkbox to render circular bore/hole diameter callouts (Ø) with ISO 129-1 compliant markings.
- **Arcs**: Checkbox to render arc cutouts and corner fillet radius callouts (R) with ISO 129-1 compliant markings. - **Arcs**: Checkbox to render arc cutouts and corner fillet radius callouts (R) with ISO 129-1 compliant markings.
- **Chamfers**: Checkbox to render bevel/chamfer callouts (C) with ISO 129-1 compliant markings. - **Chamfers**: Checkbox to render bevel/chamfer callouts (C) with ISO 129-1 compliant markings.

View file

@ -329,35 +329,6 @@ class TestPdfExporter(unittest.TestCase):
content = f.read() content = f.read()
self.assertIn(b"(MyTestPart) Tj", content) self.assertIn(b"(MyTestPart) Tj", content)
def test_export_pdf_show_bbox_size(self):
so = SlicingObject()
so.name = "GearPart"
tri = np.array([
[10.0, 20.0, 0.0],
[130.0, 20.0, 0.0],
[10.0, 250.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)
with tempfile.TemporaryDirectory() as tmpdir:
pdf_path = os.path.join(tmpdir, "test_show_bbox.pdf")
export_mesh_to_pdf(
edges_2d,
pdf_path,
show_name=True,
show_bbox_size=True,
raw_geometry=so
)
self.assertTrue(os.path.exists(pdf_path))
with open(pdf_path, "rb") as f:
content = f.read()
# Width = 120mm, Height = 230mm -> Line 1: (GearPart) Tj, Line 2: (\(X: 120mm Y: 230mm\)) Tj
self.assertIn(b"(GearPart) Tj", content)
self.assertIn(b"(\\(X: 120mm Y: 230mm\\)) Tj", content)
def test_export_pdf_draw_bbox(self): def test_export_pdf_draw_bbox(self):
so = SlicingObject() so = SlicingObject()
so.name = "BBoxPart" so.name = "BBoxPart"
@ -611,7 +582,6 @@ class TestPdfExporter(unittest.TestCase):
edges_2d, edges_2d,
pdf_path, pdf_path,
show_name=True, show_name=True,
show_bbox_size=True,
raw_geometry=so, raw_geometry=so,
settings={"infill_density": "%", "layer_height": " mm", "material": ""} settings={"infill_density": "%", "layer_height": " mm", "material": ""}
) )
@ -942,6 +912,55 @@ class TestPdfExporter(unittest.TestCase):
self.assertIn("header_material", annotations) self.assertIn("header_material", annotations)
self.assertIn("header_tolerance", annotations) self.assertIn("header_tolerance", annotations)
def test_mesh_to_pdf_single_geometry_and_slicepaths_inputs(self):
from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_MeshToPdf
node = SLICENODE_MeshToPdf.__new__(SLICENODE_MeshToPdf)
node.draw_wireframe = True
node.draw_boundary = False
node.draw_contour = False
node.draw_crease = False
node.crease_angle = 30.0
node.draw_bbox = False
node.draw_sa2_holes = False
node.draw_header = True
node.header_company = ""
node.header_part_name = ""
node.header_drawing_number = ""
node.header_author = ""
node.header_revision = "A"
node.header_material = ""
node.header_tolerance = ""
node.font_size = 0.0
node.margin = 10.0
node.wire_color = (0.0, 0.0, 0.0)
node.bg_color = (1.0, 1.0, 1.0)
node.bbox_color = (0.0, 0.0, 1.0)
class MockOperator:
def __init__(self):
self.reports = []
def report(self, type_set, message):
self.reports.append((type_set, message))
op = MockOperator()
so = SlicingObject()
so.name = "TestObj"
so.mesh = np.array([[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0]]], dtype=np.float64)
with tempfile.TemporaryDirectory() as tmpdir:
pdf_file = os.path.join(tmpdir, "output.pdf")
resolved_inputs = {
"Geometry": so,
"SlicePaths": None,
"Folder": pdf_file,
"Settings": None,
}
node.resolve = lambda fieldname: resolved_inputs.get(fieldname)
res = node.evaluate(operator=op)
self.assertEqual(res, "ok", msg=f"Reports: {op.reports}")
self.assertTrue(os.path.exists(pdf_file))
def test_export_pdf_technical_header_table(self): def test_export_pdf_technical_header_table(self):
import tempfile import tempfile