This commit is contained in:
Nicolas Predella 2026-07-30 14:15:02 +03:00
parent 7e43f42c1c
commit 46582f82d6
6 changed files with 177 additions and 50 deletions

48
AGENTS.md Normal file
View file

@ -0,0 +1,48 @@
# NodeSlicer Agent Instructions
## Project Overview
NodeSlicer is a high-performance Blender extension providing a custom node-based interface for mesh slicing and G-code generation.
## Critical Context
- **Purpose**: 3D printing slicing, CNC toolpathing, and procedural geometry tasks
- **Main Technologies**: Python, Blender API (`bpy`), NumPy, Pyclipper, Shapely
- **Blender Version**: 4.2 or newer (required for the Extension platform)
- **Architecture**: Separates Blender-specific UI and data capture (`infra/`) from core geometric and slicing algorithms (`core/`)
## Development Conventions
### Registration
All Blender classes (Nodes, Sockets, Operators, Menus) must be decorated with `@register_class` from `registry.py` to ensure they are registered correctly during initialization.
### Node Evaluation
Nodes follow a pull-based evaluation model. Use `resolve_input(self.inputs.get("SocketName"))` within a node's `evaluate_output` method to trigger the evaluation of upstream nodes.
### Performance
For any geometry processing, prioritize NumPy vectorized operations over standard Python loops to maintain high performance.
### 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.
## Project Structure
- `__init__.py`: Main entry point. Defines the `NodeSlicer Tree`, custom Sockets, and all Nodes.
- `registry.py`: Utility for automatic registration of Blender classes using the `@register_class` decorator.
- `core/slice.py`: Core slicing engine (plane-mesh intersection), buffering (offsetting), and infill generation.
- `core/geometry.py`: Shared geometry utility functions.
- `infra/blender_mesh_capture.py`: Logic for extracting mesh data from Blender objects into NumPy arrays.
- `infra/blender_slice_nodes_generic.py`: Base classes for the node system and the evaluation logic (`resolve_input`).
- `doc/`: Comprehensive documentation on modules and nodes.
## Commands
- Build the extension: `./build.sh` (generates a zip file in the `release/` directory)
- Install dependencies: `uv sync`
## Testing
- 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`

View file

@ -363,12 +363,17 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
draw_crease: NfBoolProperty(name="Crease Angle", 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="Angle (°)", default=30.0, min=0.0, max=180.0) # pyright: ignore[reportInvalidTypeForm]
draw_bbox: NfBoolProperty(name="Bounding Box", default=False) # pyright: ignore[reportInvalidTypeForm] draw_bbox: NfBoolProperty(name="Bounding Box", default=False) # pyright: ignore[reportInvalidTypeForm]
draw_sa2_measures: NfBoolProperty(name="SA2 measures", default=False) # pyright: ignore[reportInvalidTypeForm] draw_sa2_holes: NfBoolProperty(name="Holes", default=False) # pyright: ignore[reportInvalidTypeForm]
draw_sa2_arcs: NfBoolProperty(name="Arcs", default=False) # pyright: ignore[reportInvalidTypeForm]
draw_sa2_chamfers: NfBoolProperty(name="Chamfers", default=False) # pyright: ignore[reportInvalidTypeForm]
draw_sa2_step_features: NfBoolProperty(name="Step Features", 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_name: NfBoolProperty(name="Show Name", default=False) # pyright: ignore[reportInvalidTypeForm]
show_bbox_size: NfBoolProperty(name="Bounding Box Sizes", 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.5, min=0.01, max=50.0) # 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 (pt)", default=8.0, min=1.0, max=100.0) # pyright: ignore[reportInvalidTypeForm] font_size: NfFloatProperty(name="Font Size (pt)", 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] margin: NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0) # pyright: ignore[reportInvalidTypeForm]
bg_color: NfFloatVectorProperty( bg_color: NfFloatVectorProperty(
@ -406,18 +411,35 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
super().draw_buttons(context, layout) super().draw_buttons(context, layout)
layout.prop(self, "draw_wireframe")
layout.prop(self, "draw_boundary") # Lineart Visibility subpanel (collapsible)
layout.prop(self, "draw_contour") layout.prop(self, "show_lineart", text="Lineart Visibility", icon='TRIA_DOWN' if self.show_lineart else 'TRIA_RIGHT')
layout.prop(self, "draw_crease") if self.show_lineart:
if self.draw_crease: box = layout.box()
layout.prop(self, "crease_angle") box.prop(self, "draw_wireframe")
box.prop(self, "draw_boundary")
box.prop(self, "draw_contour")
box.prop(self, "draw_crease")
if self.draw_crease:
box.prop(self, "crease_angle")
# SA2 Settings subpanel (collapsible)
layout.prop(self, "show_sa2", text="SA2 Settings", icon='TRIA_DOWN' if self.show_sa2 else 'TRIA_RIGHT')
if self.show_sa2:
box = layout.box()
box.prop(self, "draw_sa2_holes")
box.prop(self, "draw_sa2_arcs")
box.prop(self, "draw_sa2_chamfers")
box.prop(self, "draw_sa2_step_features")
# Display settings
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, "draw_sa2_measures")
layout.prop(self, "show_name") layout.prop(self, "show_name")
layout.prop(self, "show_bbox_size") layout.prop(self, "show_bbox_size")
# Styling settings
layout.prop(self, "line_width") layout.prop(self, "line_width")
layout.prop(self, "font_size") layout.prop(self, "font_size")
layout.prop(self, "margin") layout.prop(self, "margin")
@ -477,7 +499,10 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
draw_contour=self.draw_contour, draw_contour=self.draw_contour,
draw_crease=self.draw_crease, draw_crease=self.draw_crease,
draw_bbox=self.draw_bbox, draw_bbox=self.draw_bbox,
draw_sa2_measures=self.draw_sa2_measures, draw_sa2_holes=self.draw_sa2_holes,
draw_sa2_arcs=self.draw_sa2_arcs,
draw_sa2_chamfers=self.draw_sa2_chamfers,
draw_sa2_step_features=self.draw_sa2_step_features,
show_name=self.show_name, show_name=self.show_name,
show_bbox_size=self.show_bbox_size, show_bbox_size=self.show_bbox_size,
crease_angle=self.crease_angle, crease_angle=self.crease_angle,

View file

@ -270,24 +270,39 @@ def extract_slicing_object_features(so_item: Any) -> tuple[
def render_sa2_measures_pdf( def render_sa2_measures_pdf(
stream_cmds: list[str], stream_cmds: list[str],
raw_geometry: Any, raw_geometry: Any,
draw_sa2_measures: bool, draw_sa2_holes: bool = False,
line_width_pt: float, draw_sa2_arcs: bool = False,
main_step: float, draw_sa2_chamfers: bool = False,
to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]], draw_sa2_step_features: bool = False,
font_size_pt: float | None = None minor_step_mm: float = 1.0,
main_step: float = 10.0,
to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]] | None = None,
font_size_pt: float | None = None,
line_width_pt: float | None = None
) -> None: ) -> None:
""" """
Renders ISO 129-1 compliant SA2 automated technical measurements on PDF stream. Renders ISO 129-1 compliant SA2 automated technical measurements on PDF stream.
""" """
if not draw_sa2_measures or raw_geometry is None: if raw_geometry is None:
return
# Check if any SA2 feature is enabled
if not (draw_sa2_holes or draw_sa2_arcs or draw_sa2_chamfers or draw_sa2_step_features):
return return
sa2_objects = extract_slicing_objects_bounds(raw_geometry) sa2_objects = extract_slicing_objects_bounds(raw_geometry)
if not sa2_objects: if not sa2_objects:
return return
# Automatic sizing based on minor grid
MM_TO_PT = 2.834645669291339
if line_width_pt is None or line_width_pt <= 0:
line_width_pt = minor_step_mm * 0.05 * MM_TO_PT
if font_size_pt is None or font_size_pt <= 0:
font_size_pt = minor_step_mm * MM_TO_PT
stream_cmds.append("/GS100 gs") stream_cmds.append("/GS100 gs")
sa2_lw_pt = max(0.4 * MM_TO_PT, line_width_pt * 0.8) sa2_lw_pt = line_width_pt * 0.8
stream_cmds.append(f"{sa2_lw_pt:.2f} w") stream_cmds.append(f"{sa2_lw_pt:.2f} w")
stream_cmds.append("1 j 1 J") stream_cmds.append("1 j 1 J")
@ -295,9 +310,8 @@ def render_sa2_measures_pdf(
red_r, red_g, red_b = 0.85, 0.15, 0.15 red_r, red_g, red_b = 0.85, 0.15, 0.15
stream_cmds.append(f"{red_r:.3f} {red_g:.3f} {red_b:.3f} RG") stream_cmds.append(f"{red_r:.3f} {red_g:.3f} {red_b:.3f} RG")
if font_size_pt is None or font_size_pt <= 0: # SA2 arrow length: scale with line width (which is based on minor grid)
font_size_pt = max(8.0, line_width_pt * 4.0) arrow_len_pt = line_width_pt * 4.0
arrow_len_pt = max(3.5 * MM_TO_PT, line_width_pt * 4.0)
arrow_half_w_pt = arrow_len_pt * 0.40 arrow_half_w_pt = arrow_len_pt * 0.40
for so_item, obj_name, min_x, max_x, min_y, max_y in sa2_objects: for so_item, obj_name, min_x, max_x, min_y, max_y in sa2_objects:
@ -388,7 +402,7 @@ def render_sa2_measures_pdf(
holes, arcs, chamfers, step_features = extract_slicing_object_features(so_item) holes, arcs, chamfers, step_features = extract_slicing_object_features(so_item)
# 1. ISO 129-1 Circular Bores / Hole Diameter Callouts (Ø - Electric Blue) # 1. ISO 129-1 Circular Bores / Hole Diameter Callouts (Ø - Electric Blue)
if holes: if draw_sa2_holes and holes:
blue_r, blue_g, blue_b = 0.10, 0.45, 0.90 blue_r, blue_g, blue_b = 0.10, 0.45, 0.90
stream_cmds.append(f"{blue_r:.3f} {blue_g:.3f} {blue_b:.3f} RG") stream_cmds.append(f"{blue_r:.3f} {blue_g:.3f} {blue_b:.3f} RG")
stream_cmds.append(f"{blue_r:.3f} {blue_g:.3f} {blue_b:.3f} rg") stream_cmds.append(f"{blue_r:.3f} {blue_g:.3f} {blue_b:.3f} rg")
@ -425,7 +439,7 @@ def render_sa2_measures_pdf(
stream_cmds.append("ET") stream_cmds.append("ET")
# 2. ISO 129-1 Arc Cutouts & Corner Fillets (R - Amber Orange) # 2. ISO 129-1 Arc Cutouts & Corner Fillets (R - Amber Orange)
if arcs: if draw_sa2_arcs and arcs:
amber_r, amber_g, amber_b = 0.90, 0.45, 0.10 amber_r, amber_g, amber_b = 0.90, 0.45, 0.10
stream_cmds.append(f"{amber_r:.3f} {amber_g:.3f} {amber_b:.3f} RG") stream_cmds.append(f"{amber_r:.3f} {amber_g:.3f} {amber_b:.3f} RG")
stream_cmds.append(f"{amber_r:.3f} {amber_g:.3f} {amber_b:.3f} rg") stream_cmds.append(f"{amber_r:.3f} {amber_g:.3f} {amber_b:.3f} rg")
@ -462,7 +476,7 @@ def render_sa2_measures_pdf(
stream_cmds.append("ET") stream_cmds.append("ET")
# 3. ISO 129-1 Bevels & Chamfers (C / 45° - Violet Purple) # 3. ISO 129-1 Bevels & Chamfers (C / 45° - Violet Purple)
if chamfers: if draw_sa2_chamfers and chamfers:
purple_r, purple_g, purple_b = 0.60, 0.20, 0.85 purple_r, purple_g, purple_b = 0.60, 0.20, 0.85
stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} RG") stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} RG")
stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} rg") stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} rg")
@ -498,7 +512,7 @@ def render_sa2_measures_pdf(
stream_cmds.append("ET") stream_cmds.append("ET")
# 4. Hole Center Pitch Distances (Teal Green) # 4. Hole Center Pitch Distances (Teal Green)
if len(holes) >= 2: if draw_sa2_holes and len(holes) >= 2:
teal_r, teal_g, teal_b = 0.05, 0.58, 0.53 teal_r, teal_g, teal_b = 0.05, 0.58, 0.53
stream_cmds.append(f"{teal_r:.3f} {teal_g:.3f} {teal_b:.3f} RG") stream_cmds.append(f"{teal_r:.3f} {teal_g:.3f} {teal_b:.3f} RG")
stream_cmds.append(f"{teal_r:.3f} {teal_g:.3f} {teal_b:.3f} rg") stream_cmds.append(f"{teal_r:.3f} {teal_g:.3f} {teal_b:.3f} rg")
@ -523,7 +537,7 @@ def render_sa2_measures_pdf(
stream_cmds.append("ET") stream_cmds.append("ET")
# 5. Interior Step / Notch Feature Segment Lengths (Deep Purple) # 5. Interior Step / Notch Feature Segment Lengths (Deep Purple)
if step_features: if draw_sa2_step_features and step_features:
purple_r, purple_g, purple_b = 0.48, 0.23, 0.92 purple_r, purple_g, purple_b = 0.48, 0.23, 0.92
stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} RG") stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} RG")
stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} rg") stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} rg")

View file

@ -72,7 +72,10 @@ def export_mesh_to_pdf(
draw_contour: bool = False, draw_contour: bool = False,
draw_crease: bool = False, draw_crease: bool = False,
draw_bbox: bool = False, draw_bbox: bool = False,
draw_sa2_measures: bool = False, draw_sa2_holes: bool = False,
draw_sa2_arcs: bool = False,
draw_sa2_chamfers: bool = False,
draw_sa2_step_features: bool = False,
show_name: bool = False, show_name: bool = False,
show_bbox_size: bool = False, show_bbox_size: bool = False,
crease_angle: float = 30.0, crease_angle: float = 30.0,
@ -158,26 +161,30 @@ def export_mesh_to_pdf(
pad_x = minor_step pad_x = minor_step
pad_y = minor_step pad_y = minor_step
half_span_x = math.ceil(max(abs((max_x + pad_x) - center_x), abs((min_x - pad_x) - center_x)) / main_step) * main_step # Calculate required grid span with padding
half_span_y = math.ceil(max(abs((max_y + pad_y) - center_y), abs((min_y - pad_y) - center_y)) / main_step) * main_step span_x = max(abs(max_x - center_x), abs(min_x - center_x)) + pad_x
span_y = max(abs(max_y - center_y), abs(min_y - center_y)) + pad_y
# Round up to nearest main_step
half_span_x = math.ceil(span_x / main_step) * main_step
half_span_y = math.ceil(span_y / main_step) * main_step
if half_span_x == 0: if half_span_x == 0:
half_span_x = main_step half_span_x = main_step
if half_span_y == 0: if half_span_y == 0:
half_span_y = main_step half_span_y = main_step
grid_min_x = math.floor((center_x - half_span_x) / main_step) * main_step # Ensure grid is symmetric around center
grid_max_x = math.ceil((center_x + half_span_x) / main_step) * main_step grid_min_x = center_x - half_span_x
grid_min_y = math.floor((center_y - half_span_y) / main_step) * main_step grid_max_x = center_x + half_span_x
grid_max_y = math.ceil((center_y + half_span_y) / main_step) * main_step grid_min_y = center_y - half_span_y
grid_max_y = center_y + half_span_y
if min_x - grid_min_x < minor_step:
grid_min_x -= main_step # Ensure grid aligns with main_step grid lines
if grid_max_x - max_x < minor_step: grid_min_x = math.floor(grid_min_x / main_step) * main_step
grid_max_x += main_step grid_max_x = math.ceil(grid_max_x / main_step) * main_step
if min_y - grid_min_y < minor_step: grid_min_y = math.floor(grid_min_y / main_step) * main_step
grid_min_y -= main_step grid_max_y = math.ceil(grid_max_y / main_step) * main_step
if grid_max_y - max_y < minor_step:
grid_max_y += main_step
if grid_min_x == grid_max_x: if grid_min_x == grid_max_x:
grid_max_x += main_step grid_max_x += main_step
@ -198,7 +205,15 @@ def export_mesh_to_pdf(
page_w_pt = w_total_mm * MM_TO_PT page_w_pt = w_total_mm * MM_TO_PT
page_h_pt = h_total_mm * MM_TO_PT page_h_pt = h_total_mm * MM_TO_PT
line_width_pt = line_width_mm * MM_TO_PT # Automatic sizing based on minor grid
if line_width_mm <= 0:
line_width_pt = minor_step * 0.05 * MM_TO_PT
else:
line_width_pt = line_width_mm * MM_TO_PT
# Automatic font size: equal to minor grid spacing
if font_size_pt is None or font_size_pt <= 0:
font_size_pt = minor_step * MM_TO_PT
def to_pdf_pt(pt_mm: NDArray[np.float64]) -> NDArray[np.float64]: def to_pdf_pt(pt_mm: NDArray[np.float64]) -> NDArray[np.float64]:
x_pt = (pt_mm[:, 0] - grid_min_x + total_margin_mm) * MM_TO_PT x_pt = (pt_mm[:, 0] - grid_min_x + total_margin_mm) * MM_TO_PT
@ -316,8 +331,20 @@ def export_mesh_to_pdf(
stream_cmds.append("S") stream_cmds.append("S")
# 5.6. SA2 Automated Technical Measures Block # 5.6. SA2 Automated Technical Measures Block
if draw_sa2_measures and raw_geometry is not None: if raw_geometry is not None and (draw_sa2_holes or draw_sa2_arcs or draw_sa2_chamfers or draw_sa2_step_features):
render_sa2_measures_pdf(stream_cmds, raw_geometry, draw_sa2_measures, line_width_pt, main_step, to_pdf_pt, font_size_pt=font_size_pt) render_sa2_measures_pdf(
stream_cmds,
raw_geometry,
draw_sa2_holes=draw_sa2_holes,
draw_sa2_arcs=draw_sa2_arcs,
draw_sa2_chamfers=draw_sa2_chamfers,
draw_sa2_step_features=draw_sa2_step_features,
minor_step_mm=minor_step,
main_step=main_step,
to_pdf_pt=to_pdf_pt,
font_size_pt=font_size_pt,
line_width_pt=line_width_pt
)
# 6. Size Scale Legend (Bottom Left, aligned directly on bottom-left main grid lines with arrows and X/Y labels) # 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("/GS100 gs")

View file

@ -130,8 +130,12 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **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. - **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.
- **Line Width (mm)**: Stroke width in millimeters for the exported lines. - **Holes**: Checkbox to render circular bore/hole diameter callouts (Ø) with ISO 129-1 compliant markings.
- **Font Size (pt)**: Font size in PostScript points for all text labels and measurement callouts. - **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. - **Margin (mm)**: Margin around the geometry grid in mm.
- **Background**: Color picker for the canvas background. - **Background**: Color picker for the canvas background.
- **Wireframe**: Color picker for the wireframe lines and grid. - **Wireframe**: Color picker for the wireframe lines and grid.

View file

@ -545,7 +545,10 @@ class TestPdfExporter(unittest.TestCase):
export_mesh_to_pdf( export_mesh_to_pdf(
edges_2d, edges_2d,
pdf_path, pdf_path,
draw_sa2_measures=True, draw_sa2_holes=True,
draw_sa2_arcs=True,
draw_sa2_chamfers=True,
draw_sa2_step_features=True,
raw_geometry=so raw_geometry=so
) )
self.assertTrue(os.path.exists(pdf_path)) self.assertTrue(os.path.exists(pdf_path))
@ -590,7 +593,10 @@ class TestPdfExporter(unittest.TestCase):
export_mesh_to_pdf( export_mesh_to_pdf(
edges_2d, edges_2d,
pdf_path, pdf_path,
draw_sa2_measures=True, draw_sa2_holes=True,
draw_sa2_arcs=True,
draw_sa2_chamfers=True,
draw_sa2_step_features=True,
raw_geometry=so raw_geometry=so
) )
self.assertTrue(os.path.exists(pdf_path)) self.assertTrue(os.path.exists(pdf_path))
@ -640,7 +646,10 @@ class TestPdfExporter(unittest.TestCase):
export_mesh_to_pdf( export_mesh_to_pdf(
edges_2d, edges_2d,
pdf_path, pdf_path,
draw_sa2_measures=True, draw_sa2_holes=True,
draw_sa2_arcs=True,
draw_sa2_chamfers=True,
draw_sa2_step_features=True,
raw_geometry=so raw_geometry=so
) )
self.assertTrue(os.path.exists(pdf_path)) self.assertTrue(os.path.exists(pdf_path))