543 lines
23 KiB
Python
543 lines
23 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
|
||
|
||
MM_TO_PT = 2.834645669291339
|
||
|
||
|
||
def escape_pdf_text(text: str) -> str:
|
||
"""
|
||
Escapes special PDF characters (parentheses, backslashes) and converts
|
||
WinAnsiEncoding symbols (Ø -> \330, ° -> \260, × -> \327, ± -> \261, µ -> \265)
|
||
into PDF octal escape codes.
|
||
"""
|
||
res = []
|
||
for ch in text:
|
||
if ch == "(":
|
||
res.append("\\(")
|
||
elif ch == ")":
|
||
res.append("\\)")
|
||
elif ch == "\\":
|
||
res.append("\\\\")
|
||
else:
|
||
code = ord(ch)
|
||
if code < 128:
|
||
res.append(ch)
|
||
elif code <= 255:
|
||
res.append(f"\\{code:03o}")
|
||
else:
|
||
res.append("?")
|
||
return "".join(res)
|
||
|
||
|
||
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
|
||
|
||
|
||
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.
|
||
"""
|
||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||
|
||
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, (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 extract_slicing_object_features(so_item: Any) -> tuple[
|
||
list[tuple[float, float, float]], # Holes (cx, cy, dia)
|
||
list[tuple[float, float, float, float, float]], # Arcs (cx, cy, r, mid_x, mid_y)
|
||
list[tuple[float, float, float, float, float]], # Chamfers (x1, y1, x2, y2, c_val)
|
||
list[tuple[float, float, float, float, float]] # Steps (x1, y1, x2, y2, length)
|
||
]:
|
||
"""
|
||
Extracts geometric features from a SlicingObject per ISO 129-1 standards:
|
||
1. Circular bores / holes: list of (center_x, center_y, diameter_mm)
|
||
2. Arc cutouts & fillets: list of (center_x, center_y, radius_mm, mid_x, mid_y)
|
||
3. Bevels / Chamfers: list of (x1, y1, x2, y2, c_val_mm)
|
||
4. Internal step / notch segments: list of (x1, y1, x2, y2, length_mm)
|
||
"""
|
||
b_edges = extract_boundary_edges(so_item)
|
||
if len(b_edges) == 0:
|
||
b_edges = extract_lineart_edges(so_item)
|
||
if len(b_edges) == 0:
|
||
b_edges = extract_unique_top_view_edges(so_item)
|
||
if len(b_edges) == 0:
|
||
return [], [], [], []
|
||
|
||
b_edges = deduplicate_2d_edges(b_edges)
|
||
|
||
b_edges = deduplicate_2d_edges(b_edges)
|
||
|
||
adj: dict[tuple[float, float], list[tuple[float, float]]] = {}
|
||
for p0, p1 in b_edges:
|
||
pt0 = (round(float(p0[0]), 2), round(float(p0[1]), 2))
|
||
pt1 = (round(float(p1[0]), 2), round(float(p1[1]), 2))
|
||
if pt0 == pt1:
|
||
continue
|
||
adj.setdefault(pt0, []).append(pt1)
|
||
adj.setdefault(pt1, []).append(pt0)
|
||
|
||
for pt, nbrs in adj.items():
|
||
nbrs.sort(key=lambda n: (n[0] - pt[0]) ** 2 + (n[1] - pt[1]) ** 2)
|
||
|
||
visited_edges: set[tuple[tuple[float, float], tuple[float, float]]] = set()
|
||
loops: list[NDArray[np.float64]] = []
|
||
|
||
for start_pt in list(adj.keys()):
|
||
for neighbor in adj[start_pt]:
|
||
edge_key = tuple(sorted([start_pt, neighbor]))
|
||
if edge_key in visited_edges:
|
||
continue
|
||
|
||
temp_visited = set()
|
||
loop_pts = [start_pt]
|
||
temp_visited.add(edge_key)
|
||
curr = neighbor
|
||
|
||
while curr != start_pt:
|
||
loop_pts.append(curr)
|
||
next_pt = None
|
||
for nxt in adj.get(curr, []):
|
||
ek = tuple(sorted([curr, nxt]))
|
||
if ek not in visited_edges and ek not in temp_visited:
|
||
temp_visited.add(ek)
|
||
next_pt = nxt
|
||
break
|
||
if next_pt is None:
|
||
break
|
||
curr = next_pt
|
||
|
||
if curr == start_pt:
|
||
visited_edges.update(temp_visited)
|
||
if len(loop_pts) >= 3:
|
||
loops.append(np.array(loop_pts, dtype=np.float64))
|
||
|
||
holes: list[tuple[float, float, float]] = []
|
||
arcs: list[tuple[float, float, float, float, float]] = []
|
||
chamfers: list[tuple[float, float, float, float, float]] = []
|
||
step_features: list[tuple[float, float, float, float, float]] = []
|
||
|
||
if so_item.mesh.size == 0:
|
||
return holes, arcs, chamfers, step_features
|
||
|
||
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())
|
||
overall_w = max_x - min_x
|
||
overall_h = max_y - min_y
|
||
|
||
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())
|
||
|
||
# 1. ISO Full Circular Bore (Ø)
|
||
if N >= 5 and r_mean_full > 0.5 and (r_std_full / r_mean_full) < 0.20:
|
||
if r_mean_full < max(overall_w, overall_h) * 0.48:
|
||
holes.append((cx_full, cy_full, r_mean_full * 2.0))
|
||
continue
|
||
|
||
# 2. Trace ISO Arc Cutouts & Fillets (R)
|
||
idx_i = 0
|
||
visited_indices: set[int] = set()
|
||
|
||
while idx_i < N:
|
||
if idx_i in visited_indices:
|
||
idx_i += 1
|
||
continue
|
||
|
||
p1 = loop[idx_i]
|
||
p2 = loop[(idx_i + 1) % N]
|
||
p3 = loop[(idx_i + 2) % N]
|
||
|
||
center, r_arc = circumcircle_3pts(p1, p2, p3)
|
||
if center is not None and 1.0 <= r_arc <= max(overall_w, overall_h) * 0.45:
|
||
arc_seq = [idx_i, (idx_i + 1) % N, (idx_i + 2) % N]
|
||
for j in range(idx_i + 3, idx_i + N):
|
||
idx_nxt = j % N
|
||
p_nxt = loop[idx_nxt]
|
||
dist_to_c = np.sqrt((p_nxt[0] - center[0]) ** 2 + (p_nxt[1] - center[1]) ** 2)
|
||
if abs(dist_to_c - r_arc) / r_arc < 0.12:
|
||
arc_seq.append(idx_nxt)
|
||
else:
|
||
break
|
||
|
||
if len(arc_seq) >= 4:
|
||
mid_idx = arc_seq[len(arc_seq) // 2]
|
||
mid_pt = loop[mid_idx]
|
||
arcs.append((float(center[0]), float(center[1]), float(r_arc), float(mid_pt[0]), float(mid_pt[1])))
|
||
visited_indices.update(arc_seq)
|
||
idx_i += len(arc_seq) - 1
|
||
continue
|
||
idx_i += 1
|
||
|
||
# 3. Trace ISO Chamfers / Bevels (C)
|
||
for k in range(N):
|
||
if k in visited_indices:
|
||
continue
|
||
p_prev = loop[(k - 1) % N]
|
||
p_curr = loop[k]
|
||
p_next = loop[(k + 1) % N]
|
||
p_next2 = loop[(k + 2) % N]
|
||
|
||
v1 = p_curr - p_prev
|
||
v2 = p_next - p_curr
|
||
v3 = p_next2 - p_next
|
||
|
||
l1, l2, l3 = float(np.linalg.norm(v1)), float(np.linalg.norm(v2)), float(np.linalg.norm(v3))
|
||
if l1 > 0 and l2 > 0 and l3 > 0:
|
||
cos12 = float(np.dot(v1, v2) / (l1 * l2))
|
||
cos23 = float(np.dot(v2, v3) / (l2 * l3))
|
||
if 0.45 <= abs(cos12) <= 0.88 and 0.45 <= abs(cos23) <= 0.88:
|
||
if 1.0 <= l2 <= min(overall_w, overall_h) * 0.4:
|
||
c_val = float(l2 * math.sin(math.pi / 4.0))
|
||
chamfers.append((float(p_curr[0]), float(p_curr[1]), float(p_next[0]), float(p_next[1]), c_val))
|
||
|
||
# 4. Interior Step Segments
|
||
for i in range(N):
|
||
p1 = loop[i]
|
||
p2 = loop[(i + 1) % N]
|
||
seg_len = float(np.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2))
|
||
|
||
is_horiz = abs(p2[1] - p1[1]) < 1e-2 and seg_len > 4.0 and abs(seg_len - overall_w) > 1e-2
|
||
is_vert = abs(p2[0] - p1[0]) < 1e-2 and seg_len > 4.0 and abs(seg_len - overall_h) > 1e-2
|
||
|
||
if is_horiz or is_vert:
|
||
on_outer_edge = (
|
||
abs(p1[0] - min_x) < 1e-2 and abs(p2[0] - min_x) < 1e-2 or
|
||
abs(p1[0] - max_x) < 1e-2 and abs(p2[0] - max_x) < 1e-2 or
|
||
abs(p1[1] - min_y) < 1e-2 and abs(p2[1] - min_y) < 1e-2 or
|
||
abs(p1[1] - max_y) < 1e-2 and abs(p2[1] - max_y) < 1e-2
|
||
)
|
||
if not on_outer_edge:
|
||
step_features.append((float(p1[0]), float(p1[1]), float(p2[0]), float(p2[1]), seg_len))
|
||
|
||
return holes, arcs, chamfers, step_features
|
||
|
||
|
||
def render_sa2_measures_pdf(
|
||
stream_cmds: list[str],
|
||
raw_geometry: Any,
|
||
draw_sa2_measures: bool,
|
||
line_width_pt: float,
|
||
main_step: float,
|
||
to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]],
|
||
font_size_pt: float | None = None
|
||
) -> None:
|
||
"""
|
||
Renders ISO 129-1 compliant SA2 automated technical measurements on PDF stream.
|
||
"""
|
||
if not draw_sa2_measures or raw_geometry is None:
|
||
return
|
||
|
||
sa2_objects = extract_slicing_objects_bounds(raw_geometry)
|
||
if not sa2_objects:
|
||
return
|
||
|
||
stream_cmds.append("/GS100 gs")
|
||
sa2_lw_pt = max(0.4 * MM_TO_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")
|
||
|
||
if font_size_pt is None or font_size_pt <= 0:
|
||
font_size_pt = max(8.0, 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
|
||
|
||
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
|
||
|
||
offset_mm = 8.0
|
||
ext_overhang_mm = 2.0
|
||
|
||
# --- Width Dimension (Horizontal below SlicingObject) ---
|
||
y_dim_mm = min_y - offset_mm
|
||
|
||
p_ext_l1 = to_pdf_pt(np.array([[min_x, min_y - 1.0]]))
|
||
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 - 1.0]]))
|
||
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 - 1.0, 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 - 1.0, 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")
|
||
|
||
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")
|
||
|
||
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")
|
||
|
||
# --- Additional SA2 Feature Measures: Bores (Ø), Arcs (R), Chamfers (C) & Pitch ---
|
||
holes, arcs, chamfers, step_features = extract_slicing_object_features(so_item)
|
||
|
||
# 1. ISO 129-1 Circular Bores / Hole Diameter Callouts (Ø - Electric Blue)
|
||
if 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")
|
||
|
||
for cx_mm, cy_mm, dia_mm in holes:
|
||
r_mm = dia_mm / 2.0
|
||
px_edge = cx_mm + r_mm * 0.7071
|
||
py_edge = cy_mm + r_mm * 0.7071
|
||
px_shoulder = px_edge + 8.0
|
||
py_shoulder = py_edge + 8.0
|
||
|
||
p_edge = to_pdf_pt(np.array([[px_edge, py_edge]]))
|
||
p_sh = to_pdf_pt(np.array([[px_shoulder, py_shoulder]]))
|
||
p_sh2 = to_pdf_pt(np.array([[px_shoulder + 10.0, py_shoulder]]))
|
||
|
||
x_e, y_e = p_edge[0, 0], p_edge[0, 1]
|
||
x_s1, y_s1 = p_sh[0, 0], p_sh[0, 1]
|
||
x_s2 = p_sh2[0, 0]
|
||
|
||
stream_cmds.append(f"{x_e:.2f} {y_e:.2f} m {x_s1:.2f} {y_s1:.2f} l {x_s2:.2f} {y_s1:.2f} l S")
|
||
|
||
stream_cmds.append(
|
||
f"{x_e + arrow_len_pt * 0.7071:.2f} {y_e + arrow_len_pt * 0.7071:.2f} m "
|
||
f"{x_e:.2f} {y_e:.2f} l "
|
||
f"{x_e + arrow_len_pt * 0.3535:.2f} {y_e + arrow_len_pt * 0.95:.2f} l S"
|
||
)
|
||
|
||
dia_val = round(dia_mm, 1)
|
||
dia_str = f"Ø{dia_val:g}mm" if main_step < 1000.0 else f"Ø{dia_val/1000:g}m"
|
||
stream_cmds.append("BT")
|
||
stream_cmds.append(f"/F1 {font_size_pt * 0.9:.1f} Tf")
|
||
stream_cmds.append(f"1 0 0 1 {x_s1 + 2.0:.2f} {y_s1 + 2.0:.2f} Tm")
|
||
stream_cmds.append(f"({escape_pdf_text(dia_str)}) Tj")
|
||
stream_cmds.append("ET")
|
||
|
||
# 2. ISO 129-1 Arc Cutouts & Corner Fillets (R - Amber Orange)
|
||
if arcs:
|
||
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")
|
||
|
||
for acx_mm, acy_mm, r_mm, mid_x_mm, mid_y_mm in arcs[:4]:
|
||
p_center = to_pdf_pt(np.array([[acx_mm, acy_mm]]))
|
||
p_mid = to_pdf_pt(np.array([[mid_x_mm, mid_y_mm]]))
|
||
p_sh1 = to_pdf_pt(np.array([[mid_x_mm + 6.0, mid_y_mm + 6.0]]))
|
||
p_sh2 = to_pdf_pt(np.array([[mid_x_mm + 14.0, mid_y_mm + 6.0]]))
|
||
|
||
xc, yc = p_center[0, 0], p_center[0, 1]
|
||
xm, ym = p_mid[0, 0], p_mid[0, 1]
|
||
xs1, ys1 = p_sh1[0, 0], p_sh1[0, 1]
|
||
xs2 = p_sh2[0, 0]
|
||
|
||
stream_cmds.append(f"{xm:.2f} {ym:.2f} m {xs1:.2f} {ys1:.2f} l {xs2:.2f} {ys1:.2f} l S")
|
||
|
||
ux, uy = (xm - xc), (ym - yc)
|
||
dist_cm = math.sqrt(ux*ux + uy*uy)
|
||
if dist_cm > 1e-3:
|
||
ux, uy = ux / dist_cm, uy / dist_cm
|
||
stream_cmds.append(
|
||
f"{xm - arrow_len_pt * ux + arrow_half_w_pt * uy:.2f} {ym - arrow_len_pt * uy - arrow_half_w_pt * ux:.2f} m "
|
||
f"{xm:.2f} {ym:.2f} l "
|
||
f"{xm - arrow_len_pt * ux - arrow_half_w_pt * uy:.2f} {ym - arrow_len_pt * uy + arrow_half_w_pt * ux:.2f} l S"
|
||
)
|
||
|
||
r_val = round(r_mm, 1)
|
||
r_str = f"R{r_val:g}mm"
|
||
stream_cmds.append("BT")
|
||
stream_cmds.append(f"/F1 {font_size_pt * 0.9:.1f} Tf")
|
||
stream_cmds.append(f"1 0 0 1 {xs1 + 2.0:.2f} {ys1 + 2.0:.2f} Tm")
|
||
stream_cmds.append(f"({escape_pdf_text(r_str)}) Tj")
|
||
stream_cmds.append("ET")
|
||
|
||
# 3. ISO 129-1 Bevels & Chamfers (C / 45° - Violet Purple)
|
||
if chamfers:
|
||
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")
|
||
|
||
for ch_x1, ch_y1, ch_x2, ch_y2, c_val in chamfers[:4]:
|
||
ch_mid_x = (ch_x1 + ch_x2) / 2.0
|
||
ch_mid_y = (ch_y1 + ch_y2) / 2.0
|
||
sh_x = ch_mid_x + 6.0
|
||
sh_y = ch_mid_y + 6.0
|
||
|
||
p_ch_mid = to_pdf_pt(np.array([[ch_mid_x, ch_mid_y]]))
|
||
p_ch_sh1 = to_pdf_pt(np.array([[sh_x, sh_y]]))
|
||
p_ch_sh2 = to_pdf_pt(np.array([[sh_x + 10.0, sh_y]]))
|
||
|
||
x_cm, y_cm = p_ch_mid[0, 0], p_ch_mid[0, 1]
|
||
x_cs1, y_cs1 = p_ch_sh1[0, 0], p_ch_sh1[0, 1]
|
||
x_cs2 = p_ch_sh2[0, 0]
|
||
|
||
stream_cmds.append(f"{x_cm:.2f} {y_cm:.2f} m {x_cs1:.2f} {y_cs1:.2f} l {x_cs2:.2f} {y_cs1:.2f} l S")
|
||
|
||
stream_cmds.append(
|
||
f"{x_cm + arrow_len_pt * 0.7071:.2f} {y_cm + arrow_len_pt * 0.7071:.2f} m "
|
||
f"{x_cm:.2f} {y_cm:.2f} l "
|
||
f"{x_cm + arrow_len_pt * 0.3535:.2f} {y_cm + arrow_len_pt * 0.95:.2f} l S"
|
||
)
|
||
|
||
c_val_rounded = round(c_val, 1)
|
||
c_str = f"C{c_val_rounded:g}mm"
|
||
stream_cmds.append("BT")
|
||
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
||
stream_cmds.append(f"1 0 0 1 {x_cs1 + 2.0:.2f} {y_cs1 + 2.0:.2f} Tm")
|
||
stream_cmds.append(f"({escape_pdf_text(c_str)}) Tj")
|
||
stream_cmds.append("ET")
|
||
|
||
# 4. Hole Center Pitch Distances (Teal Green)
|
||
if len(holes) >= 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")
|
||
|
||
sorted_holes = sorted(holes, key=lambda h: (h[0], h[1]))
|
||
for i in range(len(sorted_holes) - 1):
|
||
h1, h2 = sorted_holes[i], sorted_holes[i + 1]
|
||
dist_mm = math.sqrt((h2[0] - h1[0])**2 + (h2[1] - h1[1])**2)
|
||
if dist_mm > 1.0:
|
||
p_h1 = to_pdf_pt(np.array([[h1[0], h1[1]]]))
|
||
p_h2 = to_pdf_pt(np.array([[h2[0], h2[1]]]))
|
||
x1, y1 = p_h1[0, 0], p_h1[0, 1]
|
||
x2, y2 = p_h2[0, 0], p_h2[0, 1]
|
||
|
||
stream_cmds.append(f"{x1:.2f} {y1:.2f} m {x2:.2f} {y2:.2f} l S")
|
||
|
||
pitch_str = f"PITCH: {dist_mm:g}mm"
|
||
stream_cmds.append("BT")
|
||
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
||
stream_cmds.append(f"1 0 0 1 {(x1+x2)/2.0:.2f} {(y1+y2)/2.0 + 3.0:.2f} Tm")
|
||
stream_cmds.append(f"({escape_pdf_text(pitch_str)}) Tj")
|
||
stream_cmds.append("ET")
|
||
|
||
# 5. Interior Step / Notch Feature Segment Lengths (Deep Purple)
|
||
if step_features:
|
||
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")
|
||
|
||
for x1_m, y1_m, x2_m, y2_m, step_len in step_features[:4]:
|
||
p_s1 = to_pdf_pt(np.array([[x1_m, y1_m]]))
|
||
p_s2 = to_pdf_pt(np.array([[x2_m, y2_m]]))
|
||
xs1, ys1 = p_s1[0, 0], p_s1[0, 1]
|
||
xs2, ys2 = p_s2[0, 0], p_s2[0, 1]
|
||
|
||
stream_cmds.append(f"{xs1:.2f} {ys1:.2f} m {xs2:.2f} {ys2:.2f} l S")
|
||
step_str = f"{step_len:g}mm"
|
||
stream_cmds.append("BT")
|
||
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf")
|
||
stream_cmds.append(f"1 0 0 1 {(xs1+xs2)/2.0:.2f} {(ys1+ys2)/2.0 + 2.0:.2f} Tm")
|
||
stream_cmds.append(f"({escape_pdf_text(step_str)}) Tj")
|
||
stream_cmds.append("ET")
|