ref
This commit is contained in:
parent
9b3b2cca94
commit
4a858463af
4 changed files with 665 additions and 244 deletions
|
|
@ -4,8 +4,10 @@ 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.
|
||||
- sa2_measures.py: SA2 automated measurements (bores Ø, arcs R, chamfers C, pitch, steps).
|
||||
- writer.py: Scalable vector PDF stream assembly & file writing.
|
||||
- pdf_utils.py: Reusable PDF utilities (text escaping, drawing operators).
|
||||
- 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.
|
||||
"""
|
||||
from .geometry import (
|
||||
deduplicate_2d_edges,
|
||||
|
|
@ -22,39 +24,63 @@ from .metadata import (
|
|||
extract_object_name_positions,
|
||||
extract_object_bounding_boxes,
|
||||
)
|
||||
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 .sa2_measures import (
|
||||
circumcircle_3pts,
|
||||
extract_slicing_objects_bounds,
|
||||
extract_slicing_object_features,
|
||||
render_sa2_measures_pdf,
|
||||
)
|
||||
from .sizing import MM_TO_PT
|
||||
from .writer import (
|
||||
MM_TO_PT,
|
||||
_format_pdf_lines,
|
||||
escape_pdf_text,
|
||||
export_mesh_to_pdf,
|
||||
export_wireframe_to_pdf,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Main exporters
|
||||
"export_mesh_to_pdf",
|
||||
"export_wireframe_to_pdf",
|
||||
# PDF utilities
|
||||
"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",
|
||||
# Geometry
|
||||
"deduplicate_2d_edges",
|
||||
"extract_unique_top_view_edges",
|
||||
"extract_lineart_edges",
|
||||
"extract_boundary_edges",
|
||||
"extract_outer_contour_edges",
|
||||
"extract_crease_edges",
|
||||
# Metadata
|
||||
"get_first_attribute_value",
|
||||
"extract_settings_items",
|
||||
"extract_object_label_info",
|
||||
"extract_object_name_positions",
|
||||
"extract_object_bounding_boxes",
|
||||
# SA2 measures
|
||||
"circumcircle_3pts",
|
||||
"extract_slicing_objects_bounds",
|
||||
"extract_slicing_object_features",
|
||||
"render_sa2_measures_pdf",
|
||||
# Constants
|
||||
"MM_TO_PT",
|
||||
"_format_pdf_lines",
|
||||
]
|
||||
|
|
|
|||
156
core/pdf_exporter/pdf_utils.py
Normal file
156
core/pdf_exporter/pdf_utils.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
"""
|
||||
PDF Utilities Module.
|
||||
Reusable utilities for PDF stream generation, text escaping, and drawing operations.
|
||||
"""
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def escape_pdf_text(text: str) -> str:
|
||||
"""
|
||||
Escape special characters for PDF text strings.
|
||||
|
||||
In PDF text strings delimited by parentheses, the following characters
|
||||
must be escaped with a backslash: backslash, left-paren, right-paren.
|
||||
|
||||
Non-ASCII characters (like diameter symbol, degree, etc.) are left as-is and will be
|
||||
encoded as cp1252 bytes in the PDF stream, matching the WinAnsiEncoding
|
||||
used by the embedded font.
|
||||
|
||||
Args:
|
||||
text: The text string to escape.
|
||||
|
||||
Returns:
|
||||
The escaped text string safe for PDF text operators.
|
||||
"""
|
||||
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def pdf_line_to(x_pt: float, y_pt: float) -> str:
|
||||
"""Generate PDF line-to operator."""
|
||||
return f"{x_pt:.2f} {y_pt:.2f} l"
|
||||
|
||||
|
||||
def pdf_move_line(x1_pt: float, y1_pt: float, x2_pt: float, y2_pt: float) -> str:
|
||||
"""Generate PDF move-to and line-to sequence."""
|
||||
return f"{x1_pt:.2f} {y1_pt:.2f} m {x2_pt:.2f} {y2_pt:.2f} l"
|
||||
|
||||
|
||||
def pdf_stroke() -> str:
|
||||
"""Generate PDF stroke operator."""
|
||||
return "S"
|
||||
|
||||
|
||||
def pdf_text_show(text: str) -> str:
|
||||
"""Generate PDF text show operator with escaped text."""
|
||||
return f"({escape_pdf_text(text)}) Tj"
|
||||
|
||||
|
||||
def pdf_text_begin() -> str:
|
||||
"""Generate PDF begin text operator."""
|
||||
return "BT"
|
||||
|
||||
|
||||
def pdf_text_end() -> str:
|
||||
"""Generate PDF end text operator."""
|
||||
return "ET"
|
||||
|
||||
|
||||
def pdf_set_font(font_name: str, size_pt: float) -> str:
|
||||
"""Generate PDF font selection operator."""
|
||||
return f"/{font_name} {size_pt:.1f} Tf"
|
||||
|
||||
|
||||
def pdf_set_rgb_color(r: float, g: float, b: float, fill: bool = True) -> str:
|
||||
"""
|
||||
Generate PDF color setting operator.
|
||||
|
||||
Args:
|
||||
r: Red component (0.0-1.0)
|
||||
g: Green component (0.0-1.0)
|
||||
b: Blue component (0.0-1.0)
|
||||
fill: True for fill color (rg), False for stroke color (RG)
|
||||
|
||||
Returns:
|
||||
PDF color operator string.
|
||||
"""
|
||||
color_op = "rg" if fill else "RG"
|
||||
return f"{r:.3f} {g:.3f} {b:.3f} {color_op}"
|
||||
|
||||
|
||||
def pdf_set_line_width(width_pt: float) -> str:
|
||||
"""Generate PDF line width operator."""
|
||||
return f"{width_pt:.2f} w"
|
||||
|
||||
|
||||
def pdf_set_line_cap_join(cap: int = 1, join: int = 1) -> str:
|
||||
"""
|
||||
Generate PDF line cap and join style operators.
|
||||
|
||||
Args:
|
||||
cap: 0=butt, 1=round, 2=projecting square
|
||||
join: 0=miter, 1=round, 2=bevel
|
||||
|
||||
Returns:
|
||||
PDF line style operator string.
|
||||
"""
|
||||
return f"{cap} j {join} J"
|
||||
|
||||
|
||||
def pdf_set_graphics_state(name: str) -> str:
|
||||
"""Generate PDF graphics state selection operator."""
|
||||
return f"/{name} gs"
|
||||
|
||||
|
||||
def pdf_transform_matrix(a: float, b: float, c: float, d: float, e: float, f: float) -> str:
|
||||
"""Generate PDF transformation matrix operator."""
|
||||
return f"{a:.2f} {b:.2f} {c:.2f} {d:.2f} {e:.2f} {f:.2f} Tm"
|
||||
|
||||
|
||||
def pdf_arrowhead(
|
||||
tip_x_pt: float,
|
||||
tip_y_pt: float,
|
||||
direction_x: float,
|
||||
direction_y: float,
|
||||
arrow_len_pt: float,
|
||||
arrow_half_w_pt: float,
|
||||
) -> str:
|
||||
"""
|
||||
Generate PDF commands for an arrowhead.
|
||||
|
||||
Args:
|
||||
tip_x_pt: X coordinate of arrow tip
|
||||
tip_y_pt: Y coordinate of arrow tip
|
||||
direction_x: X direction vector (normalized)
|
||||
direction_y: Y direction vector (normalized)
|
||||
arrow_len_pt: Length of arrow in points
|
||||
arrow_half_w_pt: Half-width of arrow in points
|
||||
|
||||
Returns:
|
||||
PDF commands to draw the arrowhead.
|
||||
"""
|
||||
# Arrow base (opposite of tip direction)
|
||||
base_x = tip_x_pt - direction_x * arrow_len_pt
|
||||
base_y = tip_y_pt - direction_y * arrow_len_pt
|
||||
|
||||
# Perpendicular vectors for arrow wings
|
||||
perp_x = direction_y * arrow_half_w_pt
|
||||
perp_y = -direction_x * arrow_half_w_pt
|
||||
|
||||
# Left wing
|
||||
left_x = base_x + perp_x
|
||||
left_y = base_y + perp_y
|
||||
|
||||
# Right wing
|
||||
right_x = base_x - perp_x
|
||||
right_y = base_y - perp_y
|
||||
|
||||
return (
|
||||
f"{tip_x_pt:.2f} {tip_y_pt:.2f} m "
|
||||
f"{left_x:.2f} {left_y:.2f} l "
|
||||
f"{right_x:.2f} {right_y:.2f} l S"
|
||||
)
|
||||
|
|
@ -16,15 +16,22 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
def escape_pdf_text(text: str) -> str:
|
||||
# For PDF text strings, escape only the PDF special characters: \, (, )
|
||||
# Non-ASCII characters will be encoded as cp1252 bytes in the stream
|
||||
# which matches the WinAnsiEncoding of the font
|
||||
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Geometry Utilities
|
||||
# =============================================================================
|
||||
|
||||
def circumcircle_3pts(p1: NDArray, p2: NDArray, p3: NDArray) -> tuple[tuple[float, float] | None, float | None]:
|
||||
"""Calculate center and radius of circumcircle passing through 3 2D points."""
|
||||
|
|
@ -42,9 +49,16 @@ def circumcircle_3pts(p1: NDArray, p2: NDArray, p3: NDArray) -> tuple[tuple[floa
|
|||
return (ux, uy), r
|
||||
|
||||
|
||||
def extract_slicing_objects_bounds(mesh_data: Any) -> list[tuple[Any, str, float, float, float, float]]:
|
||||
# =============================================================================
|
||||
# Slicing Object Processing
|
||||
# =============================================================================
|
||||
|
||||
def extract_slicing_objects_bounds(
|
||||
mesh_data: Any
|
||||
) -> list[tuple[Any, str, float, float, float, float]]:
|
||||
"""
|
||||
Extracts (slicing_object, object_name, min_x_mm, max_x_mm, min_y_mm, max_y_mm) for each SlicingObject in mesh_data.
|
||||
Extracts (slicing_object, object_name, min_x_mm, max_x_mm, min_y_mm, max_y_mm)
|
||||
for each SlicingObject in mesh_data.
|
||||
"""
|
||||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||
|
||||
|
|
@ -78,6 +92,60 @@ def extract_slicing_objects_bounds(mesh_data: Any) -> list[tuple[Any, str, float
|
|||
return results
|
||||
|
||||
|
||||
def _collect_edges(
|
||||
so_item: Any,
|
||||
draw_wireframe: bool = True,
|
||||
draw_boundary: bool = False,
|
||||
draw_contour: bool = False,
|
||||
draw_crease: bool = False,
|
||||
crease_angle: float = 30.0,
|
||||
) -> NDArray[np.float64]:
|
||||
"""Collect and deduplicate edges from a SlicingObject based on draw settings."""
|
||||
edge_list: list[NDArray[np.float64]] = []
|
||||
|
||||
if draw_wireframe:
|
||||
w_edges = extract_unique_top_view_edges(so_item)
|
||||
if len(w_edges) > 0:
|
||||
edge_list.append(w_edges)
|
||||
|
||||
if draw_boundary and not draw_crease:
|
||||
b_edges = extract_boundary_edges(so_item)
|
||||
if len(b_edges) > 0:
|
||||
edge_list.append(b_edges)
|
||||
|
||||
if draw_contour:
|
||||
c_edges = extract_outer_contour_edges(so_item)
|
||||
if len(c_edges) > 0:
|
||||
edge_list.append(c_edges)
|
||||
|
||||
if draw_crease:
|
||||
cr_edges = extract_crease_edges(so_item, max_angle_deg=crease_angle)
|
||||
if len(cr_edges) > 0:
|
||||
edge_list.append(cr_edges)
|
||||
|
||||
if edge_list:
|
||||
combined = np.concatenate(edge_list, axis=0)
|
||||
return deduplicate_2d_edges(combined)
|
||||
return np.empty((0, 2, 2), dtype=np.float64)
|
||||
|
||||
|
||||
def _get_so_bounds(
|
||||
so_item: Any,
|
||||
bounds: tuple[float, float, float, float] | None = None,
|
||||
) -> tuple[float, float, float, float]:
|
||||
"""Get bounding box for a SlicingObject."""
|
||||
if bounds is not None:
|
||||
return bounds
|
||||
elif hasattr(so_item, 'mesh') and so_item.mesh.size > 0:
|
||||
verts_2d = so_item.mesh[:, :3, :2].reshape(-1, 2)
|
||||
min_x = float(verts_2d[:, 0].min())
|
||||
max_x = float(verts_2d[:, 0].max())
|
||||
min_y = float(verts_2d[:, 1].min())
|
||||
max_y = float(verts_2d[:, 1].max())
|
||||
return (min_x, max_x, min_y, max_y)
|
||||
return (0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def extract_slicing_object_features(
|
||||
so_item: Any,
|
||||
draw_wireframe: bool = True,
|
||||
|
|
@ -91,33 +159,14 @@ def extract_slicing_object_features(
|
|||
Extracts circular bore / hole features (center_x, center_y, diameter_mm) from a SlicingObject
|
||||
per ISO 129-1 standards based on visible edge layers.
|
||||
"""
|
||||
edge_list: list[NDArray[np.float64]] = []
|
||||
|
||||
if draw_wireframe:
|
||||
w_edges = extract_unique_top_view_edges(so_item)
|
||||
if len(w_edges) > 0:
|
||||
edge_list.append(w_edges)
|
||||
|
||||
if draw_boundary and not draw_crease:
|
||||
b_edges_layer = extract_boundary_edges(so_item)
|
||||
if len(b_edges_layer) > 0:
|
||||
edge_list.append(b_edges_layer)
|
||||
|
||||
if draw_contour:
|
||||
c_edges_layer = extract_outer_contour_edges(so_item)
|
||||
if len(c_edges_layer) > 0:
|
||||
edge_list.append(c_edges_layer)
|
||||
|
||||
if draw_crease:
|
||||
cr_edges_layer = extract_crease_edges(so_item, max_angle_deg=crease_angle)
|
||||
if len(cr_edges_layer) > 0:
|
||||
edge_list.append(cr_edges_layer)
|
||||
|
||||
if edge_list:
|
||||
combined = np.concatenate(edge_list, axis=0)
|
||||
b_edges = deduplicate_2d_edges(combined)
|
||||
else:
|
||||
b_edges = np.empty((0, 2, 2), dtype=np.float64)
|
||||
b_edges = _collect_edges(
|
||||
so_item,
|
||||
draw_wireframe=draw_wireframe,
|
||||
draw_boundary=draw_boundary,
|
||||
draw_contour=draw_contour,
|
||||
draw_crease=draw_crease,
|
||||
crease_angle=crease_angle,
|
||||
)
|
||||
|
||||
if len(b_edges) == 0:
|
||||
return []
|
||||
|
|
@ -129,39 +178,353 @@ def extract_slicing_object_features(
|
|||
polygons = list(polygonize(lines))
|
||||
loops: list[NDArray[np.float64]] = [np.array(poly.exterior.coords, dtype=np.float64) for poly in polygons]
|
||||
|
||||
holes: list[tuple[float, float, float]] = []
|
||||
|
||||
if bounds is not None:
|
||||
min_x, max_x, min_y, max_y = bounds
|
||||
elif hasattr(so_item, 'mesh') and so_item.mesh.size > 0:
|
||||
verts_2d = so_item.mesh[:, :3, :2].reshape(-1, 2)
|
||||
min_x, max_x = float(verts_2d[:, 0].min()), float(verts_2d[:, 0].max())
|
||||
min_y, max_y = float(verts_2d[:, 1].min()), float(verts_2d[:, 1].max())
|
||||
else:
|
||||
return holes
|
||||
|
||||
min_x, max_x, min_y, max_y = _get_so_bounds(so_item, bounds)
|
||||
overall_w = max_x - min_x
|
||||
overall_h = max_y - min_y
|
||||
|
||||
holes: list[tuple[float, float, float]] = []
|
||||
|
||||
for loop in loops:
|
||||
N = len(loop)
|
||||
if N < 3:
|
||||
continue
|
||||
|
||||
cx_full = float(loop[:, 0].mean())
|
||||
cy_full = float(loop[:, 1].mean())
|
||||
dists_full = np.sqrt((loop[:, 0] - cx_full) ** 2 + (loop[:, 1] - cy_full) ** 2)
|
||||
r_mean_full = float(dists_full.mean())
|
||||
r_std_full = float(dists_full.std())
|
||||
cx = float(loop[:, 0].mean())
|
||||
cy = float(loop[:, 1].mean())
|
||||
dists = np.sqrt((loop[:, 0] - cx) ** 2 + (loop[:, 1] - cy) ** 2)
|
||||
r_mean = float(dists.mean())
|
||||
r_std = float(dists.std())
|
||||
|
||||
# ISO Full Circular Bore (Ø) Detection
|
||||
if N >= 5 and r_mean_full > 0.5 and (r_std_full / r_mean_full) < 0.20:
|
||||
if r_mean_full < min(overall_w, overall_h) * 0.45:
|
||||
holes.append((cx_full, cy_full, r_mean_full * 2.0))
|
||||
if N >= 5 and r_mean > 0.5 and (r_std / r_mean) < 0.20:
|
||||
if r_mean < min(overall_w, overall_h) * 0.45:
|
||||
holes.append((cx, cy, r_mean * 2.0))
|
||||
|
||||
return holes
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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],
|
||||
text: str,
|
||||
x_pt: float, y_pt: float,
|
||||
font_size_pt: float,
|
||||
color: tuple[float, float, float],
|
||||
char_spacing: float = 0.35,
|
||||
text_len: int | None = None,
|
||||
x_offset: float = 0.0,
|
||||
y_offset: float = 0.0,
|
||||
) -> None:
|
||||
"""Draw text at a position with automatic centering based on text length."""
|
||||
if text_len is None:
|
||||
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())
|
||||
|
||||
|
||||
def _draw_horizontal_dimension(
|
||||
stream_cmds: list[str],
|
||||
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]],
|
||||
color: tuple[float, float, float],
|
||||
line_width_pt: float,
|
||||
arrow_len_pt: float,
|
||||
arrow_half_w_pt: float,
|
||||
text: str,
|
||||
font_size_pt: float,
|
||||
) -> None:
|
||||
"""Draw a horizontal dimension with extension lines and arrowheads."""
|
||||
# Extension lines
|
||||
y_ext_gap = y_mm - ext_gap_mm
|
||||
y_ext_over = y_mm - ext_overhang_mm
|
||||
|
||||
p_l1 = _pt_to_str(to_pdf_pt(np.array([[x1_mm, y_ext_gap]])))
|
||||
p_l2 = _pt_to_str(to_pdf_pt(np.array([[x1_mm, y_ext_over]])))
|
||||
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")
|
||||
|
||||
# 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")
|
||||
|
||||
# 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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
# Text label
|
||||
_draw_text(
|
||||
stream_cmds, 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],
|
||||
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]],
|
||||
color: tuple[float, float, float],
|
||||
line_width_pt: float,
|
||||
arrow_len_pt: float,
|
||||
arrow_half_w_pt: float,
|
||||
text: str,
|
||||
font_size_pt: float,
|
||||
) -> None:
|
||||
"""Draw a vertical dimension with extension lines and arrowheads."""
|
||||
# Extension lines
|
||||
x_ext_gap = x_mm - ext_gap_mm
|
||||
x_ext_over = x_mm - ext_overhang_mm
|
||||
|
||||
p_b1 = _pt_to_str(to_pdf_pt(np.array([[x_ext_gap, y1_mm]])))
|
||||
p_b2 = _pt_to_str(to_pdf_pt(np.array([[x_ext_over, y1_mm]])))
|
||||
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")
|
||||
|
||||
# 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")
|
||||
|
||||
# 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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
# Text label
|
||||
_draw_text(
|
||||
stream_cmds, 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
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Hole Feature Drawing
|
||||
# =============================================================================
|
||||
|
||||
def _group_concentric_holes(
|
||||
holes: list[tuple[float, float, float]],
|
||||
tolerance_mm: float = 0.5,
|
||||
) -> list[list[tuple[float, float, float]]]:
|
||||
"""Group holes by center location to identify concentric bores."""
|
||||
groups: list[list[tuple[float, float, float]]] = []
|
||||
for cx, cy, dia in holes:
|
||||
added = False
|
||||
for grp in groups:
|
||||
gcx, gcy, _ = grp[0]
|
||||
if math.hypot(cx - gcx, cy - gcy) < tolerance_mm:
|
||||
grp.append((cx, cy, dia))
|
||||
added = True
|
||||
break
|
||||
if not added:
|
||||
groups.append([(cx, cy, dia)])
|
||||
return groups
|
||||
|
||||
|
||||
def _get_hole_depth_str(
|
||||
mesh_verts_3d: NDArray[np.float64] | None,
|
||||
total_z_span: float,
|
||||
cx_mm: float, cy_mm: float,
|
||||
dia_mm: float,
|
||||
) -> str:
|
||||
"""Get depth string for a hole feature."""
|
||||
if mesh_verts_3d is None or total_z_span <= 0:
|
||||
return ""
|
||||
r_h = dia_mm / 2.0
|
||||
d_mask = np.hypot(mesh_verts_3d[:, 0] - cx_mm, mesh_verts_3d[:, 1] - cy_mm) <= (r_h + 0.3)
|
||||
if not np.any(d_mask):
|
||||
return ""
|
||||
z_sub = mesh_verts_3d[d_mask, 2]
|
||||
h_z_depth = float(z_sub.max() - z_sub.min())
|
||||
if abs(h_z_depth - total_z_span) < 0.2:
|
||||
return " THRU"
|
||||
elif h_z_depth > 0.1:
|
||||
return f" x {h_z_depth:.1f} DP"
|
||||
return ""
|
||||
|
||||
|
||||
def _draw_hole_leader_and_arrow(
|
||||
stream_cmds: list[str],
|
||||
cx_mm: float, cy_mm: float, dia_mm: float,
|
||||
to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]],
|
||||
sizing: SA2Sizing,
|
||||
color: tuple[float, float, float],
|
||||
line_width_pt: float,
|
||||
arrow_len_pt: float,
|
||||
) -> tuple[float, float, float, float]:
|
||||
"""
|
||||
Draw hole leader line and arrowhead.
|
||||
Returns (xe, ye, xs1, ys1) for text positioning.
|
||||
"""
|
||||
slant_mm, leader_mm = sizing.get_hole_callout_lengths(dia_mm)
|
||||
|
||||
# Edge point at 45 degrees
|
||||
offset = dia_mm / 2.0 * 0.7071
|
||||
px_edge = cx_mm + offset
|
||||
py_edge = cy_mm + offset
|
||||
px_sh1 = px_edge + slant_mm
|
||||
py_sh1 = py_edge + slant_mm
|
||||
px_sh2 = px_sh1 + leader_mm
|
||||
|
||||
p_edge = _pt_to_str(to_pdf_pt(np.array([[px_edge, py_edge]])))
|
||||
p_sh1 = _pt_to_str(to_pdf_pt(np.array([[px_sh1, py_sh1]])))
|
||||
p_sh2 = _pt_to_str(to_pdf_pt(np.array([[px_sh2, py_sh1]])))
|
||||
|
||||
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")
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
return xe, ye, xs1, ys1
|
||||
|
||||
|
||||
def _draw_hole_callout_text_only(
|
||||
stream_cmds: list[str],
|
||||
dia_mm: float,
|
||||
depth_str: str,
|
||||
xs1: float, ys1: float,
|
||||
font_size_pt: float,
|
||||
color: tuple[float, float, float],
|
||||
line_idx: int,
|
||||
) -> None:
|
||||
"""Draw the text for a hole callout."""
|
||||
dia_val = round(dia_mm, 1)
|
||||
dia_str = f"Ø {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())
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pitch Drawing
|
||||
# =============================================================================
|
||||
|
||||
def _draw_pitch_marks(
|
||||
stream_cmds: list[str],
|
||||
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]],
|
||||
sizing: SA2Sizing,
|
||||
color: tuple[float, float, float],
|
||||
line_width_pt: float,
|
||||
label: str,
|
||||
font_size_pt: float,
|
||||
is_horizontal: bool = True,
|
||||
) -> None:
|
||||
"""Draw pitch dimension between two hole centers."""
|
||||
if is_horizontal:
|
||||
y_pitch = pitch_y_mm
|
||||
p_h1 = _pt_to_str(to_pdf_pt(np.array([[cx1, y_pitch]])))
|
||||
p_h2 = _pt_to_str(to_pdf_pt(np.array([[cx2, y_pitch]])))
|
||||
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")
|
||||
|
||||
label_x = (p_h1[0] + p_h2[0]) / 2.0
|
||||
label_y = p_h1[1] - font_size_pt * 0.9
|
||||
|
||||
else:
|
||||
x_pitch = pitch_mm
|
||||
p_v1 = _pt_to_str(to_pdf_pt(np.array([[x_pitch, cy1]])))
|
||||
p_v2 = _pt_to_str(to_pdf_pt(np.array([[x_pitch, cy2]])))
|
||||
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")
|
||||
|
||||
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())
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main Render Function
|
||||
# =============================================================================
|
||||
|
||||
def render_sa2_measures_pdf(
|
||||
stream_cmds: list[str],
|
||||
raw_geometry: Any,
|
||||
|
|
@ -193,110 +556,52 @@ def render_sa2_measures_pdf(
|
|||
font_size_pt / MM_TO_PT if font_size_pt else 0.0,
|
||||
minor_step_mm
|
||||
)
|
||||
|
||||
stream_cmds.append("/GS100 gs")
|
||||
sa2_lw_pt = line_width_pt * 0.8
|
||||
stream_cmds.append(f"{sa2_lw_pt:.2f} w")
|
||||
stream_cmds.append("1 j 1 J")
|
||||
|
||||
# Vivid red / crimson color for main linear SA2 measures
|
||||
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")
|
||||
# Setup: graphics state, line style, and SA2 color
|
||||
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))
|
||||
|
||||
for so_item, obj_name, min_x, max_x, min_y, max_y in sa2_objects:
|
||||
w_obj_mm = max_x - min_x
|
||||
h_obj_mm = max_y - min_y
|
||||
|
||||
sizing = SA2Sizing(w_obj_mm, h_obj_mm, line_width_pt, font_size_pt)
|
||||
offset_mm = sizing.offset_mm
|
||||
ext_overhang_mm = sizing.ext_overhang_mm
|
||||
ext_gap_mm = sizing.ext_gap_mm
|
||||
arrow_len_pt = sizing.arrow_len_pt
|
||||
arrow_half_w_pt = sizing.arrow_half_w_pt
|
||||
|
||||
|
||||
# Get 3D mesh data for depth calculations
|
||||
mesh_verts_3d = None
|
||||
total_z_span = 0.0
|
||||
if hasattr(so_item, 'mesh') and so_item.mesh.size > 0:
|
||||
mesh_verts_3d = so_item.mesh[:, :3, :3].reshape(-1, 3)
|
||||
total_z_span = float(mesh_verts_3d[:, 2].max() - mesh_verts_3d[:, 2].min())
|
||||
|
||||
# --- Width Dimension (Horizontal below SlicingObject) ---
|
||||
y_dim_mm = min_y - offset_mm
|
||||
|
||||
p_ext_l1 = to_pdf_pt(np.array([[min_x, min_y - ext_gap_mm]]))
|
||||
p_ext_l2 = to_pdf_pt(np.array([[min_x, y_dim_mm - ext_overhang_mm]]))
|
||||
p_ext_r1 = to_pdf_pt(np.array([[max_x, min_y - ext_gap_mm]]))
|
||||
p_ext_r2 = to_pdf_pt(np.array([[max_x, y_dim_mm - ext_overhang_mm]]))
|
||||
|
||||
stream_cmds.append(f"{p_ext_l1[0,0]:.2f} {p_ext_l1[0,1]:.2f} m {p_ext_l2[0,0]:.2f} {p_ext_l2[0,1]:.2f} l S")
|
||||
stream_cmds.append(f"{p_ext_r1[0,0]:.2f} {p_ext_r1[0,1]:.2f} m {p_ext_r2[0,0]:.2f} {p_ext_r2[0,1]:.2f} l S")
|
||||
|
||||
p_dim_w1 = to_pdf_pt(np.array([[min_x, y_dim_mm]]))
|
||||
p_dim_w2 = to_pdf_pt(np.array([[max_x, y_dim_mm]]))
|
||||
x_w1, y_w = p_dim_w1[0, 0], p_dim_w1[0, 1]
|
||||
x_w2 = p_dim_w2[0, 0]
|
||||
|
||||
stream_cmds.append(f"{x_w1:.2f} {y_w:.2f} m {x_w2:.2f} {y_w:.2f} l S")
|
||||
|
||||
stream_cmds.append(
|
||||
f"{x_w1 + arrow_len_pt:.2f} {y_w + arrow_half_w_pt:.2f} m "
|
||||
f"{x_w1:.2f} {y_w:.2f} l "
|
||||
f"{x_w1 + arrow_len_pt:.2f} {y_w - arrow_half_w_pt:.2f} l S"
|
||||
)
|
||||
stream_cmds.append(
|
||||
f"{x_w2 - arrow_len_pt:.2f} {y_w + arrow_half_w_pt:.2f} m "
|
||||
f"{x_w2:.2f} {y_w:.2f} l "
|
||||
f"{x_w2 - arrow_len_pt:.2f} {y_w - arrow_half_w_pt:.2f} l S"
|
||||
)
|
||||
|
||||
# --- Height Dimension (Vertical to the left of SlicingObject) ---
|
||||
x_dim_mm = min_x - offset_mm
|
||||
|
||||
p_ext_b1 = to_pdf_pt(np.array([[min_x - ext_gap_mm, min_y]]))
|
||||
p_ext_b2 = to_pdf_pt(np.array([[x_dim_mm - ext_overhang_mm, min_y]]))
|
||||
p_ext_t1 = to_pdf_pt(np.array([[min_x - ext_gap_mm, max_y]]))
|
||||
p_ext_t2 = to_pdf_pt(np.array([[x_dim_mm - ext_overhang_mm, max_y]]))
|
||||
|
||||
stream_cmds.append(f"{p_ext_b1[0,0]:.2f} {p_ext_b1[0,1]:.2f} m {p_ext_b2[0,0]:.2f} {p_ext_b2[0,1]:.2f} l S")
|
||||
stream_cmds.append(f"{p_ext_t1[0,0]:.2f} {p_ext_t1[0,1]:.2f} m {p_ext_t2[0,0]:.2f} {p_ext_t2[0,1]:.2f} l S")
|
||||
|
||||
p_dim_h1 = to_pdf_pt(np.array([[x_dim_mm, min_y]]))
|
||||
p_dim_h2 = to_pdf_pt(np.array([[x_dim_mm, max_y]]))
|
||||
x_h, y_h1 = p_dim_h1[0, 0], p_dim_h1[0, 1]
|
||||
y_h2 = p_dim_h2[0, 1]
|
||||
|
||||
stream_cmds.append(f"{x_h:.2f} {y_h1:.2f} m {x_h:.2f} {y_h2:.2f} l S")
|
||||
|
||||
stream_cmds.append(
|
||||
f"{x_h - arrow_half_w_pt:.2f} {y_h1 + arrow_len_pt:.2f} m "
|
||||
f"{x_h:.2f} {y_h1:.2f} l "
|
||||
f"{x_h + arrow_half_w_pt:.2f} {y_h1 + arrow_len_pt:.2f} l S"
|
||||
)
|
||||
stream_cmds.append(
|
||||
f"{x_h - arrow_half_w_pt:.2f} {y_h2 - arrow_len_pt:.2f} m "
|
||||
f"{x_h:.2f} {y_h2:.2f} l "
|
||||
f"{x_h + arrow_half_w_pt:.2f} {y_h2 - arrow_len_pt:.2f} l S"
|
||||
)
|
||||
|
||||
# --- Text Labels for Width and Height ---
|
||||
stream_cmds.append(f"{red_r:.3f} {red_g:.3f} {red_b:.3f} rg")
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt:.1f} Tf")
|
||||
|
||||
# --- Width Dimension ---
|
||||
w_str = f"{w_obj_mm/1000:g}m" if main_step >= 1000.0 else f"{w_obj_mm:g}mm"
|
||||
x_w_text = (x_w1 + x_w2) / 2.0 - (len(w_str) * font_size_pt * 0.28)
|
||||
y_w_text = y_w - font_size_pt * 1.1
|
||||
stream_cmds.append(f"1 0 0 1 {x_w_text:.2f} {y_w_text:.2f} Tm")
|
||||
stream_cmds.append(f"({escape_pdf_text(w_str)}) Tj")
|
||||
_draw_horizontal_dimension(
|
||||
stream_cmds,
|
||||
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,
|
||||
sizing.arrow_len_pt, sizing.arrow_half_w_pt,
|
||||
w_str, font_size_pt
|
||||
)
|
||||
|
||||
# --- Height Dimension ---
|
||||
h_str = f"{h_obj_mm/1000:g}m" if main_step >= 1000.0 else f"{h_obj_mm:g}mm"
|
||||
x_h_text = x_h - (len(h_str) * font_size_pt * 0.55) - (2.0 * MM_TO_PT)
|
||||
y_h_text = (y_h1 + y_h2) / 2.0 - (font_size_pt * 0.35)
|
||||
stream_cmds.append(f"1 0 0 1 {x_h_text:.2f} {y_h_text:.2f} Tm")
|
||||
stream_cmds.append(f"({escape_pdf_text(h_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
_draw_vertical_dimension(
|
||||
stream_cmds,
|
||||
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,
|
||||
sizing.arrow_len_pt, sizing.arrow_half_w_pt,
|
||||
h_str, font_size_pt
|
||||
)
|
||||
|
||||
# --- SA2 Hole Features Extraction (Ø) ---
|
||||
# --- Hole Features ---
|
||||
holes = extract_slicing_object_features(
|
||||
so_item,
|
||||
draw_wireframe=draw_wireframe,
|
||||
|
|
@ -307,77 +612,37 @@ def render_sa2_measures_pdf(
|
|||
bounds=(min_x, max_x, min_y, max_y),
|
||||
)
|
||||
|
||||
# Concentric Bores Grouping & ISO 129-1 Callouts with Z-depth Specs (Electric Blue)
|
||||
# Concentric Bores Grouping & Hole Callouts
|
||||
if draw_sa2_holes and holes:
|
||||
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")
|
||||
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))
|
||||
|
||||
# Group holes by center location (tolerance = 0.5mm)
|
||||
concentric_groups: list[list[tuple[float, float, float]]] = []
|
||||
for cx, cy, dia in holes:
|
||||
added = False
|
||||
for grp in concentric_groups:
|
||||
gcx, gcy, _ = grp[0]
|
||||
if math.hypot(cx - gcx, cy - gcy) < 0.5:
|
||||
grp.append((cx, cy, dia))
|
||||
added = True
|
||||
break
|
||||
if not added:
|
||||
concentric_groups.append([(cx, cy, dia)])
|
||||
concentric_groups = _group_concentric_holes(holes)
|
||||
|
||||
for grp in concentric_groups:
|
||||
grp.sort(key=lambda h: h[2], reverse=True)
|
||||
gcx, gcy, max_dia = grp[0]
|
||||
|
||||
slant_mm, leader_mm = sizing.get_hole_callout_lengths(max_dia)
|
||||
px_edge = gcx + (max_dia / 2.0) * 0.7071
|
||||
py_edge = gcy + (max_dia / 2.0) * 0.7071
|
||||
px_sh1 = px_edge + slant_mm
|
||||
py_sh1 = py_edge + slant_mm
|
||||
|
||||
p_edge = to_pdf_pt(np.array([[px_edge, py_edge]]))
|
||||
p_sh1 = to_pdf_pt(np.array([[px_sh1, py_sh1]]))
|
||||
p_sh2 = to_pdf_pt(np.array([[px_sh1 + leader_mm, py_sh1]]))
|
||||
|
||||
xe, ye = p_edge[0, 0], p_edge[0, 1]
|
||||
xs1, ys1 = p_sh1[0, 0], p_sh1[0, 1]
|
||||
|
||||
stream_cmds.append(f"{xe:.2f} {ye:.2f} m {xs1:.2f} {ys1:.2f} l {p_sh2[0, 0]:.2f} {ys1:.2f} l S")
|
||||
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"
|
||||
xe, ye, xs1, ys1 = _draw_hole_leader_and_arrow(
|
||||
stream_cmds, gcx, gcy, max_dia,
|
||||
to_pdf_pt, sizing, hole_color, sa2_lw_pt, sizing.arrow_len_pt
|
||||
)
|
||||
|
||||
for line_idx, (hcx, hcy, hdia) in enumerate(grp):
|
||||
depth_str = ""
|
||||
if mesh_verts_3d is not None and total_z_span > 0:
|
||||
r_h = hdia / 2.0
|
||||
d_mask = np.hypot(mesh_verts_3d[:, 0] - hcx, mesh_verts_3d[:, 1] - hcy) <= (r_h + 0.3)
|
||||
if np.any(d_mask):
|
||||
z_sub = mesh_verts_3d[d_mask, 2]
|
||||
h_z_depth = float(z_sub.max() - z_sub.min())
|
||||
if abs(h_z_depth - total_z_span) < 0.2:
|
||||
depth_str = " THRU"
|
||||
elif h_z_depth > 0.1:
|
||||
depth_str = f" x {h_z_depth:.1f} DP"
|
||||
depth_str = _get_hole_depth_str(
|
||||
mesh_verts_3d, total_z_span, hcx, hcy, hdia
|
||||
)
|
||||
_draw_hole_callout_text_only(
|
||||
stream_cmds, hdia, depth_str, xs1, ys1,
|
||||
font_size_pt, hole_color, line_idx
|
||||
)
|
||||
|
||||
dia_val = round(hdia, 1)
|
||||
dia_str = f"Ø {dia_val:g}{depth_str}"
|
||||
y_text_offset = ys1 + 2.0 - (line_idx * font_size_pt * 1.1)
|
||||
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {xs1 + 2.0:.2f} {y_text_offset:.2f} Tm")
|
||||
stream_cmds.append(f"({escape_pdf_text(dia_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
||||
# Orthogonal Hole Center Pitch & Datum Offset Baselines (Teal Green)
|
||||
# --- Pitch Dimensions ---
|
||||
if draw_sa2_holes and 'concentric_groups' in locals() and len(concentric_groups) >= 2:
|
||||
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")
|
||||
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))
|
||||
|
||||
distinct_centers = [grp[0][:2] for grp in concentric_groups]
|
||||
distinct_centers.sort(key=lambda c: (c[0], c[1]))
|
||||
|
|
@ -387,41 +652,20 @@ def render_sa2_measures_pdf(
|
|||
dx_mm = abs(cx2 - cx1)
|
||||
dy_mm = abs(cy2 - cy1)
|
||||
|
||||
# Horizontal Pitch Axis (if dx_mm > 0.5)
|
||||
if dx_mm > 0.5:
|
||||
y_pitch_mm = min(cy1, cy2) - sizing.offset_mm * 0.5
|
||||
p_h1 = to_pdf_pt(np.array([[cx1, y_pitch_mm]]))
|
||||
p_h2 = to_pdf_pt(np.array([[cx2, y_pitch_mm]]))
|
||||
p_e1 = to_pdf_pt(np.array([[cx1, cy1]]))
|
||||
p_e2 = to_pdf_pt(np.array([[cx2, cy2]]))
|
||||
_draw_pitch_marks(
|
||||
stream_cmds, 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
|
||||
)
|
||||
|
||||
stream_cmds.append(f"{p_e1[0,0]:.2f} {p_e1[0,1]:.2f} m {p_h1[0,0]:.2f} {p_h1[0,1]:.2f} l S")
|
||||
stream_cmds.append(f"{p_e2[0,0]:.2f} {p_e2[0,1]:.2f} m {p_h2[0,0]:.2f} {p_h2[0,1]:.2f} l S")
|
||||
stream_cmds.append(f"{p_h1[0,0]:.2f} {p_h1[0,1]:.2f} m {p_h2[0,0]:.2f} {p_h2[0,1]:.2f} l S")
|
||||
|
||||
pitch_str = f"{dx_mm:.1f} C-C"
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {(p_h1[0,0]+p_h2[0,0])/2.0:.2f} {p_h1[0,1] - font_size_pt * 0.9:.2f} Tm")
|
||||
stream_cmds.append(f"({escape_pdf_text(pitch_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
||||
# Vertical Pitch Axis (if dy_mm > 0.5)
|
||||
if dy_mm > 0.5:
|
||||
x_pitch_mm = max(cx1, cx2) + sizing.offset_mm * 0.5
|
||||
p_v1 = to_pdf_pt(np.array([[x_pitch_mm, cy1]]))
|
||||
p_v2 = to_pdf_pt(np.array([[x_pitch_mm, cy2]]))
|
||||
p_e1 = to_pdf_pt(np.array([[cx1, cy1]]))
|
||||
p_e2 = to_pdf_pt(np.array([[cx2, cy2]]))
|
||||
|
||||
stream_cmds.append(f"{p_e1[0,0]:.2f} {p_e1[0,1]:.2f} m {p_v1[0,0]:.2f} {p_v1[0,1]:.2f} l S")
|
||||
stream_cmds.append(f"{p_e2[0,0]:.2f} {p_e2[0,1]:.2f} m {p_v2[0,0]:.2f} {p_v2[0,1]:.2f} l S")
|
||||
stream_cmds.append(f"{p_v1[0,0]:.2f} {p_v1[0,1]:.2f} m {p_v2[0,0]:.2f} {p_v2[0,1]:.2f} l S")
|
||||
|
||||
pitch_str = f"{dy_mm:.1f} C-C"
|
||||
stream_cmds.append("BT")
|
||||
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf")
|
||||
stream_cmds.append(f"1 0 0 1 {p_v1[0,0] + 2.0:.2f} {(p_v1[0,1]+p_v2[0,1])/2.0:.2f} Tm")
|
||||
stream_cmds.append(f"({escape_pdf_text(pitch_str)}) Tj")
|
||||
stream_cmds.append("ET")
|
||||
|
||||
_draw_pitch_marks(
|
||||
stream_cmds, 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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,12 +29,7 @@ from .sizing import (
|
|||
MM_TO_PT,
|
||||
)
|
||||
|
||||
|
||||
def escape_pdf_text(text: str) -> str:
|
||||
# For PDF text strings, escape only the PDF special characters: \, (, )
|
||||
# Non-ASCII characters will be encoded as cp1252 bytes in the stream
|
||||
# which matches the WinAnsiEncoding of the font
|
||||
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
||||
from .pdf_utils import escape_pdf_text
|
||||
|
||||
|
||||
def _format_pdf_lines(lines_arr: NDArray[np.float64]) -> list[str]:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue