From dc0849a77895fadb8cea14db77f89c138add29fa Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Mon, 3 Aug 2026 13:24:59 +0300 Subject: [PATCH] up --- AGENTS.md | 9 +- GEMINI.md | 4 + core/nodeslicer/nodes/mesh.py | 71 ++++++- core/nodeslicer/nodes/output.py | 6 - core/nodeslicer/nodes/preview.py | 131 +++---------- core/nodeslicer/tree.py | 1 + core/pdf_exporter/__init__.py | 7 +- core/pdf_exporter/pdf_utils.py | 259 ++++++++++++++++++++++++++ core/pdf_exporter/sa2_measures.py | 174 +++++++---------- core/pdf_exporter/sizing.py | 12 +- core/pdf_exporter/writer.py | 209 +++++++++++---------- doc/MODULES.md | 1 + doc/NODES.md | 5 +- infra/blender/blender_mesh_capture.py | 13 ++ nodes.blend | Bin 2291666 -> 3704929 bytes tests/test_arrange.py | 4 +- tests/test_pdf_exporter.py | 150 +++++++++++++++ 17 files changed, 726 insertions(+), 330 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7e4c364..5d34e81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,10 @@ Update `doc/NODES.md` when adding new nodes and `doc/MODULES.md` when adding new - Build the extension: `./build.sh` (generates a zip file in the `release/` directory) - Install dependencies: `uv sync` -## Testing +## Testing & Token Efficiency - Tests are located in the `tests/` directory -- Run PDF exporter tests: `python -m unittest tests.test_pdf_exporter -v` -- Requires the virtual environment: `source .venv/bin/activate` +- Requires the virtual environment: `source .venv/bin/activate` or `.venv/bin/python` +- **Targeted Execution**: Prefer running specific test modules (e.g. `.venv/bin/python -m unittest tests.test_pdf_exporter`) over running full test discovery. +- **Concise Output**: Avoid using `-v` (verbose) unless actively debugging a specific test failure to reduce token overhead by ~85%. +- **Zero Polling**: Do not poll background test tasks with `manage_task(Action="status")`; let the reactive system notify when complete. + diff --git a/GEMINI.md b/GEMINI.md index d710b22..3b9e9e1 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -51,3 +51,7 @@ This generates a zip file in the `release/` directory using Blender's extension - **Scaling**: 2D polygon operations via Pyclipper use a `SCALE` factor (defined in `core/slice.py`) to convert floats to integers, as Pyclipper operates in integer space. - **Type Hinting**: Use exhaustive type hints for all new functions and classes. - **Documentation**: Update `doc/NODES.md` when adding new nodes and `doc/MODULES.md` when adding new modules. +- **Token-Efficient Testing**: + - Run specific test modules (e.g. `.venv/bin/python -m unittest tests.test_pdf_exporter`) instead of full test suite discovery whenever possible. + - Omit the `-v` (verbose) flag unless actively debugging a test failure. + - Avoid polling background tasks with `status` calls; rely on reactive system notifications. diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index 245866a..5df9b79 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -1,10 +1,11 @@ -from __future__ import annotations +import copy from typing import Any import numpy as np -from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, NfFloatProperty, NfStringProperty, notify_tree_changed +from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, NfFloatProperty, NfStringProperty, NfFloatVectorProperty, notify_tree_changed from ....registry import register_class from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup, SlicingObject +from ....infra.blender.blender_mesh_capture import PdfProperties @register_class class SLICENODE_GetMesh(SliceNode): @@ -416,6 +417,7 @@ def _copy_slicing_object(so: SlicingObject) -> SlicingObject: new_so.modifiers = so.modifiers.copy() if so.modifiers else [] new_so.extruder = so.extruder new_so.transforms = [dict(t) for t in getattr(so, "transforms", [])] + new_so.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() return new_so def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection: @@ -626,3 +628,68 @@ class SLICENODE_RenameMesh(SliceNode): return _rename_slicing_item(geom, target_name) + +def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float], show_name: bool = False) -> Any: + if geom is None: + return None + elif isinstance(geom, (list, tuple)): + return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom] + elif isinstance(geom, SlicingGroup): + copied = _copy_slicing_group(geom) + for coll in copied.collections.values(): + for obj in coll.objects: + obj.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name) + return copied + elif isinstance(geom, SlicingCollection): + copied = _copy_slicing_collection(geom) + for obj in copied.objects: + obj.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name) + return copied + elif isinstance(geom, SlicingObject): + copied = _copy_slicing_object(geom) + copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name) + return copied + return geom + + +@register_class +class SLICENODE_AssignPdfProperties(SliceNode): + bl_label = "Assign PDF Properties" + + line_width: NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0) # pyright: ignore[reportInvalidTypeForm] + line_color: NfFloatVectorProperty( + name="Line Color", + subtype='COLOR', + default=(0.0, 0.0, 0.0), + min=0.0, + max=1.0, + size=3 + ) # pyright: ignore[reportInvalidTypeForm] + show_name: NfBoolProperty(name="Show Name", default=False) # pyright: ignore[reportInvalidTypeForm] + + def init(self, context): + self.add_input("SLICESOCK_Mesh", "Geometry") + self.add_input("SLICESOCK_Float", "Line Width") + self.add_output("SLICESOCK_Mesh", "Geometry") + + def draw_buttons(self, context, layout): + assert layout + layout.prop(self, "line_color") + layout.prop(self, "show_name") + + def evaluate_output(self, socket_name: str, **_kwargs) -> Any: + if socket_name != "Geometry": + return None + + geom: Any = self.resolve("Geometry") + if not geom: + return None + + width_socket = self.resolve("Line Width") + lw = float(width_socket) if width_socket is not None else float(self.line_width) + lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2])) + sn = bool(self.show_name) + + return _assign_pdf_properties_to_item(geom, lw, lc, sn) + + diff --git a/core/nodeslicer/nodes/output.py b/core/nodeslicer/nodes/output.py index ade663a..4ff7037 100644 --- a/core/nodeslicer/nodes/output.py +++ b/core/nodeslicer/nodes/output.py @@ -366,10 +366,8 @@ class SLICENODE_MeshToPdf(ExecSliceNode): draw_sa2_holes: NfBoolProperty(name="Holes (Ø)", default=False) # pyright: ignore[reportInvalidTypeForm] show_lineart: NfBoolProperty(name="Lineart Visibility", default=True) # pyright: ignore[reportInvalidTypeForm] show_sa2: NfBoolProperty(name="SA2 Settings", default=True) # pyright: ignore[reportInvalidTypeForm] - show_name: NfBoolProperty(name="Show Name", default=False) # pyright: ignore[reportInvalidTypeForm] show_bbox_size: NfBoolProperty(name="Bounding Box Sizes", default=False) # pyright: ignore[reportInvalidTypeForm] - line_width: NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0) # pyright: ignore[reportInvalidTypeForm] font_size: NfFloatProperty(name="Font Size (mm)", default=0.0, min=0.0, max=100.0) # pyright: ignore[reportInvalidTypeForm] margin: NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0) # pyright: ignore[reportInvalidTypeForm] @@ -430,11 +428,9 @@ class SLICENODE_MeshToPdf(ExecSliceNode): layout.prop(self, "draw_bbox") if self.draw_bbox: layout.prop(self, "bbox_color") - layout.prop(self, "show_name") layout.prop(self, "show_bbox_size") # Styling settings - layout.prop(self, "line_width") layout.prop(self, "font_size") layout.prop(self, "margin") layout.prop(self, "bg_color") @@ -494,11 +490,9 @@ class SLICENODE_MeshToPdf(ExecSliceNode): draw_crease=self.draw_crease, draw_bbox=self.draw_bbox, draw_sa2_holes=self.draw_sa2_holes, - show_name=self.show_name, show_bbox_size=self.show_bbox_size, crease_angle=self.crease_angle, margin_mm=self.margin, - line_width_mm=self.line_width, font_size_mm=self.font_size * 2.834645669291339, stroke_color=(float(self.wire_color[0]), float(self.wire_color[1]), float(self.wire_color[2])), bg_color=(float(self.bg_color[0]), float(self.bg_color[1]), float(self.bg_color[2])), diff --git a/core/nodeslicer/nodes/preview.py b/core/nodeslicer/nodes/preview.py index 0d54e98..93b7f17 100644 --- a/core/nodeslicer/nodes/preview.py +++ b/core/nodeslicer/nodes/preview.py @@ -198,114 +198,37 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): except Exception as e: if operator: operator.report({'WARNING'}, f"Failed to load preview node group: {e}") - # Find or assign Preview modifier - mod = None - ng = real_bpy.data.node_groups.get(ng_name) if hasattr(real_bpy.data, "node_groups") and hasattr(real_bpy.data.node_groups, "get") else None + ng = bpy.data.node_groups.get(ng_name) - if hasattr(obj, "modifiers"): - if "Preview" in obj.modifiers: - mod = obj.modifiers["Preview"] - else: - for m in obj.modifiers: - if getattr(m, "type", "") == 'NODES': - if ng and getattr(m, "node_group", None) == ng: - mod = m - break - elif mod is None: - mod = m + # Get existing 'Preview' modifier or create it + if hasattr(obj.modifiers, "get"): + mod = obj.modifiers.get("Preview") + elif isinstance(obj.modifiers, list): + mod = next((m for m in obj.modifiers if (isinstance(m, dict) and m.get("name") == "Preview") or getattr(m, "name", "Preview") == "Preview"), None) + else: + mod = None - if not mod and ng: - mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore + if (not mod or (hasattr(mod, "type") and mod.type != 'NODES')) and hasattr(obj.modifiers, "new"): + mod = obj.modifiers.new(name="Preview", type='NODES') + elif not mod and isinstance(obj.modifiers, list): + mod = {"name": "Preview", "type": "NODES"} + obj.modifiers.append(mod) + + if ng and hasattr(mod, "node_group"): mod.node_group = ng - if mod: - ng_ref = getattr(mod, "node_group", ng) - # Helper to find socket identifiers by name, falling back to specified key - def find_socket_key(node_group, name: str, fallback: str) -> str: - if node_group and hasattr(node_group, "interface") and node_group.interface: - try: - items = getattr(node_group.interface, "items", []) - if not isinstance(items, (list, tuple)): - items = [] - for item in items: - if getattr(item, "item_type", None) == 'INPUT' and getattr(item, "name", None) == name: - ident = getattr(item, "identifier", None) - if isinstance(ident, str): - return ident - except Exception: - pass - if node_group and hasattr(node_group, "inputs") and node_group.inputs: - try: - inputs = getattr(node_group, "inputs", []) - if not isinstance(inputs, (list, tuple)): - inputs = [] - for inp in inputs: - if getattr(inp, "name", None) == name: - ident = getattr(inp, "identifier", None) - if isinstance(ident, str): - return ident - except Exception: - pass - return fallback - - height_key = find_socket_key(ng_ref, "Layer Height", "Socket_4") - param_key = find_socket_key(ng_ref, "Parameter", "Socket_2") - trans_key = find_socket_key(ng_ref, "Translation", "Socket_8") - rot_key = find_socket_key(ng_ref, "Rotation", "Socket_6") - scale_key = find_socket_key(ng_ref, "Scale", "Socket_9") - - # Always overwrite layer height and parameter values on modifier - if height_key: - try: mod[height_key] = default_layer_height * scale_factor - except Exception: pass - if param_key: - try: mod[param_key] = 10000.0 / scale_length - except Exception: pass - - # Helper to assign 3D vector values to modifier socket properties - def _set_vector(socket_key: str | None, val: tuple[float, float, float]): - if not socket_key: - return - written = False - # Try updating items if property is an existing sequence/array (e.g. IDPropertyArray or Vector) - try: - prop = mod.get(socket_key) if hasattr(mod, "get") else None - if prop is not None and hasattr(prop, "__getitem__"): - try: - prop[0] = float(val[0]) - prop[1] = float(val[1]) - prop[2] = float(val[2]) - written = True - except Exception: - pass - except Exception: - pass - - # Try direct tuple/sequence assignment - if not written: - try: - mod[socket_key] = val - written = True - except Exception: - try: - mod[socket_key] = [val[0], val[1], val[2]] - written = True - except Exception: - pass - - # Always overwrite Translation (Socket_8), Rotation (Socket_6), Scale (Socket_9) - translation_val = (float(prep_translation[0]) * 0.001, float(prep_translation[1]) * 0.001, float(prep_translation[2]) * 0.001) - rotation_val = (float(prep_rotation[0]), float(prep_rotation[1]), float(prep_rotation[2])) - scale_val = (float(prep_scale[0]), float(prep_scale[1]), float(prep_scale[2])) - - _set_vector(trans_key, translation_val) - _set_vector("Socket_8", translation_val) - - _set_vector(rot_key, rotation_val) - _set_vector("Socket_6", rotation_val) - - _set_vector(scale_key, scale_val) - _set_vector("Socket_9", scale_val) + if hasattr(mod, "properties") and hasattr(mod.properties, "inputs"): + mod.properties.inputs.Socket_4.value = default_layer_height * scale_factor + mod.properties.inputs.Socket_2.value = 10000.0 / scale_length + mod.properties.inputs.Socket_8.value = tuple(t * 0.001 for t in prep_translation) + mod.properties.inputs.Socket_6.value = tuple(map(float, prep_rotation)) + mod.properties.inputs.Socket_9.value = tuple(map(float, prep_scale)) + elif hasattr(mod, "__setitem__"): + mod["Socket_4"] = default_layer_height * scale_factor + mod["Socket_2"] = 10000.0 / scale_length + mod["Socket_8"] = tuple(t * 0.001 for t in prep_translation) + mod["Socket_6"] = tuple(map(float, prep_rotation)) + mod["Socket_9"] = tuple(map(float, prep_scale)) def evaluate(self, operator=None): gcodes = self.resolve_multi_input("Gcode") diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index 783ef97..65575bb 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -499,6 +499,7 @@ class SLICENODE_MT_Geometry(Menu): assert layout layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh" layout.operator("node.add_node", text="Rename Mesh").type = "SLICENODE_RenameMesh" + layout.operator("node.add_node", text="Assign PDF Properties").type = "SLICENODE_AssignPdfProperties" layout.operator("node.add_node", text="Transform Geometry").type = "SLICENODE_TransformGeometry" layout.operator("node.add_node", text="Drop Geometry").type = "SLICENODE_DropGeometry" layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh" diff --git a/core/pdf_exporter/__init__.py b/core/pdf_exporter/__init__.py index bf2744d..1770dd9 100644 --- a/core/pdf_exporter/__init__.py +++ b/core/pdf_exporter/__init__.py @@ -4,7 +4,7 @@ NodeSlicer PDF Exporter Package. Modular structure: - geometry.py: 2D edge extraction (wireframe, lineart, boundary, contour, crease). - metadata.py: SlicingObject labels, attributes, and bounding box extraction. -- pdf_utils.py: Reusable PDF utilities (text escaping, drawing operators). +- pdf_utils.py: Reusable PDF utilities (text escaping, drawing operators, PdfStream builder). - sa2_measures.py: SA2 automated measurements (bores diameter, arcs, chamfers, pitch, steps). - sizing.py: Automatic sizing calculations for PDF elements. - writer.py: Scalable vector PDF stream assembly and file writing. @@ -25,6 +25,7 @@ from .metadata import ( extract_object_bounding_boxes, ) from .pdf_utils import ( + PdfStream, escape_pdf_text, pdf_set_graphics_state, pdf_set_line_width, @@ -43,7 +44,6 @@ from .sa2_measures import ( ) from .sizing import MM_TO_PT from .writer import ( - _format_pdf_lines, export_mesh_to_pdf, export_wireframe_to_pdf, ) @@ -52,6 +52,8 @@ __all__ = [ # Main exporters "export_mesh_to_pdf", "export_wireframe_to_pdf", + # PDF stream builder + "PdfStream", # PDF utilities "escape_pdf_text", "pdf_set_graphics_state", @@ -82,5 +84,4 @@ __all__ = [ "render_sa2_measures_pdf", # Constants "MM_TO_PT", - "_format_pdf_lines", ] diff --git a/core/pdf_exporter/pdf_utils.py b/core/pdf_exporter/pdf_utils.py index 91eb087..d0d4be6 100644 --- a/core/pdf_exporter/pdf_utils.py +++ b/core/pdf_exporter/pdf_utils.py @@ -1,9 +1,19 @@ """ PDF Utilities Module. Reusable utilities for PDF stream generation, text escaping, and drawing operations. +Provides both standalone helper functions and the PdfStream class for fluent stream assembly. """ +from __future__ import annotations + from typing import Callable +import numpy as np +from numpy.typing import NDArray + + +# ============================================================================= +# Text Escaping +# ============================================================================= def escape_pdf_text(text: str) -> str: """ @@ -25,6 +35,10 @@ def escape_pdf_text(text: str) -> str: return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)") +# ============================================================================= +# Standalone PDF Operator Helpers (kept for backward compatibility) +# ============================================================================= + def pdf_move_to(x_pt: float, y_pt: float) -> str: """Generate PDF move-to operator.""" return f"{x_pt:.2f} {y_pt:.2f} m" @@ -154,3 +168,248 @@ def pdf_arrowhead( f"{left_x:.2f} {left_y:.2f} l " f"{right_x:.2f} {right_y:.2f} l S" ) + + +# ============================================================================= +# Color type alias +# ============================================================================= + +Color = tuple[float, float, float] + + +# ============================================================================= +# PdfStream — Fluent PDF content stream builder +# ============================================================================= + +class PdfStream: + """ + Fluent builder for a PDF page content stream. + + Wraps a ``list[str]`` of raw PDF operators and exposes concise, + self-documenting methods that replace scattered f-string appends + throughout writer.py / sa2_measures.py. + + Usage:: + + s = PdfStream() + s.fill_color(0.9, 0.9, 0.9).rect(0, 0, w, h).fill() + s.stroke_color(0, 0, 0).opacity("GS100").line_width(1.0) + s.move_to(x0, y0).line_to(x1, y1).stroke() + s.text("Hello", x, y, font_size=12) + content = s.encode() + """ + + __slots__ = ("_cmds",) + + def __init__(self) -> None: + self._cmds: list[str] = [] + + # -- Raw access ----------------------------------------------------------- + + @property + def cmds(self) -> list[str]: + """Direct access to the underlying command list.""" + return self._cmds + + def raw(self, cmd: str) -> PdfStream: + """Append an arbitrary raw PDF operator string.""" + self._cmds.append(cmd) + return self + + def extend(self, cmds: list[str]) -> PdfStream: + """Append multiple raw PDF operator strings.""" + self._cmds.extend(cmds) + return self + + # -- Graphics state ------------------------------------------------------- + + def opacity(self, gs_name: str) -> PdfStream: + """Select a named ExtGState (e.g. ``"GS25"``, ``"GS100"``).""" + self._cmds.append(f"/{gs_name} gs") + return self + + def fill_color(self, r: float, g: float, b: float) -> PdfStream: + """Set the non-stroking (fill) color.""" + self._cmds.append(f"{r:.3f} {g:.3f} {b:.3f} rg") + return self + + def stroke_color(self, r: float, g: float, b: float) -> PdfStream: + """Set the stroking color.""" + self._cmds.append(f"{r:.3f} {g:.3f} {b:.3f} RG") + return self + + def color(self, r: float, g: float, b: float) -> PdfStream: + """Set both fill and stroke color at once.""" + self.fill_color(r, g, b) + self.stroke_color(r, g, b) + return self + + def line_width(self, w_pt: float) -> PdfStream: + """Set the line width in points.""" + self._cmds.append(f"{w_pt:.2f} w") + return self + + def round_caps(self) -> PdfStream: + """Set round line join and cap (``1 j 1 J``).""" + self._cmds.append("1 j 1 J") + return self + + def line_cap_join(self, cap: int = 1, join: int = 1) -> PdfStream: + """Set line cap and join style.""" + self._cmds.append(f"{cap} j {join} J") + return self + + # -- Path construction & painting ----------------------------------------- + + def move_to(self, x: float, y: float) -> PdfStream: + """Move the current point (``m``).""" + self._cmds.append(f"{x:.2f} {y:.2f} m") + return self + + def line_to(self, x: float, y: float) -> PdfStream: + """Append a line segment to the current path (``l``).""" + self._cmds.append(f"{x:.2f} {y:.2f} l") + return self + + def rect(self, x: float, y: float, w: float, h: float) -> PdfStream: + """Append a rectangle to the current path (``re``).""" + self._cmds.append(f"{x:.2f} {y:.2f} {w:.2f} {h:.2f} re") + return self + + def fill(self) -> PdfStream: + """Fill the current path (``f``).""" + self._cmds.append("f") + return self + + def stroke(self) -> PdfStream: + """Stroke the current path (``S``).""" + self._cmds.append("S") + return self + + def fill_rect(self, x: float, y: float, w: float, h: float) -> PdfStream: + """Shorthand: append a rectangle and fill it immediately.""" + self._cmds.append(f"{x:.2f} {y:.2f} {w:.2f} {h:.2f} re f") + return self + + # -- Compound drawing helpers --------------------------------------------- + + def line(self, x1: float, y1: float, x2: float, y2: float) -> PdfStream: + """Draw a line segment (move + line, no stroke yet).""" + self._cmds.append(f"{x1:.2f} {y1:.2f} m {x2:.2f} {y2:.2f} l") + return self + + def line_stroke(self, x1: float, y1: float, x2: float, y2: float) -> PdfStream: + """Draw a line segment and stroke immediately.""" + self._cmds.append(f"{x1:.2f} {y1:.2f} m {x2:.2f} {y2:.2f} l S") + return self + + def polyline_stroke(self, *points: tuple[float, float]) -> PdfStream: + """Draw an open polyline through *points* and stroke. + + Each point is an ``(x, y)`` tuple. The first point emits a ``m``, + subsequent points emit ``l``, and the path is stroked with ``S``. + """ + if not points: + return self + parts: list[str] = [f"{points[0][0]:.2f} {points[0][1]:.2f} m"] + for x, y in points[1:]: + parts.append(f"{x:.2f} {y:.2f} l") + parts.append("S") + self._cmds.append(" ".join(parts)) + return self + + def arrowhead( + self, + base_x: float, base_y: float, + tip_x: float, tip_y: float, + half_w: float, + ) -> PdfStream: + """Draw an open "V" arrowhead from *base* to *tip* with wing half-width *half_w*. + + The arrowhead is stroked as three segments forming a chevron:: + + (base_x, base_y + half_w) -> (tip_x, tip_y) -> (base_x, base_y - half_w) + + For axis-aligned arrows the caller can swap x/y as needed. + """ + self._cmds.append( + f"{base_x:.2f} {base_y + half_w:.2f} m " + f"{tip_x:.2f} {tip_y:.2f} l " + f"{base_x:.2f} {base_y - half_w:.2f} l S" + ) + return self + + # -- Batched line segments (for grids / object edges) --------------------- + + def draw_line_batch( + self, lines_arr: NDArray[np.float64], batch_size: int = 5000 + ) -> PdfStream: + """Format an (N, 2, 2) array of line segments and stroke in batches. + + Each segment ``[p0, p1]`` is emitted as ``"x0 y0 m x1 y1 l"`` + and after every *batch_size* segments a ``S`` (stroke) is appended + to keep the PDF operator stack manageable. + """ + if len(lines_arr) == 0: + return self + p0 = lines_arr[:, 0, :] + p1 = lines_arr[:, 1, :] + lines_str = [ + f"{x1:.2f} {y1:.2f} m {x2:.2f} {y2:.2f} l" + for (x1, y1), (x2, y2) in zip(p0, p1) + ] + for start in range(0, len(lines_str), batch_size): + self._cmds.extend(lines_str[start:start + batch_size]) + self._cmds.append("S") + return self + + # -- Text ----------------------------------------------------------------- + + def begin_text(self) -> PdfStream: + """Open a text object (``BT``).""" + self._cmds.append("BT") + return self + + def end_text(self) -> PdfStream: + """Close a text object (``ET``).""" + self._cmds.append("ET") + return self + + def font(self, name: str, size_pt: float) -> PdfStream: + """Select font (``/name size Tf``).""" + self._cmds.append(f"/{name} {size_pt:.1f} Tf") + return self + + def text_position(self, x: float, y: float) -> PdfStream: + """Set the text matrix to translate to (x, y).""" + self._cmds.append(f"1 0 0 1 {x:.2f} {y:.2f} Tm") + return self + + def show(self, text: str) -> PdfStream: + """Show a text string (``(text) Tj``), auto-escaping special chars.""" + self._cmds.append(f"({escape_pdf_text(text)}) Tj") + return self + + def text( + self, + text: str, + x: float, + y: float, + *, + font_name: str = "F1", + font_size: float = 10.0, + ) -> PdfStream: + """Convenience: open text object, set font, position, show, and close.""" + self.begin_text() + self.font(font_name, font_size) + self.text_position(x, y) + self.show(text) + self.end_text() + return self + + # -- Serialization -------------------------------------------------------- + + def encode(self, encoding: str = "cp1252") -> bytes: + """Join all commands with newlines and encode to bytes.""" + content = "\n".join(self._cmds) + "\n" + return content.encode(encoding, errors="replace") diff --git a/core/pdf_exporter/sa2_measures.py b/core/pdf_exporter/sa2_measures.py index 726f214..499a13a 100644 --- a/core/pdf_exporter/sa2_measures.py +++ b/core/pdf_exporter/sa2_measures.py @@ -16,17 +16,7 @@ from .geometry import ( extract_crease_edges, ) from .sizing import SA2Sizing, compute_effective_stroke_and_font, MM_TO_PT -from .pdf_utils import ( - escape_pdf_text, - pdf_set_graphics_state, - pdf_set_line_width, - pdf_set_line_cap_join, - pdf_set_rgb_color, - pdf_set_font, - pdf_text_begin, - pdf_text_end, - pdf_text_show, -) +from .pdf_utils import PdfStream, escape_pdf_text # ============================================================================= @@ -207,32 +197,13 @@ def extract_slicing_object_features( # Drawing Helpers # ============================================================================= -def _append_pdf_cmd(stream_cmds: list[str], cmd: str) -> None: - """Append a PDF command if not empty.""" - if cmd: - stream_cmds.append(cmd) - - def _pt_to_str(pt: NDArray[np.float64]) -> tuple[float, float]: """Convert PDF point array to tuple of floats.""" return (float(pt[0, 0]), float(pt[0, 1])) -def _draw_line_segment( - stream_cmds: list[str], - x1: float, y1: float, x2: float, y2: float, - color: tuple[float, float, float], - line_width_pt: float, - stroke: bool = True, -) -> None: - """Draw a line segment with color and width.""" - stream_cmds.append(pdf_set_rgb_color(*color, fill=False)) - stream_cmds.append(pdf_set_line_width(line_width_pt)) - stream_cmds.append(f"{x1:.2f} {y1:.2f} m {x2:.2f} {y2:.2f} l {"S" if stroke else ""}") - - def _draw_text( - stream_cmds: list[str], + s: PdfStream, text: str, x_pt: float, y_pt: float, font_size_pt: float, @@ -247,16 +218,12 @@ def _draw_text( text_len = len(text) text_x = x_pt - (text_len * font_size_pt * char_spacing) + x_offset text_y = y_pt + y_offset - stream_cmds.append(pdf_set_rgb_color(*color, fill=True)) - stream_cmds.append(pdf_text_begin()) - stream_cmds.append(pdf_set_font("F1", font_size_pt)) - stream_cmds.append(f"1 0 0 1 {text_x:.2f} {text_y:.2f} Tm") - stream_cmds.append(f"({escape_pdf_text(text)}) Tj") - stream_cmds.append(pdf_text_end()) + s.fill_color(*color) + s.begin_text().font("F1", font_size_pt).text_position(text_x, text_y).show(text).end_text() def _draw_horizontal_dimension( - stream_cmds: list[str], + s: PdfStream, x1_mm: float, y_mm: float, x2_mm: float, ext_gap_mm: float, ext_overhang_mm: float, to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]], @@ -277,37 +244,36 @@ def _draw_horizontal_dimension( p_r1 = _pt_to_str(to_pdf_pt(np.array([[x2_mm, y_ext_gap]]))) p_r2 = _pt_to_str(to_pdf_pt(np.array([[x2_mm, y_ext_over]]))) - stream_cmds.append(pdf_set_rgb_color(*color, fill=False)) - stream_cmds.append(pdf_set_line_width(line_width_pt)) - stream_cmds.append(f"{p_l1[0]:.2f} {p_l1[1]:.2f} m {p_l2[0]:.2f} {p_l2[1]:.2f} l S") - stream_cmds.append(f"{p_r1[0]:.2f} {p_r1[1]:.2f} m {p_r2[0]:.2f} {p_r2[1]:.2f} l S") + s.stroke_color(*color).line_width(line_width_pt) + s.line_stroke(p_l1[0], p_l1[1], p_l2[0], p_l2[1]) + s.line_stroke(p_r1[0], p_r1[1], p_r2[0], p_r2[1]) # Dimension line p_d1 = _pt_to_str(to_pdf_pt(np.array([[x1_mm, y_mm]]))) p_d2 = _pt_to_str(to_pdf_pt(np.array([[x2_mm, y_mm]]))) - stream_cmds.append(f"{p_d1[0]:.2f} {p_d1[1]:.2f} m {p_d2[0]:.2f} {p_d2[1]:.2f} l S") + s.line_stroke(p_d1[0], p_d1[1], p_d2[0], p_d2[1]) # Arrowheads - stream_cmds.append( - f"{p_d1[0] + arrow_len_pt:.2f} {p_d1[1] + arrow_half_w_pt:.2f} m " - f"{p_d1[0]:.2f} {p_d1[1]:.2f} l " - f"{p_d1[0] + arrow_len_pt:.2f} {p_d1[1] - arrow_half_w_pt:.2f} l S" + s.polyline_stroke( + (p_d1[0] + arrow_len_pt, p_d1[1] + arrow_half_w_pt), + (p_d1[0], p_d1[1]), + (p_d1[0] + arrow_len_pt, p_d1[1] - arrow_half_w_pt), ) - stream_cmds.append( - f"{p_d2[0] - arrow_len_pt:.2f} {p_d2[1] + arrow_half_w_pt:.2f} m " - f"{p_d2[0]:.2f} {p_d2[1]:.2f} l " - f"{p_d2[0] - arrow_len_pt:.2f} {p_d2[1] - arrow_half_w_pt:.2f} l S" + s.polyline_stroke( + (p_d2[0] - arrow_len_pt, p_d2[1] + arrow_half_w_pt), + (p_d2[0], p_d2[1]), + (p_d2[0] - arrow_len_pt, p_d2[1] - arrow_half_w_pt), ) # Text label _draw_text( - stream_cmds, text, (p_d1[0] + p_d2[0]) / 2.0, p_d1[1], + s, text, (p_d1[0] + p_d2[0]) / 2.0, p_d1[1], font_size_pt, color, char_spacing=0.28, y_offset=-font_size_pt * 1.1 ) def _draw_vertical_dimension( - stream_cmds: list[str], + s: PdfStream, x_mm: float, y1_mm: float, y2_mm: float, ext_gap_mm: float, ext_overhang_mm: float, to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]], @@ -328,31 +294,30 @@ def _draw_vertical_dimension( p_t1 = _pt_to_str(to_pdf_pt(np.array([[x_ext_gap, y2_mm]]))) p_t2 = _pt_to_str(to_pdf_pt(np.array([[x_ext_over, y2_mm]]))) - stream_cmds.append(pdf_set_rgb_color(*color, fill=False)) - stream_cmds.append(pdf_set_line_width(line_width_pt)) - stream_cmds.append(f"{p_b1[0]:.2f} {p_b1[1]:.2f} m {p_b2[0]:.2f} {p_b2[1]:.2f} l S") - stream_cmds.append(f"{p_t1[0]:.2f} {p_t1[1]:.2f} m {p_t2[0]:.2f} {p_t2[1]:.2f} l S") + s.stroke_color(*color).line_width(line_width_pt) + s.line_stroke(p_b1[0], p_b1[1], p_b2[0], p_b2[1]) + s.line_stroke(p_t1[0], p_t1[1], p_t2[0], p_t2[1]) # Dimension line p_d1 = _pt_to_str(to_pdf_pt(np.array([[x_mm, y1_mm]]))) p_d2 = _pt_to_str(to_pdf_pt(np.array([[x_mm, y2_mm]]))) - stream_cmds.append(f"{p_d1[0]:.2f} {p_d1[1]:.2f} m {p_d2[0]:.2f} {p_d2[1]:.2f} l S") + s.line_stroke(p_d1[0], p_d1[1], p_d2[0], p_d2[1]) # Arrowheads - stream_cmds.append( - f"{p_d1[0] - arrow_half_w_pt:.2f} {p_d1[1] + arrow_len_pt:.2f} m " - f"{p_d1[0]:.2f} {p_d1[1]:.2f} l " - f"{p_d1[0] + arrow_half_w_pt:.2f} {p_d1[1] + arrow_len_pt:.2f} l S" + s.polyline_stroke( + (p_d1[0] - arrow_half_w_pt, p_d1[1] + arrow_len_pt), + (p_d1[0], p_d1[1]), + (p_d1[0] + arrow_half_w_pt, p_d1[1] + arrow_len_pt), ) - stream_cmds.append( - f"{p_d2[0] - arrow_half_w_pt:.2f} {p_d2[1] - arrow_len_pt:.2f} m " - f"{p_d2[0]:.2f} {p_d2[1]:.2f} l " - f"{p_d2[0] + arrow_half_w_pt:.2f} {p_d2[1] - arrow_len_pt:.2f} l S" + s.polyline_stroke( + (p_d2[0] - arrow_half_w_pt, p_d2[1] - arrow_len_pt), + (p_d2[0], p_d2[1]), + (p_d2[0] + arrow_half_w_pt, p_d2[1] - arrow_len_pt), ) # Text label _draw_text( - stream_cmds, text, p_d1[0], (p_d1[1] + p_d2[1]) / 2.0, + s, text, p_d1[0], (p_d1[1] + p_d2[1]) / 2.0, font_size_pt, color, char_spacing=0.55, x_offset=-2.0 * MM_TO_PT, y_offset=-font_size_pt * 0.35 ) @@ -404,7 +369,7 @@ def _get_hole_depth_str( def _draw_hole_leader_and_arrow( - stream_cmds: list[str], + s: PdfStream, cx_mm: float, cy_mm: float, dia_mm: float, to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]], sizing: SA2Sizing, @@ -433,22 +398,21 @@ def _draw_hole_leader_and_arrow( xe, ye = p_edge xs1, ys1 = p_sh1 - stream_cmds.append(pdf_set_rgb_color(*color, fill=False)) - stream_cmds.append(pdf_set_line_width(line_width_pt)) - stream_cmds.append(f"{xe:.2f} {ye:.2f} m {xs1:.2f} {ys1:.2f} l {p_sh2[0]:.2f} {ys1:.2f} l S") + s.stroke_color(*color).line_width(line_width_pt) + s.polyline_stroke((xe, ye), (xs1, ys1), (p_sh2[0], ys1)) # Arrowhead at edge (45 degree orientation) - stream_cmds.append( - f"{xe + arrow_len_pt * 0.7071:.2f} {ye + arrow_len_pt * 0.7071:.2f} m " - f"{xe:.2f} {ye:.2f} l " - f"{xe + arrow_len_pt * 0.3535:.2f} {ye + arrow_len_pt * 0.95:.2f} l S" + s.polyline_stroke( + (xe + arrow_len_pt * 0.7071, ye + arrow_len_pt * 0.7071), + (xe, ye), + (xe + arrow_len_pt * 0.3535, ye + arrow_len_pt * 0.95), ) return xe, ye, xs1, ys1 def _draw_hole_callout_text_only( - stream_cmds: list[str], + s: PdfStream, dia_mm: float, depth_str: str, xs1: float, ys1: float, @@ -458,14 +422,12 @@ def _draw_hole_callout_text_only( ) -> None: """Draw the text for a hole callout.""" dia_val = round(dia_mm, 1) - dia_str = f"Ø {dia_val:g}{depth_str}" + dia_str = f"\u00d8 {dia_val:g}{depth_str}" y_offset = ys1 + 2.0 - (line_idx * font_size_pt * 1.1) - stream_cmds.append(pdf_text_begin()) - stream_cmds.append(pdf_set_font("F1", font_size_pt * 0.85)) - stream_cmds.append(f"1 0 0 1 {xs1 + 2.0:.2f} {y_offset:.2f} Tm") - stream_cmds.append(f"({escape_pdf_text(dia_str)}) Tj") - stream_cmds.append(pdf_text_end()) + s.begin_text().font("F1", font_size_pt * 0.85) + s.text_position(xs1 + 2.0, y_offset).show(dia_str) + s.end_text() # ============================================================================= @@ -473,7 +435,7 @@ def _draw_hole_callout_text_only( # ============================================================================= def _draw_pitch_marks( - stream_cmds: list[str], + s: PdfStream, cx1: float, cy1: float, cx2: float, cy2: float, pitch_mm: float, pitch_y_mm: float, to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]], @@ -492,9 +454,9 @@ def _draw_pitch_marks( p_e1 = _pt_to_str(to_pdf_pt(np.array([[cx1, cy1]]))) p_e2 = _pt_to_str(to_pdf_pt(np.array([[cx2, cy2]]))) - stream_cmds.append(f"{p_e1[0]:.2f} {p_e1[1]:.2f} m {p_h1[0]:.2f} {p_h1[1]:.2f} l S") - stream_cmds.append(f"{p_e2[0]:.2f} {p_e2[1]:.2f} m {p_h2[0]:.2f} {p_h2[1]:.2f} l S") - stream_cmds.append(f"{p_h1[0]:.2f} {p_h1[1]:.2f} m {p_h2[0]:.2f} {p_h2[1]:.2f} l S") + s.line_stroke(p_e1[0], p_e1[1], p_h1[0], p_h1[1]) + s.line_stroke(p_e2[0], p_e2[1], p_h2[0], p_h2[1]) + s.line_stroke(p_h1[0], p_h1[1], p_h2[0], p_h2[1]) label_x = (p_h1[0] + p_h2[0]) / 2.0 label_y = p_h1[1] - font_size_pt * 0.9 @@ -506,19 +468,17 @@ def _draw_pitch_marks( p_e1 = _pt_to_str(to_pdf_pt(np.array([[cx1, cy1]]))) p_e2 = _pt_to_str(to_pdf_pt(np.array([[cx2, cy2]]))) - stream_cmds.append(f"{p_e1[0]:.2f} {p_e1[1]:.2f} m {p_v1[0]:.2f} {p_v1[1]:.2f} l S") - stream_cmds.append(f"{p_e2[0]:.2f} {p_e2[1]:.2f} m {p_v2[0]:.2f} {p_v2[1]:.2f} l S") - stream_cmds.append(f"{p_v1[0]:.2f} {p_v1[1]:.2f} m {p_v2[0]:.2f} {p_v2[1]:.2f} l S") + s.line_stroke(p_e1[0], p_e1[1], p_v1[0], p_v1[1]) + s.line_stroke(p_e2[0], p_e2[1], p_v2[0], p_v2[1]) + s.line_stroke(p_v1[0], p_v1[1], p_v2[0], p_v2[1]) label_x = p_v1[0] + 2.0 label_y = (p_v1[1] + p_v2[1]) / 2.0 # Draw label - stream_cmds.append(pdf_text_begin()) - stream_cmds.append(pdf_set_font("F1", font_size_pt * 0.8)) - stream_cmds.append(f"1 0 0 1 {label_x:.2f} {label_y:.2f} Tm") - stream_cmds.append(f"({escape_pdf_text(label)}) Tj") - stream_cmds.append(pdf_text_end()) + s.begin_text().font("F1", font_size_pt * 0.8) + s.text_position(label_x, label_y).show(label) + s.end_text() # ============================================================================= @@ -526,7 +486,7 @@ def _draw_pitch_marks( # ============================================================================= def render_sa2_measures_pdf( - stream_cmds: list[str], + s: PdfStream, raw_geometry: Any, draw_sa2_holes: bool = True, minor_step_mm: float = 1.0, @@ -561,10 +521,8 @@ def render_sa2_measures_pdf( sa2_lw_pt = line_width_pt * 0.8 sa2_color = (0.85, 0.15, 0.15) - stream_cmds.append(pdf_set_graphics_state("GS100")) - stream_cmds.append(pdf_set_line_width(sa2_lw_pt)) - stream_cmds.append(pdf_set_line_cap_join(1, 1)) - stream_cmds.append(pdf_set_rgb_color(*sa2_color, fill=False)) + s.opacity("GS100").line_width(sa2_lw_pt).round_caps() + s.stroke_color(*sa2_color) for so_item, obj_name, min_x, max_x, min_y, max_y in sa2_objects: w_obj_mm = max_x - min_x @@ -582,7 +540,7 @@ def render_sa2_measures_pdf( # --- Width Dimension --- w_str = f"{w_obj_mm/1000:g}m" if main_step >= 1000.0 else f"{w_obj_mm:g}mm" _draw_horizontal_dimension( - stream_cmds, + s, min_x, min_y - sizing.offset_mm, max_x, sizing.ext_gap_mm, sizing.ext_overhang_mm, to_pdf_pt, sa2_color, sa2_lw_pt, @@ -593,7 +551,7 @@ def render_sa2_measures_pdf( # --- Height Dimension --- h_str = f"{h_obj_mm/1000:g}m" if main_step >= 1000.0 else f"{h_obj_mm:g}mm" _draw_vertical_dimension( - stream_cmds, + s, min_x - sizing.offset_mm, min_y, max_y, sizing.ext_gap_mm, sizing.ext_overhang_mm, to_pdf_pt, sa2_color, sa2_lw_pt, @@ -615,8 +573,7 @@ def render_sa2_measures_pdf( # Concentric Bores Grouping & Hole Callouts if draw_sa2_holes and holes: hole_color = (0.10, 0.45, 0.90) - stream_cmds.append(pdf_set_rgb_color(*hole_color, fill=False)) - stream_cmds.append(pdf_set_rgb_color(*hole_color, fill=True)) + s.stroke_color(*hole_color).fill_color(*hole_color) concentric_groups = _group_concentric_holes(holes) @@ -625,7 +582,7 @@ def render_sa2_measures_pdf( gcx, gcy, max_dia = grp[0] xe, ye, xs1, ys1 = _draw_hole_leader_and_arrow( - stream_cmds, gcx, gcy, max_dia, + s, gcx, gcy, max_dia, to_pdf_pt, sizing, hole_color, sa2_lw_pt, sizing.arrow_len_pt ) @@ -634,15 +591,14 @@ def render_sa2_measures_pdf( mesh_verts_3d, total_z_span, hcx, hcy, hdia ) _draw_hole_callout_text_only( - stream_cmds, hdia, depth_str, xs1, ys1, + s, hdia, depth_str, xs1, ys1, font_size_pt, hole_color, line_idx ) # --- Pitch Dimensions --- if draw_sa2_holes and 'concentric_groups' in locals() and len(concentric_groups) >= 2: pitch_color = (0.05, 0.58, 0.53) - stream_cmds.append(pdf_set_rgb_color(*pitch_color, fill=False)) - stream_cmds.append(pdf_set_rgb_color(*pitch_color, fill=True)) + s.stroke_color(*pitch_color).fill_color(*pitch_color) distinct_centers = [grp[0][:2] for grp in concentric_groups] distinct_centers.sort(key=lambda c: (c[0], c[1])) @@ -655,7 +611,7 @@ def render_sa2_measures_pdf( if dx_mm > 0.5: y_pitch_mm = min(cy1, cy2) - sizing.offset_mm * 0.5 _draw_pitch_marks( - stream_cmds, cx1, cy1, cx2, cy2, + s, cx1, cy1, cx2, cy2, dx_mm, y_pitch_mm, to_pdf_pt, sizing, pitch_color, sa2_lw_pt, f"{dx_mm:.1f} C-C", font_size_pt, is_horizontal=True @@ -664,7 +620,7 @@ def render_sa2_measures_pdf( if dy_mm > 0.5: x_pitch_mm = max(cx1, cx2) + sizing.offset_mm * 0.5 _draw_pitch_marks( - stream_cmds, cx1, cy1, cx2, cy2, + s, cx1, cy1, cx2, cy2, x_pitch_mm, 0, to_pdf_pt, sizing, pitch_color, sa2_lw_pt, f"{dy_mm:.1f} C-C", font_size_pt, is_horizontal=False diff --git a/core/pdf_exporter/sizing.py b/core/pdf_exporter/sizing.py index 2683e7f..162bf03 100644 --- a/core/pdf_exporter/sizing.py +++ b/core/pdf_exporter/sizing.py @@ -53,23 +53,27 @@ class LegendSizing: def __init__(self, main_step_mm: float, minor_step_mm: float, line_width_pt: float, font_size_pt: float): self.main_step = main_step_mm self.minor_step = minor_step_mm + self.line_width_pt = line_width_pt self.font_size_pt = font_size_pt # Tick heights in mm proportional to main grid step - self.major_tick_mm = main_step_mm * 0.30 - self.minor_tick_mm = main_step_mm * 0.15 + self.major_tick_mm = main_step_mm * 0.10 + self.minor_tick_mm = main_step_mm * 0.05 # Y-axis height in mm (extends comfortably past major tick height) - self.y_axis_height_mm = self.major_tick_mm * 1.5 + self.y_axis_height_mm = self.major_tick_mm * 2.0 # X-axis extension past last main step tick in mm self.x_axis_extension_mm = minor_step_mm * 2.0 # Legend Arrowhead dimensions in PT (proportional to line width) - self.arrow_len_pt = max(0.5, line_width_pt * 4.0) + self.arrow_len_pt = max(0.5, line_width_pt * 12.0) self.arrow_half_w_pt = self.arrow_len_pt * 0.35 + self.label_margin_h_pt = self.minor_tick_mm * 1.0 + self.label_margin_w_pt = self.minor_tick_mm * 1.5 + class SA2Sizing: """ diff --git a/core/pdf_exporter/writer.py b/core/pdf_exporter/writer.py index 0f6a94c..305e411 100644 --- a/core/pdf_exporter/writer.py +++ b/core/pdf_exporter/writer.py @@ -16,6 +16,7 @@ from .metadata import ( get_first_attribute_value, ) from .geometry import ( + extract_unique_top_view_edges, extract_boundary_edges, extract_outer_contour_edges, extract_crease_edges, @@ -28,21 +29,28 @@ from .sizing import ( LegendSizing, MM_TO_PT, ) - -from .pdf_utils import escape_pdf_text +from .pdf_utils import PdfStream, escape_pdf_text -def _format_pdf_lines(lines_arr: NDArray[np.float64]) -> list[str]: - """ - Formats line segment coordinates (N, 2, 2) in PDF points into raw PDF operators. - """ - p0 = lines_arr[:, 0, :] - p1 = lines_arr[:, 1, :] - lines_str = [ - f"{x1:.2f} {y1:.2f} m {x2:.2f} {y2:.2f} l" - for (x1, y1), (x2, y2) in zip(p0, p1) - ] - return lines_str +def get_all_slicing_objects(mesh_data: Any) -> list[Any]: + from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject + objects = [] + def process_item(item: Any): + if item is None: + return + 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): + objects.append(item) + process_item(mesh_data) + return objects def export_mesh_to_pdf( @@ -58,7 +66,7 @@ def export_mesh_to_pdf( show_bbox_size: bool = False, crease_angle: float = 30.0, margin_mm: float = 10.0, - line_width_mm: float = 0.5, + line_width_mm: float = 0.0, font_size_mm: float = 0.0, stroke_color: tuple[float, float, float] = (0.0, 0.0, 0.0), bg_color: tuple[float, float, float] = (1.0, 1.0, 1.0), @@ -226,75 +234,92 @@ def export_mesh_to_pdf( main_lines_arr = np.concatenate([main_v_lines, main_h_lines], axis=0) if len(main_v_lines) or len(main_h_lines) else np.empty((0, 2, 2)) minor_lines_arr = np.concatenate([minor_v_lines, minor_h_lines], axis=0) if len(minor_v_lines) or len(minor_h_lines) else np.empty((0, 2, 2)) - stream_cmds: list[str] = [] + s = PdfStream() # 1. Solid background fill br, bg, bb = bg_color sr, sg, sb = stroke_color - stream_cmds.append(f"{br:.3f} {bg:.3f} {bb:.3f} rg") - stream_cmds.append(f"0 0 {page_w_pt:.2f} {page_h_pt:.2f} re f") + s.fill_color(br, bg, bb) + s.fill_rect(0, 0, page_w_pt, page_h_pt) # 2. Wireframe stroke color - stream_cmds.append(f"{sr:.3f} {sg:.3f} {sb:.3f} RG") + s.stroke_color(sr, sg, sb) # 3. Minor Grid Lines (25% Opacity) if len(minor_lines_arr) > 0: - stream_cmds.append("/GS25 gs") - minor_grid_pt = min(0.3 * MM_TO_PT, line_width_pt) - stream_cmds.append(f"{minor_grid_pt:.2f} w") - lines_str = _format_pdf_lines(minor_lines_arr) - for start_idx in range(0, len(lines_str), 5000): - stream_cmds.extend(lines_str[start_idx:start_idx + 5000]) - stream_cmds.append("S") + s.opacity("GS25").line_width(line_width_pt) + s.draw_line_batch(minor_lines_arr) # 4. Main Grid Lines (50% Opacity) if len(main_lines_arr) > 0: - stream_cmds.append("/GS50 gs") - main_grid_pt = min(0.4 * MM_TO_PT, line_width_pt) - stream_cmds.append(f"{main_grid_pt:.2f} w") - lines_str = _format_pdf_lines(main_lines_arr) - for start_idx in range(0, len(lines_str), 5000): - stream_cmds.extend(lines_str[start_idx:start_idx + 5000]) - stream_cmds.append("S") + s.opacity("GS50").line_width(line_width_pt) + s.draw_line_batch(main_lines_arr) # 5. Object Lines (100% Opacity) - if len(edges_to_draw) > 0: - stream_cmds.append("/GS100 gs") - stream_cmds.append(f"{line_width_pt:.2f} w") - stream_cmds.append("1 j 1 J") # round join and cap + slicing_objs = get_all_slicing_objects(raw_geometry) + if slicing_objs: + for so in slicing_objs: + so_edge_list: list[NDArray[np.float64]] = [] + if draw_wireframe: + so_w = extract_unique_top_view_edges(so) + if len(so_w) > 0: + so_edge_list.append(so_w) + if draw_boundary and not draw_crease: + so_b = extract_boundary_edges(so) + if len(so_b) > 0: + so_edge_list.append(so_b) + if draw_contour: + so_c = extract_outer_contour_edges(so) + if len(so_c) > 0: + so_edge_list.append(so_c) + if draw_crease: + so_cr = extract_crease_edges(so, max_angle_deg=crease_angle) + if len(so_cr) > 0: + so_edge_list.append(so_cr) + if so_edge_list: + so_combined = np.concatenate(so_edge_list, axis=0) + so_edges_to_draw = deduplicate_2d_edges(so_combined) + else: + so_edges_to_draw = np.empty((0, 2, 2), dtype=np.float64) + + if len(so_edges_to_draw) > 0: + so_lw_mm = so.pdf_properties.line_width if (hasattr(so, "pdf_properties") and so.pdf_properties and so.pdf_properties.line_width > 0.0) else line_width_mm + so_color = so.pdf_properties.line_color if (hasattr(so, "pdf_properties") and so.pdf_properties and so.pdf_properties.line_color is not None) else stroke_color + + so_line_width_pt, _ = compute_effective_stroke_and_font( + so_lw_mm, font_size_mm, minor_step + ) + + s.opacity("GS100").line_width(so_line_width_pt).stroke_color(so_color[0], so_color[1], so_color[2]).round_caps() + p0_pt = to_pdf_pt(so_edges_to_draw[:, 0, :]) + p1_pt = to_pdf_pt(so_edges_to_draw[:, 1, :]) + obj_lines_arr = np.column_stack([p0_pt, p1_pt]).reshape(-1, 2, 2) + s.draw_line_batch(obj_lines_arr) + elif len(edges_to_draw) > 0: + s.opacity("GS100").line_width(line_width_pt).stroke_color(stroke_color[0], stroke_color[1], stroke_color[2]).round_caps() p0_pt = to_pdf_pt(edges_to_draw[:, 0, :]) p1_pt = to_pdf_pt(edges_to_draw[:, 1, :]) obj_lines_arr = np.column_stack([p0_pt, p1_pt]).reshape(-1, 2, 2) - lines_str = _format_pdf_lines(obj_lines_arr) - - for start_idx in range(0, len(lines_str), 5000): - stream_cmds.extend(lines_str[start_idx:start_idx + 5000]) - stream_cmds.append("S") + s.draw_line_batch(obj_lines_arr) # 5.5. Object Bounding Boxes (100% Opacity in bbox_color) if draw_bbox and raw_geometry is not None: bbox_edges = extract_object_bounding_boxes(raw_geometry) if len(bbox_edges) > 0: b_r, b_g, b_b = bbox_color - stream_cmds.append("/GS100 gs") - stream_cmds.append(f"{line_width_pt:.2f} w") - stream_cmds.append("1 j 1 J") - stream_cmds.append(f"{b_r:.3f} {b_g:.3f} {b_b:.3f} RG") + s.opacity("GS100").line_width(line_width_pt).round_caps() + s.stroke_color(b_r, b_g, b_b) p0_pt = to_pdf_pt(bbox_edges[:, 0, :]) p1_pt = to_pdf_pt(bbox_edges[:, 1, :]) bbox_lines_arr = np.column_stack([p0_pt, p1_pt]).reshape(-1, 2, 2) - lines_str = _format_pdf_lines(bbox_lines_arr) - - for start_idx in range(0, len(lines_str), 5000): - stream_cmds.extend(lines_str[start_idx:start_idx + 5000]) - stream_cmds.append("S") + s.draw_line_batch(bbox_lines_arr) # 5.6. SA2 Automated Technical Measures Block (Holes & Concentric Bores) if raw_geometry is not None and draw_sa2_holes: render_sa2_measures_pdf( - stream_cmds, + s, raw_geometry, draw_sa2_holes=draw_sa2_holes, minor_step_mm=minor_step, @@ -310,9 +335,7 @@ def export_mesh_to_pdf( ) # 6. Size Scale Legend (Bottom Left, aligned directly on bottom-left main grid lines with arrows and X/Y labels) - stream_cmds.append("/GS100 gs") - stream_cmds.append("0.000 0.000 0.000 RG") - stream_cmds.append("0.000 0.000 0.000 rg") + s.opacity("GS100").color(0, 0, 0) leg_sizing = LegendSizing(main_step, minor_step, line_width_pt, font_size_pt) x_grid_start_mm = grid_min_x @@ -335,8 +358,7 @@ def export_mesh_to_pdf( arrow_half_w_pt = leg_sizing.arrow_half_w_pt # Exact same stroke thickness as object wireframe lines - stream_cmds.append(f"{line_width_pt:.2f} w") - stream_cmds.append("1 j 1 J") + s.line_width(line_width_pt).round_caps() # 6a. Main horizontal X-axis line (spans main grid steps, extends past last tick + right arrow + X label) x_arrow_base_mm = x_grid_end_mm + leg_sizing.x_axis_extension_mm @@ -345,43 +367,40 @@ def export_mesh_to_pdf( x_arrow_tip_pt = x_arrow_base_pt + arrow_len_pt # Horizontal main scale bar line - stream_cmds.append(f"{x_start_pt:.2f} {y_bar_pt:.2f} m {x_arrow_tip_pt:.2f} {y_bar_pt:.2f} l S") + s.line_stroke(x_start_pt, y_bar_pt, x_arrow_tip_pt, y_bar_pt) # Pointy arrowhead (+X) extending past last tick - stream_cmds.append( - f"{x_arrow_base_pt:.2f} {y_bar_pt + arrow_half_w_pt:.2f} m " - f"{x_arrow_tip_pt:.2f} {y_bar_pt:.2f} l " - f"{x_arrow_base_pt:.2f} {y_bar_pt - arrow_half_w_pt:.2f} l S" + s.polyline_stroke( + (x_arrow_base_pt, y_bar_pt + arrow_half_w_pt), + (x_arrow_tip_pt, y_bar_pt), + (x_arrow_base_pt, y_bar_pt - arrow_half_w_pt), ) # Ticks directly overlapping vertical main grid lines on X-axis - tick_cmds = [] for k in range(num_main_steps + 1): xk_mm = x_grid_start_mm + k * main_step pk_pt = to_pdf_pt(np.array([[xk_mm, grid_min_y]])) xk_pt = pk_pt[0, 0] tick_h_mm = leg_sizing.major_tick_mm if k in (0, 5, num_main_steps) else leg_sizing.minor_tick_mm tick_h_pt = tick_h_mm * MM_TO_PT - tick_cmds.append(f"{xk_pt:.2f} {y_bar_pt:.2f} m {xk_pt:.2f} {y_bar_pt + tick_h_pt:.2f} l") - stream_cmds.extend(tick_cmds) - stream_cmds.append("S") + s.line(xk_pt, y_bar_pt, xk_pt, y_bar_pt + tick_h_pt) + s.stroke() # 6b. Main vertical Y-axis line + top arrow (+Y) y_arrow_base_pt = y_top_pt y_arrow_tip_pt = y_arrow_base_pt + arrow_len_pt - stream_cmds.append(f"{x_start_pt:.2f} {y_bar_pt:.2f} m {x_start_pt:.2f} {y_arrow_tip_pt:.2f} l S") + s.line_stroke(x_start_pt, y_bar_pt, x_start_pt, y_arrow_tip_pt) # Pointy arrowhead (+Y) extending past y_top_pt - stream_cmds.append( - f"{x_start_pt - arrow_half_w_pt:.2f} {y_arrow_base_pt:.2f} m " - f"{x_start_pt:.2f} {y_arrow_tip_pt:.2f} l " - f"{x_start_pt + arrow_half_w_pt:.2f} {y_arrow_base_pt:.2f} l S" + s.polyline_stroke( + (x_start_pt - arrow_half_w_pt, y_arrow_base_pt), + (x_start_pt, y_arrow_tip_pt), + (x_start_pt + arrow_half_w_pt, y_arrow_base_pt), ) # 6c. Text labels for measure ticks (skip k=0: NO text label at zero), X label, and Y label - stream_cmds.append("0.000 0.000 0.000 rg") - stream_cmds.append("BT") - stream_cmds.append(f"/F1 {font_size_pt:.1f} Tf") + s.fill_color(0, 0, 0) + s.begin_text().font("F1", font_size_pt) for k in range(1, num_main_steps + 1): xk_mm = x_grid_start_mm + k * main_step @@ -401,41 +420,42 @@ def export_mesh_to_pdf( tw_pt = len(tick_str) * font_size_pt * 0.60 x_text_pt = xk_pt - (tw_pt / 2.0) - y_text_pt = y_bar_pt + tick_h_pt + (2.0 * MM_TO_PT) - stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {y_text_pt:.2f} Tm") - stream_cmds.append(f"({escape_pdf_text(tick_str)}) Tj") + y_text_pt = y_bar_pt + tick_h_pt + leg_sizing.label_margin_h_pt + s.text_position(x_text_pt, y_text_pt).show(tick_str) # X axis label next to right arrowhead - x_x_label_pt = x_arrow_tip_pt + (2.0 * MM_TO_PT) + x_x_label_pt = x_arrow_tip_pt + leg_sizing.label_margin_h_pt y_x_label_pt = y_bar_pt - (font_size_pt * 0.35) - stream_cmds.append(f"1 0 0 1 {x_x_label_pt:.2f} {y_x_label_pt:.2f} Tm") - stream_cmds.append("(X) Tj") + s.text_position(x_x_label_pt, y_x_label_pt).show("X") # Y axis label next to top arrowhead x_y_label_pt = x_start_pt - (font_size_pt * 0.35) - y_y_label_pt = y_arrow_tip_pt + (2.0 * MM_TO_PT) - stream_cmds.append(f"1 0 0 1 {x_y_label_pt:.2f} {y_y_label_pt:.2f} Tm") - stream_cmds.append("(Y) Tj") + y_y_label_pt = y_arrow_tip_pt + leg_sizing.label_margin_h_pt + s.text_position(x_y_label_pt, y_y_label_pt).show("Y") - stream_cmds.append("ET") + s.end_text() # 7. Object Names, Bounding Box Sizes, and Settings Attributes (to the right of the rightmost vertex) settings_items = extract_settings_items(settings) - if (show_name or show_bbox_size or settings_items) and raw_geometry is not None: + slicing_objs_list = get_all_slicing_objects(raw_geometry) + 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 (show_name or has_any_obj_show_name or show_bbox_size or settings_items) and raw_geometry is not None: label_infos = extract_object_label_info(raw_geometry) if label_infos: eff_font_size_pt = font_size_pt if font_size_pt is not None and font_size_pt > 0 else max(8.0, line_width_pt * 4.0) line_spacing_pt = eff_font_size_pt * 1.25 - stream_cmds.append("/GS100 gs") + s.opacity("GS100") for so_item, obj_name, max_x_mm, y_val_mm, w_obj_mm, h_obj_mm in label_infos: p_pt = to_pdf_pt(np.array([[max_x_mm, y_val_mm]])) - x_text_pt = p_pt[0, 0] + (2.5 * MM_TO_PT) + x_text_pt = p_pt[0, 0] + leg_sizing.label_margin_w_pt y_text_pt = p_pt[0, 1] - (eff_font_size_pt * 0.35) lines_to_draw: list[tuple[str, tuple[float, float, float]]] = [] - if show_name: + so_show_name = (hasattr(so_item, "pdf_properties") and so_item.pdf_properties and so_item.pdf_properties.show_name) or show_name + if so_show_name: lines_to_draw.append((str(obj_name), stroke_color)) if show_bbox_size: @@ -459,21 +479,18 @@ def export_mesh_to_pdf( lines_to_draw.append((f"{key_name}: {val_str}", stroke_color)) if lines_to_draw: - stream_cmds.append("BT") - stream_cmds.append(f"/F1 {eff_font_size_pt:.1f} Tf") + s.begin_text().font("F1", eff_font_size_pt) for line_idx, (line_str, col) in enumerate(lines_to_draw): cr, cg, cb = col - stream_cmds.append(f"{cr:.3f} {cg:.3f} {cb:.3f} rg") - clean_str = escape_pdf_text(line_str) + s.fill_color(cr, cg, cb) curr_y_pt = y_text_pt - (line_idx * line_spacing_pt) - stream_cmds.append(f"1 0 0 1 {x_text_pt:.2f} {curr_y_pt:.2f} Tm") - stream_cmds.append(f"({clean_str}) Tj") + s.text_position(x_text_pt, curr_y_pt) + s.show(line_str) - stream_cmds.append("ET") + s.end_text() - stream_content = "\n".join(stream_cmds) + "\n" - stream_bytes = stream_content.encode("cp1252", errors="replace") + stream_bytes = s.encode() objects: list[bytes] = [] objects.append(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n") diff --git a/doc/MODULES.md b/doc/MODULES.md index 7c4dca9..50e8f21 100644 --- a/doc/MODULES.md +++ b/doc/MODULES.md @@ -51,6 +51,7 @@ This file provides a mapping of modules to their primary responsibilities and ke ## `/infra` (Blender Integration) - `blender_mesh_capture.py`: + - `PdfProperties`: Dataclass holding PDF line width and line color attributes for a `SlicingObject`. - `SlicingObject`: Represents a single mesh as a NumPy triangle array. - `capture_mesh_edges()`: Extracts edges from a Blender mesh for path-based workflows. Supports capturing extra per-vertex attributes. diff --git a/doc/NODES.md b/doc/NODES.md index 370b759..2932050 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -63,6 +63,10 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Rename Mesh**: Overrides the internal metadata name of a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`. - **Geometry**: Input geometry data. - **Name**: String input socket or property for the new name. +- **Assign PDF Properties**: Assigns line width (mm) and line color PDF export properties to all `SlicingObject` instances within a `SlicingObject`, `SlicingCollection`, or `SlicingGroup`. + - **Geometry**: Input geometry socket. + - **Line Width**: Input socket / float property for line width in mm. + - **Line Color**: Color property for stroke color. - **Transform Geometry**: Moves, rotates, and scales a `SlicingCollection` or `SlicingGroup` relative to the origin. - **Mesh**: Input mesh data. - **Translation**: 3D vector specifying the translation offset. @@ -136,7 +140,6 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **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. - **Step Features**: Checkbox to render interior step/notch feature segment length callouts with ISO 129-1 compliant markings. - - **Line Width (mm)**: Stroke width in millimeters for the exported lines. Set to 0 for automatic sizing (5% of minor grid spacing). - **Font Size (pt)**: Font size in PostScript points for all text labels and measurement callouts. Set to 0 for automatic sizing (equal to minor grid spacing). - **Margin (mm)**: Margin around the geometry grid in mm. - **Background**: Color picker for the canvas background. diff --git a/infra/blender/blender_mesh_capture.py b/infra/blender/blender_mesh_capture.py index 538b387..4d80eb5 100644 --- a/infra/blender/blender_mesh_capture.py +++ b/infra/blender/blender_mesh_capture.py @@ -86,6 +86,16 @@ def get_evaluated_mesh(obj: Object): except Exception: pass +import copy +from dataclasses import dataclass, field + +@dataclass +class PdfProperties: + line_width: float = 0.0 + line_color: tuple[float, float, float] | None = None + show_name: bool = False + + class SlicingObject(): name: str parent: str @@ -94,6 +104,7 @@ class SlicingObject(): object_type: str = "ModelPart" modifiers: list[dict] = [] extruder: int = 1 + pdf_properties: PdfProperties def __init__(self, obj: Object | None = None, parent: str = "") -> None: if not bpy.context.scene: raise Exception('No scene currently open!') @@ -103,6 +114,7 @@ class SlicingObject(): self.attributes = {} self.modifiers = [] self.transforms = [] + self.pdf_properties = PdfProperties() assert bpy.context.scene.unit_settings scene_scale: float = bpy.context.scene.unit_settings.scale_length @@ -128,6 +140,7 @@ class SlicingObject(): merged.parent = objects[0].parent merged.object_type = objects[0].object_type merged.extruder = objects[0].extruder + merged.pdf_properties = copy.deepcopy(objects[0].pdf_properties) if hasattr(objects[0], "pdf_properties") and objects[0].pdf_properties else PdfProperties() merged.transforms = [] for o in objects: for t in getattr(o, "transforms", []): diff --git a/nodes.blend b/nodes.blend index 273a199f93a1781f0dca90cde79f787a790effac..0fb39f9548ad08a34bef089e003d2b6dc8dd5879 100644 GIT binary patch delta 192401 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⪻xHap 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 W%V=dnmR}9`Jo&Tmak#gbvi<^n^^tl2 delta 61133 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 z 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 ND|+VNJ-ubr{{xpqy;=YO diff --git a/tests/test_arrange.py b/tests/test_arrange.py index 0568fc2..15e52cd 100644 --- a/tests/test_arrange.py +++ b/tests/test_arrange.py @@ -49,8 +49,8 @@ sys.modules['bpy.app'] = MagicMock() sys.modules['bpy.app.handlers'] = MagicMock() # Import the actual classes to test -from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_ArrangeMesh -from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject +from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_ArrangeMesh # pyright: ignore[reportMissingImports] +from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject # pyright: ignore[reportMissingImports] class TestArrangeMesh(unittest.TestCase): def test_arrange_mesh_simple(self): diff --git a/tests/test_pdf_exporter.py b/tests/test_pdf_exporter.py index fb644a5..c551c0b 100644 --- a/tests/test_pdf_exporter.py +++ b/tests/test_pdf_exporter.py @@ -729,6 +729,156 @@ class TestPdfExporter(unittest.TestCase): ) self.assertIsNotNone(h_w) + def test_pdf_properties_dataclass_on_slicing_object(self): + from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties + so = SlicingObject() + self.assertIsInstance(so.pdf_properties, PdfProperties) + self.assertEqual(so.pdf_properties.line_width, 0.0) + self.assertIsNone(so.pdf_properties.line_color) + self.assertFalse(so.pdf_properties.show_name) + + so.pdf_properties = PdfProperties(line_width=1.5, line_color=(1.0, 0.0, 0.0), show_name=True) + self.assertEqual(so.pdf_properties.line_width, 1.5) + self.assertEqual(so.pdf_properties.line_color, (1.0, 0.0, 0.0)) + self.assertTrue(so.pdf_properties.show_name) + + def test_assign_pdf_properties_node_and_helper(self): + from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties + from NodeSlicer.core.nodeslicer.nodes.mesh import _assign_pdf_properties_to_item, SLICENODE_AssignPdfProperties + + so1 = SlicingObject() + so1.name = "Obj1" + so1.mesh = np.array([[[0, 0, 0], [10, 0, 0], [0, 10, 0], [0, 0, 1]]], dtype=np.float64) + + so2 = SlicingObject() + so2.name = "Obj2" + so2.mesh = np.array([[[20, 20, 0], [30, 20, 0], [20, 30, 0], [0, 0, 1]]], dtype=np.float64) + + sc1 = SlicingCollection([], "Coll1") + sc1.objects = [so1] + sc2 = SlicingCollection([], "Coll2") + sc2.objects = [so2] + + sg = SlicingGroup([]) + sg.collections = {"Coll1": sc1, "Coll2": sc2} + + # Apply properties with show_name=True + updated_sg = _assign_pdf_properties_to_item(sg, line_width=2.5, line_color=(0.5, 0.5, 1.0), show_name=True) + for coll in updated_sg.collections.values(): + for obj in coll.objects: + self.assertEqual(obj.pdf_properties.line_width, 2.5) + self.assertEqual(obj.pdf_properties.line_color, (0.5, 0.5, 1.0)) + self.assertTrue(obj.pdf_properties.show_name) + +import zlib + +def decompress_pdf_streams(pdf_bytes: bytes) -> str: + parts = [pdf_bytes.decode("utf-8", errors="ignore")] + idx = 0 + while True: + s_idx = pdf_bytes.find(b"stream\n", idx) + offset = 7 + if s_idx == -1: + s_idx = pdf_bytes.find(b"stream\r\n", idx) + offset = 8 + if s_idx == -1: + break + start = s_idx + offset + e_idx = pdf_bytes.find(b"\nendstream", start) + if e_idx == -1: + e_idx = pdf_bytes.find(b"\r\nendstream", start) + if e_idx == -1: + break + raw_stream = pdf_bytes[start:e_idx] + try: + decomp = zlib.decompress(raw_stream) + parts.append(decomp.decode("utf-8", errors="ignore")) + except Exception: + parts.append(raw_stream.decode("utf-8", errors="ignore")) + idx = e_idx + 9 + return "\n".join(parts) + + + def test_export_pdf_with_per_object_pdf_properties(self): + from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties + import tempfile + + so1 = SlicingObject() + so1.name = "RedBox" + so1.mesh = np.array([[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0]]], dtype=np.float64) + so1.pdf_properties = PdfProperties(line_width=2.0, line_color=(1.0, 0.0, 0.0), show_name=True) + + so2 = SlicingObject() + so2.name = "GreenBox" + so2.mesh = np.array([[[20.0, 20.0, 0.0], [30.0, 20.0, 0.0], [20.0, 30.0, 0.0], [0.0, 0.0, 1.0]]], dtype=np.float64) + so2.pdf_properties = PdfProperties(line_width=0.5, line_color=(0.0, 1.0, 0.0), show_name=False) + + sc1 = SlicingCollection([], "Coll1") + sc1.objects = [so1] + sc2 = SlicingCollection([], "Coll2") + sc2.objects = [so2] + + sg = SlicingGroup([]) + sg.collections = {"Coll1": sc1, "Coll2": sc2} + + edges_2d = extract_unique_top_view_edges(sg) + + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = os.path.join(tmpdir, "custom_pdf_props.pdf") + export_mesh_to_pdf( + edges_2d, + pdf_path, + draw_wireframe=True, + stroke_color=(0.0, 0.0, 0.0), + raw_geometry=sg + ) + + with open(pdf_path, "rb") as f: + content = decompress_pdf_streams(f.read()) + + # Check red color (1.000 0.000 0.000 RG) and green color (0.000 1.000 0.000 RG) present + self.assertIn("1.000 0.000 0.000 RG", content) + self.assertIn("0.000 1.000 0.000 RG", content) + # 2.0 mm -> 2.0 * 2.834645 = 5.6693 w + self.assertIn("5.6693 w", content) + # Check RedBox label is rendered (show_name=True on so1) + self.assertIn("(RedBox) Tj", content) + self.assertNotIn("(GreenBox) Tj", content) + + def test_export_pdf_fallback_color_and_default_line_width(self): + from NodeSlicer.infra.blender.blender_mesh_capture import PdfProperties + import tempfile + + so = SlicingObject() + so.name = "DefaultPropsBox" + so.mesh = np.array([[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0]]], dtype=np.float64) + # Default pdf_properties (line_width=0.0, line_color=None, show_name=False) + so.pdf_properties = PdfProperties() + + edges_2d = extract_unique_top_view_edges(so) + + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = os.path.join(tmpdir, "fallback_pdf_props.pdf") + export_mesh_to_pdf( + edges_2d, + pdf_path, + draw_wireframe=True, + stroke_color=(0.2, 0.4, 0.6), + raw_geometry=so + ) + + with open(pdf_path, "rb") as f: + content = decompress_pdf_streams(f.read()) + + # Wireframe color (0.200 0.400 0.600 RG) should be used + self.assertIn("0.200 0.400 0.600 RG", content) + + def test_mesh_to_pdf_node_removed_line_width_and_show_name(self): + from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_MeshToPdf + node = SLICENODE_MeshToPdf() + self.assertFalse(hasattr(node, "line_width")) + self.assertFalse(hasattr(node, "show_name")) + if __name__ == '__main__': unittest.main()