639 lines
22 KiB
Python
639 lines
22 KiB
Python
"""
|
|
SA2 Automated Vector Geometry Measurement & ISO 129-1 Technical Marking Engine.
|
|
Detects circular bores (Ø), arc cutouts & fillets (R), bevels & chamfers (C), hole pitch, and step features per SlicingObject.
|
|
"""
|
|
import math
|
|
from typing import Any, Callable
|
|
import numpy as np
|
|
from numpy.typing import NDArray
|
|
|
|
from .geometry import (
|
|
extract_lineart_edges,
|
|
extract_unique_top_view_edges,
|
|
deduplicate_2d_edges,
|
|
extract_boundary_edges,
|
|
extract_outer_contour_edges,
|
|
extract_crease_edges,
|
|
)
|
|
from .sizing import SA2Sizing, compute_effective_stroke_and_font, MM_TO_PT
|
|
from .pdf_utils import PdfStream, escape_pdf_text
|
|
|
|
|
|
# =============================================================================
|
|
# 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."""
|
|
ax, ay = float(p1[0]), float(p1[1])
|
|
bx, by = float(p2[0]), float(p2[1])
|
|
cx, cy = float(p3[0]), float(p3[1])
|
|
|
|
d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
|
|
if abs(d) < 1e-8:
|
|
return None, None
|
|
|
|
ux = ((ax**2 + ay**2) * (by - cy) + (bx**2 + by**2) * (cy - ay) + (cx**2 + cy**2) * (ay - by)) / d
|
|
uy = ((ax**2 + ay**2) * (cx - bx) + (bx**2 + by**2) * (ax - cx) + (cx**2 + cy**2) * (bx - ax)) / d
|
|
r = float(np.sqrt((ax - ux)**2 + (ay - uy)**2))
|
|
return (ux, uy), r
|
|
|
|
|
|
# =============================================================================
|
|
# 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 or SlicePaths in mesh_data.
|
|
"""
|
|
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
|
from ..slice import SlicePaths
|
|
|
|
results: list[tuple[Any, str, float, float, float, float]] = []
|
|
|
|
def process_item(item: Any, fallback_name: str = ""):
|
|
if item is None:
|
|
return
|
|
elif isinstance(item, SlicePaths):
|
|
all_pts = [p[:, :2] for p in item if p is not None and len(p) > 0]
|
|
if all_pts:
|
|
verts_2d = np.concatenate(all_pts, axis=0)
|
|
if len(verts_2d) > 0:
|
|
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())
|
|
name = getattr(item, "name", "") or fallback_name or "SlicePaths"
|
|
results.append((item, name, min_x, max_x, min_y, max_y))
|
|
elif isinstance(item, (list, tuple)):
|
|
for i, sub in enumerate(item):
|
|
process_item(sub, f"{fallback_name}_{i}" if fallback_name else f"object_{i}")
|
|
elif isinstance(item, SlicingGroup):
|
|
for key, coll in item.collections.items():
|
|
process_item(coll, key)
|
|
elif isinstance(item, SlicingCollection):
|
|
coll_name = item.name or fallback_name
|
|
for i, so in enumerate(item.objects):
|
|
process_item(so, so.name or f"{coll_name}_{i}")
|
|
elif isinstance(item, SlicingObject):
|
|
if item.mesh.size > 0:
|
|
name = item.name or fallback_name or "object"
|
|
verts_2d = item.mesh[:, :3, :2].reshape(-1, 2)
|
|
if len(verts_2d) > 0:
|
|
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())
|
|
results.append((item, name, min_x, max_x, min_y, max_y))
|
|
|
|
process_item(mesh_data)
|
|
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,
|
|
draw_boundary: bool = False,
|
|
draw_contour: bool = False,
|
|
draw_crease: bool = False,
|
|
crease_angle: float = 30.0,
|
|
bounds: tuple[float, float, float, float] | None = None,
|
|
) -> list[tuple[float, float, float]]:
|
|
"""
|
|
Extracts circular bore / hole features (center_x, center_y, diameter_mm) from a SlicingObject
|
|
per ISO 129-1 standards based on visible edge layers.
|
|
"""
|
|
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 []
|
|
|
|
from shapely.geometry import LineString
|
|
from shapely.ops import polygonize
|
|
|
|
lines = [LineString([(float(p0[0]), float(p0[1])), (float(p1[0]), float(p1[1]))]) for p0, p1 in b_edges]
|
|
polygons = list(polygonize(lines))
|
|
loops: list[NDArray[np.float64]] = [np.array(poly.exterior.coords, dtype=np.float64) for poly in polygons]
|
|
|
|
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 = 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 > 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 _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_text(
|
|
s: PdfStream,
|
|
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
|
|
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(
|
|
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]],
|
|
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]])))
|
|
|
|
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]])))
|
|
s.line_stroke(p_d1[0], p_d1[1], p_d2[0], p_d2[1])
|
|
|
|
# Arrowheads
|
|
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),
|
|
)
|
|
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(
|
|
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(
|
|
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]],
|
|
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]])))
|
|
|
|
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]])))
|
|
s.line_stroke(p_d1[0], p_d1[1], p_d2[0], p_d2[1])
|
|
|
|
# Arrowheads
|
|
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),
|
|
)
|
|
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(
|
|
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
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# 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(
|
|
s: PdfStream,
|
|
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
|
|
|
|
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)
|
|
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(
|
|
s: PdfStream,
|
|
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"\u00d8 {dia_val:g}{depth_str}"
|
|
y_offset = ys1 + 2.0 - (line_idx * font_size_pt * 1.1)
|
|
|
|
s.begin_text().font("F1", font_size_pt * 0.85)
|
|
s.text_position(xs1 + 2.0, y_offset).show(dia_str)
|
|
s.end_text()
|
|
|
|
|
|
# =============================================================================
|
|
# Pitch Drawing
|
|
# =============================================================================
|
|
|
|
def _draw_pitch_marks(
|
|
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]],
|
|
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]])))
|
|
|
|
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
|
|
|
|
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]])))
|
|
|
|
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
|
|
s.begin_text().font("F1", font_size_pt * 0.8)
|
|
s.text_position(label_x, label_y).show(label)
|
|
s.end_text()
|
|
|
|
|
|
# =============================================================================
|
|
# Main Render Function
|
|
# =============================================================================
|
|
|
|
def render_sa2_measures_pdf(
|
|
s: PdfStream,
|
|
raw_geometry: Any,
|
|
draw_sa2_holes: bool = True,
|
|
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,
|
|
draw_wireframe: bool = True,
|
|
draw_boundary: bool = False,
|
|
draw_contour: bool = False,
|
|
draw_crease: bool = False,
|
|
crease_angle: float = 30.0,
|
|
) -> None:
|
|
"""
|
|
Renders ISO 129-1 compliant SA2 hole measurements (bores, concentric steps, Z-depth, pitch) on PDF stream.
|
|
"""
|
|
if raw_geometry is None or not draw_sa2_holes:
|
|
return
|
|
|
|
sa2_objects = extract_slicing_objects_bounds(raw_geometry)
|
|
if not sa2_objects:
|
|
return
|
|
|
|
# Automatic sizing based on minor grid
|
|
line_width_pt, font_size_pt = compute_effective_stroke_and_font(
|
|
line_width_pt / MM_TO_PT if line_width_pt else 0.0,
|
|
font_size_pt / MM_TO_PT if font_size_pt else 0.0,
|
|
minor_step_mm
|
|
)
|
|
|
|
# Setup: graphics state, line style, and SA2 color
|
|
sa2_lw_pt = line_width_pt * 0.8
|
|
sa2_color = (0.85, 0.15, 0.15)
|
|
|
|
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
|
|
h_obj_mm = max_y - min_y
|
|
|
|
sizing = SA2Sizing(w_obj_mm, h_obj_mm, line_width_pt, font_size_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 ---
|
|
w_str = f"{w_obj_mm/1000:g}m" if main_step >= 1000.0 else f"{w_obj_mm:g}mm"
|
|
_draw_horizontal_dimension(
|
|
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,
|
|
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"
|
|
_draw_vertical_dimension(
|
|
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,
|
|
sizing.arrow_len_pt, sizing.arrow_half_w_pt,
|
|
h_str, font_size_pt
|
|
)
|
|
|
|
# --- Hole Features ---
|
|
holes = extract_slicing_object_features(
|
|
so_item,
|
|
draw_wireframe=draw_wireframe,
|
|
draw_boundary=draw_boundary,
|
|
draw_contour=draw_contour,
|
|
draw_crease=draw_crease,
|
|
crease_angle=crease_angle,
|
|
bounds=(min_x, max_x, min_y, max_y),
|
|
)
|
|
|
|
# Concentric Bores Grouping & Hole Callouts
|
|
if draw_sa2_holes and holes:
|
|
hole_color = (0.10, 0.45, 0.90)
|
|
s.stroke_color(*hole_color).fill_color(*hole_color)
|
|
|
|
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]
|
|
|
|
xe, ye, xs1, ys1 = _draw_hole_leader_and_arrow(
|
|
s, 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 = _get_hole_depth_str(
|
|
mesh_verts_3d, total_z_span, hcx, hcy, hdia
|
|
)
|
|
_draw_hole_callout_text_only(
|
|
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)
|
|
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]))
|
|
|
|
for i in range(len(distinct_centers) - 1):
|
|
(cx1, cy1), (cx2, cy2) = distinct_centers[i], distinct_centers[i + 1]
|
|
dx_mm = abs(cx2 - cx1)
|
|
dy_mm = abs(cy2 - cy1)
|
|
|
|
if dx_mm > 0.5:
|
|
y_pitch_mm = min(cy1, cy2) - sizing.offset_mm * 0.5
|
|
_draw_pitch_marks(
|
|
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
|
|
)
|
|
|
|
if dy_mm > 0.5:
|
|
x_pitch_mm = max(cx1, cx2) + sizing.offset_mm * 0.5
|
|
_draw_pitch_marks(
|
|
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
|
|
)
|