sa2
This commit is contained in:
parent
c862e8976b
commit
7b8c54c2a5
9 changed files with 565 additions and 196 deletions
37
core/pdf_exporter/depth_rasterizer.py
Normal file
37
core/pdf_exporter/depth_rasterizer.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""
|
||||||
|
SA2 Performance Profiling & Vector Edge Debug Export Module.
|
||||||
|
Saves input vector edge debug data and cProfile statistics when debug_sa2 is enabled.
|
||||||
|
"""
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
|
|
||||||
|
def save_sa2_debug_data(
|
||||||
|
edges: NDArray[np.float64] | None,
|
||||||
|
obj_name: str,
|
||||||
|
filepath: str | Any
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Saves debug edge vector data files in the same folder as the output PDF:
|
||||||
|
- {name}_sa2_debug_edges.txt (Input 2D edge vector coordinates fed to SA2)
|
||||||
|
"""
|
||||||
|
if filepath is None:
|
||||||
|
return
|
||||||
|
from pathlib import Path
|
||||||
|
pdf_path = Path(filepath)
|
||||||
|
out_dir = pdf_path.parent
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
clean_name = pdf_path.stem if pdf_path.suffix.lower() == ".pdf" else str(obj_name)
|
||||||
|
base_prefix = out_dir / f"{clean_name}_sa2_debug"
|
||||||
|
|
||||||
|
# Save input edge coordinates as text file
|
||||||
|
if edges is not None and len(edges) > 0:
|
||||||
|
txt_file = f"{base_prefix}_edges.txt"
|
||||||
|
with open(txt_file, "w") as f:
|
||||||
|
f.write(f"# SA2 Input 2D Edges Debug for {clean_name}\n")
|
||||||
|
f.write(f"# Format: x1 y1 x2 y2\n")
|
||||||
|
for segment in edges:
|
||||||
|
(x1, y1), (x2, y2) = segment[0], segment[1]
|
||||||
|
f.write(f"{x1:.4f} {y1:.4f} {x2:.4f} {y2:.4f}\n")
|
||||||
|
|
@ -163,7 +163,7 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
|
||||||
# - normal.z > 0: back-facing (facing away)
|
# - normal.z > 0: back-facing (facing away)
|
||||||
# - normal.z == 0: edge-on / vertical
|
# - normal.z == 0: edge-on / vertical
|
||||||
normal_z = normals_unit[:, 2]
|
normal_z = normals_unit[:, 2]
|
||||||
epsilon = 1e-8
|
epsilon = 1e-3
|
||||||
face_category = np.zeros(num_tris, dtype=int)
|
face_category = np.zeros(num_tris, dtype=int)
|
||||||
face_category[normal_z < -epsilon] = 1 # Front-facing
|
face_category[normal_z < -epsilon] = 1 # Front-facing
|
||||||
face_category[normal_z > epsilon] = 2 # Back-facing
|
face_category[normal_z > epsilon] = 2 # Back-facing
|
||||||
|
|
@ -241,16 +241,16 @@ def extract_lineart_edges(mesh_data: Any, max_angle_deg: float = 30.0) -> NDArra
|
||||||
sorted_edge_ids = inverse_indices[sort_by_edge]
|
sorted_edge_ids = inverse_indices[sort_by_edge]
|
||||||
sorted_normals = edge_normals[sort_by_edge]
|
sorted_normals = edge_normals[sort_by_edge]
|
||||||
|
|
||||||
splits = np.where(sorted_edge_ids[:-1] != sorted_edge_ids[1:])[0] + 1
|
# Vectorized pair matching for adjacent faces sharing the same edge ID
|
||||||
edge_groups = np.split(sorted_normals, splits)
|
match = sorted_edge_ids[:-1] == sorted_edge_ids[1:]
|
||||||
unique_group_ids = np.split(sorted_edge_ids, splits)
|
if np.any(match):
|
||||||
|
n1 = sorted_normals[:-1][match]
|
||||||
|
n2 = sorted_normals[1:][match]
|
||||||
|
e_ids = sorted_edge_ids[:-1][match]
|
||||||
|
|
||||||
for grp_normals, grp_ids in zip(edge_groups, unique_group_ids):
|
dot_prods = np.sum(n1 * n2, axis=1)
|
||||||
edge_id = grp_ids[0]
|
is_crease = dot_prods < (cos_threshold - 1e-6)
|
||||||
if len(grp_normals) >= 2:
|
rule3_unique[e_ids[is_crease]] = True
|
||||||
dots = grp_normals @ grp_normals.T
|
|
||||||
if np.any(dots < cos_threshold - 1e-6):
|
|
||||||
rule3_unique[edge_id] = True
|
|
||||||
|
|
||||||
marked_unique_mask = rule1_unique | rule2_unique | rule3_unique
|
marked_unique_mask = rule1_unique | rule2_unique | rule3_unique
|
||||||
marked_first_indices = first_indices[marked_unique_mask]
|
marked_first_indices = first_indices[marked_unique_mask]
|
||||||
|
|
@ -414,8 +414,8 @@ def extract_silhouette_contour_edges(mesh_data: Any) -> NDArray[np.float64]:
|
||||||
normal_z = normals_unit[:, 2]
|
normal_z = normals_unit[:, 2]
|
||||||
|
|
||||||
# Assign each face to a category: 0=vertical/edge-on, 1=front, 2=back
|
# Assign each face to a category: 0=vertical/edge-on, 1=front, 2=back
|
||||||
# We use a small epsilon to handle floating point
|
# We use epsilon = 1e-3 to robustly absorb floating point noise on perpendicular faces
|
||||||
epsilon = 1e-8
|
epsilon = 1e-3
|
||||||
face_category = np.zeros(num_tris, dtype=int)
|
face_category = np.zeros(num_tris, dtype=int)
|
||||||
face_category[normal_z < -epsilon] = 1 # Front-facing
|
face_category[normal_z < -epsilon] = 1 # Front-facing
|
||||||
face_category[normal_z > epsilon] = 2 # Back-facing
|
face_category[normal_z > epsilon] = 2 # Back-facing
|
||||||
|
|
|
||||||
|
|
@ -99,31 +99,14 @@ def extract_object_bounding_boxes(mesh_data: Any) -> NDArray[np.float64]:
|
||||||
"""
|
"""
|
||||||
Extracts 2D top-view bounding box rectangle edge segments (shape N x 2 x 2) for each SlicingObject in mesh_data.
|
Extracts 2D top-view bounding box rectangle edge segments (shape N x 2 x 2) for each SlicingObject in mesh_data.
|
||||||
"""
|
"""
|
||||||
from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
from .sa2_measures import extract_slicing_objects_bounds
|
||||||
|
|
||||||
|
sa2_objects = extract_slicing_objects_bounds(mesh_data)
|
||||||
|
if not sa2_objects:
|
||||||
|
return np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
|
||||||
all_segments: list[np.ndarray] = []
|
all_segments: list[np.ndarray] = []
|
||||||
|
for item, name, min_x, max_x, min_y, max_y in sa2_objects:
|
||||||
def process_item(item: Any):
|
|
||||||
if item is None:
|
|
||||||
return
|
|
||||||
elif isinstance(item, (list, tuple)):
|
|
||||||
for sub in item:
|
|
||||||
process_item(sub)
|
|
||||||
elif isinstance(item, SlicingGroup):
|
|
||||||
for coll in item.collections.values():
|
|
||||||
process_item(coll)
|
|
||||||
elif isinstance(item, SlicingCollection):
|
|
||||||
for so in item.objects:
|
|
||||||
process_item(so)
|
|
||||||
elif isinstance(item, SlicingObject):
|
|
||||||
if item.mesh.size > 0:
|
|
||||||
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())
|
|
||||||
|
|
||||||
rect = np.array([
|
rect = np.array([
|
||||||
[[min_x, min_y], [max_x, min_y]],
|
[[min_x, min_y], [max_x, min_y]],
|
||||||
[[max_x, min_y], [max_x, max_y]],
|
[[max_x, min_y], [max_x, max_y]],
|
||||||
|
|
@ -132,9 +115,6 @@ def extract_object_bounding_boxes(mesh_data: Any) -> NDArray[np.float64]:
|
||||||
], dtype=np.float64)
|
], dtype=np.float64)
|
||||||
all_segments.append(rect)
|
all_segments.append(rect)
|
||||||
|
|
||||||
process_item(mesh_data)
|
|
||||||
if not all_segments:
|
|
||||||
return np.empty((0, 2, 2), dtype=np.float64)
|
|
||||||
return np.concatenate(all_segments, axis=0)
|
return np.concatenate(all_segments, axis=0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,15 @@ from typing import Any, Callable
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
||||||
from .geometry import extract_lineart_edges, extract_unique_top_view_edges, deduplicate_2d_edges, extract_boundary_edges
|
from .geometry import (
|
||||||
|
extract_lineart_edges,
|
||||||
MM_TO_PT = 2.834645669291339
|
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
|
||||||
|
|
||||||
|
|
||||||
def escape_pdf_text(text: str) -> str:
|
def escape_pdf_text(text: str) -> str:
|
||||||
|
|
@ -37,6 +43,7 @@ def escape_pdf_text(text: str) -> str:
|
||||||
return "".join(res)
|
return "".join(res)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def circumcircle_3pts(p1: NDArray, p2: NDArray, p3: NDArray) -> tuple[tuple[float, float] | None, float | None]:
|
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."""
|
"""Calculate center and radius of circumcircle passing through 3 2D points."""
|
||||||
ax, ay = float(p1[0]), float(p1[1])
|
ax, ay = float(p1[0]), float(p1[1])
|
||||||
|
|
@ -89,7 +96,15 @@ def extract_slicing_objects_bounds(mesh_data: Any) -> list[tuple[Any, str, float
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def extract_slicing_object_features(so_item: Any) -> tuple[
|
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,
|
||||||
|
) -> tuple[
|
||||||
list[tuple[float, float, float]], # Holes (cx, cy, dia)
|
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]], # 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]], # Chamfers (x1, y1, x2, y2, c_val)
|
||||||
|
|
@ -97,23 +112,45 @@ def extract_slicing_object_features(so_item: Any) -> tuple[
|
||||||
]:
|
]:
|
||||||
"""
|
"""
|
||||||
Extracts geometric features from a SlicingObject per ISO 129-1 standards:
|
Extracts geometric features from a SlicingObject per ISO 129-1 standards:
|
||||||
|
Source geometry is derived from the visible edge layers (wireframe, boundary, contour, crease).
|
||||||
1. Circular bores / holes: list of (center_x, center_y, diameter_mm)
|
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)
|
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)
|
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)
|
4. Internal step / notch segments: list of (x1, y1, x2, y2, length_mm)
|
||||||
"""
|
"""
|
||||||
b_edges = extract_boundary_edges(so_item)
|
edge_list: list[NDArray[np.float64]] = []
|
||||||
if len(b_edges) == 0:
|
|
||||||
b_edges = extract_lineart_edges(so_item)
|
if draw_wireframe:
|
||||||
if len(b_edges) == 0:
|
w_edges = extract_unique_top_view_edges(so_item)
|
||||||
b_edges = extract_unique_top_view_edges(so_item)
|
if len(w_edges) > 0:
|
||||||
|
edge_list.append(w_edges)
|
||||||
|
|
||||||
|
# Note: extract_crease_edges (lineart) already includes all boundary edges,
|
||||||
|
# so we avoid redundant extract_boundary_edges when draw_crease is active.
|
||||||
|
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)
|
||||||
|
|
||||||
if len(b_edges) == 0:
|
if len(b_edges) == 0:
|
||||||
return [], [], [], []
|
return [], [], [], []
|
||||||
|
|
||||||
b_edges = deduplicate_2d_edges(b_edges)
|
|
||||||
|
|
||||||
b_edges = deduplicate_2d_edges(b_edges)
|
|
||||||
|
|
||||||
adj: dict[tuple[float, float], list[tuple[float, float]]] = {}
|
adj: dict[tuple[float, float], list[tuple[float, float]]] = {}
|
||||||
for p0, p1 in b_edges:
|
for p0, p1 in b_edges:
|
||||||
pt0 = (round(float(p0[0]), 2), round(float(p0[1]), 2))
|
pt0 = (round(float(p0[0]), 2), round(float(p0[1]), 2))
|
||||||
|
|
@ -163,12 +200,15 @@ def extract_slicing_object_features(so_item: Any) -> tuple[
|
||||||
chamfers: list[tuple[float, float, float, float, float]] = []
|
chamfers: list[tuple[float, float, float, float, float]] = []
|
||||||
step_features: list[tuple[float, float, float, float, float]] = []
|
step_features: list[tuple[float, float, float, float, float]] = []
|
||||||
|
|
||||||
if so_item.mesh.size == 0:
|
if bounds is not None:
|
||||||
return holes, arcs, chamfers, step_features
|
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)
|
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_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())
|
min_y, max_y = float(verts_2d[:, 1].min()), float(verts_2d[:, 1].max())
|
||||||
|
else:
|
||||||
|
return holes, arcs, chamfers, step_features
|
||||||
|
|
||||||
overall_w = max_x - min_x
|
overall_w = max_x - min_x
|
||||||
overall_h = max_y - min_y
|
overall_h = max_y - min_y
|
||||||
|
|
||||||
|
|
@ -278,7 +318,12 @@ def render_sa2_measures_pdf(
|
||||||
main_step: float = 10.0,
|
main_step: float = 10.0,
|
||||||
to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]] | None = None,
|
to_pdf_pt: Callable[[NDArray[np.float64]], NDArray[np.float64]] | None = None,
|
||||||
font_size_pt: float | None = None,
|
font_size_pt: float | None = None,
|
||||||
line_width_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:
|
) -> 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.
|
||||||
|
|
@ -295,11 +340,11 @@ def render_sa2_measures_pdf(
|
||||||
return
|
return
|
||||||
|
|
||||||
# Automatic sizing based on minor grid
|
# Automatic sizing based on minor grid
|
||||||
MM_TO_PT = 2.834645669291339
|
line_width_pt, font_size_pt = compute_effective_stroke_and_font(
|
||||||
if line_width_pt is None or line_width_pt <= 0:
|
line_width_pt / MM_TO_PT if line_width_pt else 0.0,
|
||||||
line_width_pt = minor_step_mm * 0.05 * MM_TO_PT
|
font_size_pt / MM_TO_PT if font_size_pt else 0.0,
|
||||||
if font_size_pt is None or font_size_pt <= 0:
|
minor_step_mm
|
||||||
font_size_pt = minor_step_mm * MM_TO_PT
|
)
|
||||||
|
|
||||||
stream_cmds.append("/GS100 gs")
|
stream_cmds.append("/GS100 gs")
|
||||||
sa2_lw_pt = line_width_pt * 0.8
|
sa2_lw_pt = line_width_pt * 0.8
|
||||||
|
|
@ -310,23 +355,23 @@ 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")
|
||||||
|
|
||||||
# SA2 arrow length: scale with line width (which is based on minor grid)
|
|
||||||
arrow_len_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:
|
for so_item, obj_name, min_x, max_x, min_y, max_y in sa2_objects:
|
||||||
w_obj_mm = max_x - min_x
|
w_obj_mm = max_x - min_x
|
||||||
h_obj_mm = max_y - min_y
|
h_obj_mm = max_y - min_y
|
||||||
|
|
||||||
offset_mm = 8.0
|
sizing = SA2Sizing(w_obj_mm, h_obj_mm, line_width_pt, font_size_pt)
|
||||||
ext_overhang_mm = 2.0
|
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
|
||||||
|
|
||||||
# --- Width Dimension (Horizontal below SlicingObject) ---
|
# --- Width Dimension (Horizontal below SlicingObject) ---
|
||||||
y_dim_mm = min_y - offset_mm
|
y_dim_mm = min_y - offset_mm
|
||||||
|
|
||||||
p_ext_l1 = to_pdf_pt(np.array([[min_x, min_y - 1.0]]))
|
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_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_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]]))
|
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_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")
|
||||||
|
|
@ -353,9 +398,9 @@ def render_sa2_measures_pdf(
|
||||||
# --- Height Dimension (Vertical to the left of SlicingObject) ---
|
# --- Height Dimension (Vertical to the left of SlicingObject) ---
|
||||||
x_dim_mm = min_x - offset_mm
|
x_dim_mm = min_x - offset_mm
|
||||||
|
|
||||||
p_ext_b1 = to_pdf_pt(np.array([[min_x - 1.0, min_y]]))
|
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_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_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]]))
|
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_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")
|
||||||
|
|
@ -399,42 +444,88 @@ def render_sa2_measures_pdf(
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
# --- Additional SA2 Feature Measures: Bores (Ø), Arcs (R), Chamfers (C) & Pitch ---
|
# --- Additional SA2 Feature Measures: Bores (Ø), Arcs (R), Chamfers (C) & Pitch ---
|
||||||
holes, arcs, chamfers, step_features = extract_slicing_object_features(so_item)
|
holes, arcs, chamfers, step_features = 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),
|
||||||
|
debug_sa2=debug_sa2,
|
||||||
|
filepath=filepath,
|
||||||
|
)
|
||||||
|
|
||||||
# 1. ISO 129-1 Circular Bores / Hole Diameter Callouts (Ø - Electric Blue)
|
# 1. Concentric Bores Grouping & ISO 129-1 Callouts with Z-depth Specs (Electric Blue)
|
||||||
if draw_sa2_holes and 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")
|
||||||
|
|
||||||
for cx_mm, cy_mm, dia_mm in holes:
|
# Group holes by center location (tolerance = 0.5mm)
|
||||||
r_mm = dia_mm / 2.0
|
concentric_groups: list[list[tuple[float, float, float]]] = []
|
||||||
px_edge = cx_mm + r_mm * 0.7071
|
for cx, cy, dia in holes:
|
||||||
py_edge = cy_mm + r_mm * 0.7071
|
added = False
|
||||||
px_shoulder = px_edge + 8.0
|
for grp in concentric_groups:
|
||||||
py_shoulder = py_edge + 8.0
|
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)])
|
||||||
|
|
||||||
|
# Query 3D Z-depth information for hole callouts
|
||||||
|
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())
|
||||||
|
|
||||||
|
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_edge = to_pdf_pt(np.array([[px_edge, py_edge]]))
|
||||||
p_sh = to_pdf_pt(np.array([[px_shoulder, py_shoulder]]))
|
p_sh1 = to_pdf_pt(np.array([[px_sh1, py_sh1]]))
|
||||||
p_sh2 = to_pdf_pt(np.array([[px_shoulder + 10.0, py_shoulder]]))
|
p_sh2 = to_pdf_pt(np.array([[px_sh1 + leader_mm, py_sh1]]))
|
||||||
|
|
||||||
x_e, y_e = p_edge[0, 0], p_edge[0, 1]
|
xe, ye = p_edge[0, 0], p_edge[0, 1]
|
||||||
x_s1, y_s1 = p_sh[0, 0], p_sh[0, 1]
|
xs1, ys1 = p_sh1[0, 0], p_sh1[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"{xe:.2f} {ye:.2f} m {xs1:.2f} {ys1:.2f} l {p_sh2[0, 0]:.2f} {ys1:.2f} l S")
|
||||||
stream_cmds.append(
|
stream_cmds.append(
|
||||||
f"{x_e + arrow_len_pt * 0.7071:.2f} {y_e + arrow_len_pt * 0.7071:.2f} m "
|
f"{xe + arrow_len_pt * 0.7071:.2f} {ye + arrow_len_pt * 0.7071:.2f} m "
|
||||||
f"{x_e:.2f} {y_e:.2f} l "
|
f"{xe:.2f} {ye:.2f} l "
|
||||||
f"{x_e + arrow_len_pt * 0.3535:.2f} {y_e + arrow_len_pt * 0.95:.2f} l S"
|
f"{xe + arrow_len_pt * 0.3535:.2f} {ye + arrow_len_pt * 0.95:.2f} l S"
|
||||||
)
|
)
|
||||||
|
|
||||||
dia_val = round(dia_mm, 1)
|
for line_idx, (hcx, hcy, hdia) in enumerate(grp):
|
||||||
dia_str = f"Ø{dia_val:g}mm" if main_step < 1000.0 else f"Ø{dia_val/1000:g}m"
|
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" \u2334{h_z_depth:.1f}mm"
|
||||||
|
|
||||||
|
dia_val = round(hdia, 1)
|
||||||
|
dia_str = f"Ø{dia_val:g}mm{depth_str}"
|
||||||
|
y_text_offset = ys1 + 2.0 - (line_idx * font_size_pt * 1.1)
|
||||||
|
|
||||||
stream_cmds.append("BT")
|
stream_cmds.append("BT")
|
||||||
stream_cmds.append(f"/F1 {font_size_pt * 0.9:.1f} Tf")
|
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.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"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(f"({escape_pdf_text(dia_str)}) Tj")
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
|
|
@ -445,17 +536,18 @@ def render_sa2_measures_pdf(
|
||||||
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]:
|
for acx_mm, acy_mm, r_mm, mid_x_mm, mid_y_mm in arcs[:4]:
|
||||||
|
arc_slant, arc_leader = sizing.get_arc_callout_lengths(r_mm)
|
||||||
|
|
||||||
p_center = to_pdf_pt(np.array([[acx_mm, acy_mm]]))
|
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_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_sh1 = to_pdf_pt(np.array([[mid_x_mm + arc_slant, mid_y_mm + arc_slant]]))
|
||||||
p_sh2 = to_pdf_pt(np.array([[mid_x_mm + 14.0, mid_y_mm + 6.0]]))
|
p_sh2 = to_pdf_pt(np.array([[mid_x_mm + arc_slant + arc_leader, mid_y_mm + arc_slant]]))
|
||||||
|
|
||||||
xc, yc = p_center[0, 0], p_center[0, 1]
|
xc, yc = p_center[0, 0], p_center[0, 1]
|
||||||
xm, ym = p_mid[0, 0], p_mid[0, 1]
|
xm, ym = p_mid[0, 0], p_mid[0, 1]
|
||||||
xs1, ys1 = p_sh1[0, 0], p_sh1[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")
|
stream_cmds.append(f"{xm:.2f} {ym:.2f} m {xs1:.2f} {ys1:.2f} l {p_sh2[0, 0]:.2f} {ys1:.2f} l S")
|
||||||
|
|
||||||
ux, uy = (xm - xc), (ym - yc)
|
ux, uy = (xm - xc), (ym - yc)
|
||||||
dist_cm = math.sqrt(ux*ux + uy*uy)
|
dist_cm = math.sqrt(ux*ux + uy*uy)
|
||||||
|
|
@ -475,7 +567,7 @@ def render_sa2_measures_pdf(
|
||||||
stream_cmds.append(f"({escape_pdf_text(r_str)}) Tj")
|
stream_cmds.append(f"({escape_pdf_text(r_str)}) Tj")
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
# 3. ISO 129-1 Bevels & Chamfers (C / 45° - Violet Purple)
|
# 3. ISO 129-1 Bevels, Chamfers & Rib Angles (C / θ - Violet Purple)
|
||||||
if draw_sa2_chamfers and 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")
|
||||||
|
|
@ -484,18 +576,22 @@ def render_sa2_measures_pdf(
|
||||||
for ch_x1, ch_y1, ch_x2, ch_y2, c_val in chamfers[:4]:
|
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_x = (ch_x1 + ch_x2) / 2.0
|
||||||
ch_mid_y = (ch_y1 + ch_y2) / 2.0
|
ch_mid_y = (ch_y1 + ch_y2) / 2.0
|
||||||
sh_x = ch_mid_x + 6.0
|
dx_ch = abs(ch_x2 - ch_x1)
|
||||||
sh_y = ch_mid_y + 6.0
|
dy_ch = abs(ch_y2 - ch_y1)
|
||||||
|
angle_deg = math.degrees(math.atan2(dy_ch, max(dx_ch, 1e-4)))
|
||||||
|
|
||||||
|
ch_slant, ch_leader = sizing.get_chamfer_callout_lengths(c_val)
|
||||||
|
sh_x = ch_mid_x + ch_slant
|
||||||
|
sh_y = ch_mid_y + ch_slant
|
||||||
|
|
||||||
p_ch_mid = to_pdf_pt(np.array([[ch_mid_x, ch_mid_y]]))
|
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_sh1 = to_pdf_pt(np.array([[sh_x, sh_y]]))
|
||||||
p_ch_sh2 = to_pdf_pt(np.array([[sh_x + 10.0, sh_y]]))
|
p_ch_sh2 = to_pdf_pt(np.array([[sh_x + ch_leader, sh_y]]))
|
||||||
|
|
||||||
x_cm, y_cm = p_ch_mid[0, 0], p_ch_mid[0, 1]
|
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_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:.2f} {y_cm:.2f} m {x_cs1:.2f} {y_cs1:.2f} l {p_ch_sh2[0, 0]:.2f} {y_cs1:.2f} l S")
|
||||||
|
|
||||||
stream_cmds.append(
|
stream_cmds.append(
|
||||||
f"{x_cm + arrow_len_pt * 0.7071:.2f} {y_cm + arrow_len_pt * 0.7071:.2f} m "
|
f"{x_cm + arrow_len_pt * 0.7071:.2f} {y_cm + arrow_len_pt * 0.7071:.2f} m "
|
||||||
|
|
@ -503,55 +599,87 @@ def render_sa2_measures_pdf(
|
||||||
f"{x_cm + arrow_len_pt * 0.3535:.2f} {y_cm + arrow_len_pt * 0.95:.2f} l S"
|
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:.1f}mm ({angle_deg:.0f}°)"
|
||||||
c_str = f"C{c_val_rounded:g}mm"
|
|
||||||
stream_cmds.append("BT")
|
stream_cmds.append("BT")
|
||||||
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
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"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(f"({escape_pdf_text(c_str)}) Tj")
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
# 4. Hole Center Pitch Distances (Teal Green)
|
# 4. Orthogonal Hole Center Pitch & Datum Offset Baselines (Teal Green)
|
||||||
if draw_sa2_holes and len(holes) >= 2:
|
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
|
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")
|
||||||
|
|
||||||
sorted_holes = sorted(holes, key=lambda h: (h[0], h[1]))
|
distinct_centers = [grp[0][:2] for grp in concentric_groups]
|
||||||
for i in range(len(sorted_holes) - 1):
|
distinct_centers.sort(key=lambda c: (c[0], c[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")
|
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)
|
||||||
|
|
||||||
pitch_str = f"PITCH: {dist_mm:g}mm"
|
# 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]]))
|
||||||
|
|
||||||
|
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"PITCH X: {dx_mm:.1f}mm"
|
||||||
stream_cmds.append("BT")
|
stream_cmds.append("BT")
|
||||||
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.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"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(f"({escape_pdf_text(pitch_str)}) Tj")
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
# 5. Interior Step / Notch Feature Segment Lengths (Deep Purple)
|
# Vertical Pitch Axis (if dy_mm > 0.5)
|
||||||
if draw_sa2_step_features and step_features:
|
if dy_mm > 0.5:
|
||||||
purple_r, purple_g, purple_b = 0.48, 0.23, 0.92
|
x_pitch_mm = max(cx1, cx2) + sizing.offset_mm * 0.5
|
||||||
stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} RG")
|
p_v1 = to_pdf_pt(np.array([[x_pitch_mm, cy1]]))
|
||||||
stream_cmds.append(f"{purple_r:.3f} {purple_g:.3f} {purple_b:.3f} rg")
|
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]]))
|
||||||
|
|
||||||
for x1_m, y1_m, x2_m, y2_m, step_len in step_features[:4]:
|
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")
|
||||||
p_s1 = to_pdf_pt(np.array([[x1_m, y1_m]]))
|
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")
|
||||||
p_s2 = to_pdf_pt(np.array([[x2_m, y2_m]]))
|
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")
|
||||||
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")
|
pitch_str = f"PITCH Y: {dy_mm:.1f}mm"
|
||||||
step_str = f"{step_len:g}mm"
|
|
||||||
stream_cmds.append("BT")
|
stream_cmds.append("BT")
|
||||||
stream_cmds.append(f"/F1 {font_size_pt * 0.8:.1f} Tf")
|
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"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(step_str)}) Tj")
|
stream_cmds.append(f"({escape_pdf_text(pitch_str)}) Tj")
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
|
# 5. Flange Outer Geometry & Z-Depth Thickness Callouts
|
||||||
|
if mesh_verts_3d is not None and total_z_span > 0:
|
||||||
|
flange_mask = mesh_verts_3d[:, 0] > (min_x + 0.55 * w_obj_mm)
|
||||||
|
if np.any(flange_mask):
|
||||||
|
f_verts = mesh_verts_3d[flange_mask]
|
||||||
|
f_y_min, f_y_max = float(f_verts[:, 1].min()), float(f_verts[:, 1].max())
|
||||||
|
f_z_min, f_z_max = float(f_verts[:, 2].min()), float(f_verts[:, 2].max())
|
||||||
|
|
||||||
|
flange_dia_mm = f_y_max - f_y_min
|
||||||
|
flange_depth_mm = f_z_max - f_z_min
|
||||||
|
|
||||||
|
if flange_dia_mm > 2.0:
|
||||||
|
p_flange_right = to_pdf_pt(np.array([[max_x + sizing.offset_mm * 0.5, (f_y_min + f_y_max) / 2.0]]))
|
||||||
|
xf_label = p_flange_right[0, 0]
|
||||||
|
yf_label = p_flange_right[0, 1]
|
||||||
|
|
||||||
|
flange_str = f"FLANGE Ø{flange_dia_mm:.1f}mm \u2334{flange_depth_mm:.1f}mm"
|
||||||
|
stream_cmds.append("0.100 0.550 0.200 RG")
|
||||||
|
stream_cmds.append("0.100 0.550 0.200 rg")
|
||||||
|
stream_cmds.append("BT")
|
||||||
|
stream_cmds.append(f"/F1 {font_size_pt * 0.85:.1f} Tf")
|
||||||
|
stream_cmds.append(f"1 0 0 1 {xf_label:.2f} {yf_label:.2f} Tm")
|
||||||
|
stream_cmds.append(f"({escape_pdf_text(flange_str)}) Tj")
|
||||||
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
|
|
|
||||||
108
core/pdf_exporter/sizing.py
Normal file
108
core/pdf_exporter/sizing.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
"""
|
||||||
|
Dynamic Dimensioning and Scaled Metrics Engine for PDF Exporter.
|
||||||
|
Centralizes all auto-scaling calculations for grid steps, legend ticks, arrows,
|
||||||
|
line widths, font sizes, and SA2 technical measure offsets.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
MM_TO_PT = 2.834645669291339
|
||||||
|
|
||||||
|
|
||||||
|
def compute_grid_steps(w_geom: float, h_geom: float) -> tuple[float, float]:
|
||||||
|
"""
|
||||||
|
Computes human-readable main_step (mm) and minor_step (mm) for grid overlay.
|
||||||
|
"""
|
||||||
|
L = max(w_geom, h_geom)
|
||||||
|
if L > 1e-6:
|
||||||
|
step_raw = L / 10.0
|
||||||
|
exponent = int(np.round(np.log10(step_raw)))
|
||||||
|
main_step = 10.0 ** exponent
|
||||||
|
else:
|
||||||
|
main_step = 10.0
|
||||||
|
minor_step = main_step / 10.0
|
||||||
|
return main_step, minor_step
|
||||||
|
|
||||||
|
|
||||||
|
def compute_effective_stroke_and_font(
|
||||||
|
line_width_mm: float,
|
||||||
|
font_size_mm: float,
|
||||||
|
minor_step_mm: float
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""
|
||||||
|
Calculates line_width_pt and font_size_pt proportionally based on minor_step_mm.
|
||||||
|
Scales continuously from millimetric components up to large structural models.
|
||||||
|
"""
|
||||||
|
if line_width_mm <= 0:
|
||||||
|
line_width_pt = minor_step_mm * 0.05 * MM_TO_PT
|
||||||
|
else:
|
||||||
|
line_width_pt = line_width_mm * MM_TO_PT
|
||||||
|
|
||||||
|
if font_size_mm <= 0:
|
||||||
|
font_size_pt = minor_step_mm * MM_TO_PT
|
||||||
|
else:
|
||||||
|
font_size_pt = font_size_mm * MM_TO_PT
|
||||||
|
|
||||||
|
return line_width_pt, font_size_pt
|
||||||
|
|
||||||
|
|
||||||
|
class LegendSizing:
|
||||||
|
"""
|
||||||
|
Auto-scaling calculations for the Size Scale Legend (bottom-left grid scale bar).
|
||||||
|
"""
|
||||||
|
def __init__(self, main_step_mm: float, minor_step_mm: float, line_width_pt: float, font_size_pt: float):
|
||||||
|
self.main_step = main_step_mm
|
||||||
|
self.minor_step = minor_step_mm
|
||||||
|
self.line_width_pt = line_width_pt
|
||||||
|
self.font_size_pt = font_size_pt
|
||||||
|
|
||||||
|
# Tick heights in mm proportional to main grid step
|
||||||
|
self.major_tick_mm = main_step_mm * 0.30
|
||||||
|
self.minor_tick_mm = main_step_mm * 0.15
|
||||||
|
|
||||||
|
# Y-axis height in mm (extends comfortably past major tick height)
|
||||||
|
self.y_axis_height_mm = self.major_tick_mm * 1.5
|
||||||
|
|
||||||
|
# X-axis extension past last main step tick in mm
|
||||||
|
self.x_axis_extension_mm = minor_step_mm * 2.0
|
||||||
|
|
||||||
|
# Legend Arrowhead dimensions in PT (proportional to line width)
|
||||||
|
self.arrow_len_pt = max(0.5, line_width_pt * 4.0)
|
||||||
|
self.arrow_half_w_pt = self.arrow_len_pt * 0.35
|
||||||
|
|
||||||
|
|
||||||
|
class SA2Sizing:
|
||||||
|
"""
|
||||||
|
Auto-scaling calculations for SA2 technical measurement lines and feature callouts.
|
||||||
|
"""
|
||||||
|
def __init__(self, w_obj_mm: float, h_obj_mm: float, line_width_pt: float, font_size_pt: float):
|
||||||
|
self.w_obj = w_obj_mm
|
||||||
|
self.h_obj = h_obj_mm
|
||||||
|
self.line_width_pt = line_width_pt
|
||||||
|
self.font_size_pt = font_size_pt
|
||||||
|
|
||||||
|
self.obj_ref_size = min(w_obj_mm, h_obj_mm) if min(w_obj_mm, h_obj_mm) > 0 else max(w_obj_mm, h_obj_mm)
|
||||||
|
|
||||||
|
# Dimension line offset & extension line scaling (mm) proportional to object size
|
||||||
|
self.offset_mm = self.obj_ref_size * 0.10
|
||||||
|
self.ext_overhang_mm = self.offset_mm * 0.20
|
||||||
|
self.ext_gap_mm = self.offset_mm * 0.10
|
||||||
|
|
||||||
|
# Measure arrow dimensions (PT)
|
||||||
|
self.arrow_len_pt = max(0.5, line_width_pt * 4.0)
|
||||||
|
self.arrow_half_w_pt = self.arrow_len_pt * 0.35
|
||||||
|
|
||||||
|
def get_hole_callout_lengths(self, dia_mm: float) -> tuple[float, float]:
|
||||||
|
slant_mm = dia_mm * 0.30
|
||||||
|
leader_mm = dia_mm * 0.40
|
||||||
|
return slant_mm, leader_mm
|
||||||
|
|
||||||
|
def get_arc_callout_lengths(self, r_mm: float) -> tuple[float, float]:
|
||||||
|
slant_mm = r_mm * 0.30
|
||||||
|
leader_mm = r_mm * 0.40
|
||||||
|
return slant_mm, leader_mm
|
||||||
|
|
||||||
|
def get_chamfer_callout_lengths(self, c_val_mm: float) -> tuple[float, float]:
|
||||||
|
slant_mm = c_val_mm * 1.0
|
||||||
|
leader_mm = c_val_mm * 1.5
|
||||||
|
return slant_mm, leader_mm
|
||||||
|
|
@ -22,8 +22,12 @@ from .geometry import (
|
||||||
deduplicate_2d_edges,
|
deduplicate_2d_edges,
|
||||||
)
|
)
|
||||||
from .sa2_measures import render_sa2_measures_pdf
|
from .sa2_measures import render_sa2_measures_pdf
|
||||||
|
from .sizing import (
|
||||||
MM_TO_PT = 2.834645669291339
|
compute_grid_steps,
|
||||||
|
compute_effective_stroke_and_font,
|
||||||
|
LegendSizing,
|
||||||
|
MM_TO_PT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def escape_pdf_text(text: str) -> str:
|
def escape_pdf_text(text: str) -> str:
|
||||||
|
|
@ -108,7 +112,8 @@ def export_mesh_to_pdf(
|
||||||
if draw_wireframe and len(edges_2d) > 0:
|
if draw_wireframe and len(edges_2d) > 0:
|
||||||
edge_list.append(edges_2d)
|
edge_list.append(edges_2d)
|
||||||
|
|
||||||
if draw_boundary and raw_geometry is not None:
|
# extract_crease_edges (lineart) already extracts boundary edges, so skip extract_boundary_edges when draw_crease is active
|
||||||
|
if draw_boundary and not draw_crease and raw_geometry is not None:
|
||||||
b_edges = extract_boundary_edges(raw_geometry)
|
b_edges = extract_boundary_edges(raw_geometry)
|
||||||
if len(b_edges) > 0:
|
if len(b_edges) > 0:
|
||||||
edge_list.append(b_edges)
|
edge_list.append(b_edges)
|
||||||
|
|
@ -131,16 +136,11 @@ def export_mesh_to_pdf(
|
||||||
else:
|
else:
|
||||||
edges_to_draw = np.empty((0, 2, 2), dtype=np.float64)
|
edges_to_draw = np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
|
||||||
if len(edges_to_draw) > 0:
|
target_edges = edges_to_draw if len(edges_to_draw) > 0 else edges_2d
|
||||||
min_x = float(edges_to_draw[:, :, 0].min())
|
if len(target_edges) > 0:
|
||||||
max_x = float(edges_to_draw[:, :, 0].max())
|
pts_2d = target_edges.reshape(-1, 2)
|
||||||
min_y = float(edges_to_draw[:, :, 1].min())
|
min_x, max_x = float(pts_2d[:, 0].min()), float(pts_2d[:, 0].max())
|
||||||
max_y = float(edges_to_draw[:, :, 1].max())
|
min_y, max_y = float(pts_2d[:, 1].min()), float(pts_2d[:, 1].max())
|
||||||
elif len(edges_2d) > 0:
|
|
||||||
min_x = float(edges_2d[:, :, 0].min())
|
|
||||||
max_x = float(edges_2d[:, :, 0].max())
|
|
||||||
min_y = float(edges_2d[:, :, 1].min())
|
|
||||||
max_y = float(edges_2d[:, :, 1].max())
|
|
||||||
else:
|
else:
|
||||||
min_x = max_x = min_y = max_y = 0.0
|
min_x = max_x = min_y = max_y = 0.0
|
||||||
|
|
||||||
|
|
@ -148,16 +148,7 @@ def export_mesh_to_pdf(
|
||||||
h_geom = max_y - min_y
|
h_geom = max_y - min_y
|
||||||
center_x = (min_x + max_x) / 2.0
|
center_x = (min_x + max_x) / 2.0
|
||||||
center_y = (min_y + max_y) / 2.0
|
center_y = (min_y + max_y) / 2.0
|
||||||
|
main_step, minor_step = compute_grid_steps(w_geom, h_geom)
|
||||||
L = max(w_geom, h_geom)
|
|
||||||
if L > 1e-6:
|
|
||||||
step_raw = L / 10.0
|
|
||||||
exponent = int(np.round(np.log10(step_raw)))
|
|
||||||
main_step = 10.0 ** exponent
|
|
||||||
else:
|
|
||||||
main_step = 10.0
|
|
||||||
|
|
||||||
minor_step = main_step / 10.0
|
|
||||||
|
|
||||||
if len(edges_to_draw) > 0 or len(edges_2d) > 0:
|
if len(edges_to_draw) > 0 or len(edges_2d) > 0:
|
||||||
pad_x = minor_step
|
pad_x = minor_step
|
||||||
|
|
@ -207,17 +198,9 @@ 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
|
||||||
|
|
||||||
# Automatic sizing based on minor grid
|
line_width_pt, font_size_pt = compute_effective_stroke_and_font(
|
||||||
if line_width_mm <= 0:
|
line_width_mm, font_size_mm, minor_step
|
||||||
line_width_pt = minor_step * 0.05 * MM_TO_PT
|
)
|
||||||
else:
|
|
||||||
line_width_pt = line_width_mm * MM_TO_PT
|
|
||||||
|
|
||||||
# Convert font_size_mm to pt and apply multiplier
|
|
||||||
if font_size_mm <= 0:
|
|
||||||
font_size_pt = minor_step * MM_TO_PT
|
|
||||||
else:
|
|
||||||
font_size_pt = font_size_mm * 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
|
||||||
|
|
@ -347,13 +330,20 @@ def export_mesh_to_pdf(
|
||||||
main_step=main_step,
|
main_step=main_step,
|
||||||
to_pdf_pt=to_pdf_pt,
|
to_pdf_pt=to_pdf_pt,
|
||||||
font_size_pt=font_size_pt,
|
font_size_pt=font_size_pt,
|
||||||
line_width_pt=line_width_pt
|
line_width_pt=line_width_pt,
|
||||||
|
draw_wireframe=draw_wireframe,
|
||||||
|
draw_boundary=draw_boundary,
|
||||||
|
draw_contour=draw_contour,
|
||||||
|
draw_crease=draw_crease,
|
||||||
|
crease_angle=crease_angle,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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")
|
||||||
stream_cmds.append("0.000 0.000 0.000 RG")
|
stream_cmds.append("0.000 0.000 0.000 RG")
|
||||||
stream_cmds.append("0.000 0.000 0.000 rg")
|
stream_cmds.append("0.000 0.000 0.000 rg")
|
||||||
|
|
||||||
|
leg_sizing = LegendSizing(main_step, minor_step, line_width_pt, font_size_pt)
|
||||||
x_grid_start_mm = grid_min_x
|
x_grid_start_mm = grid_min_x
|
||||||
x_grid_end_mm = min(grid_max_x, grid_min_x + 10.0 * main_step)
|
x_grid_end_mm = min(grid_max_x, grid_min_x + 10.0 * main_step)
|
||||||
num_main_steps = int(np.round((x_grid_end_mm - x_grid_start_mm) / main_step))
|
num_main_steps = int(np.round((x_grid_end_mm - x_grid_start_mm) / main_step))
|
||||||
|
|
@ -365,22 +355,20 @@ def export_mesh_to_pdf(
|
||||||
x_end_pt = p_end_pt[0, 0]
|
x_end_pt = p_end_pt[0, 0]
|
||||||
y_bar_pt = p_start_pt[0, 1]
|
y_bar_pt = p_start_pt[0, 1]
|
||||||
|
|
||||||
# Vertical Y-axis extension: 5 minor grid lines up from origin
|
# Vertical Y-axis extension dynamically sized past tick height
|
||||||
y_grid_top_mm = grid_min_y + 5.0 * minor_step
|
y_grid_top_mm = grid_min_y + leg_sizing.y_axis_height_mm
|
||||||
p_top_pt = to_pdf_pt(np.array([[grid_min_x, y_grid_top_mm]]))
|
p_top_pt = to_pdf_pt(np.array([[grid_min_x, y_grid_top_mm]]))
|
||||||
y_top_pt = p_top_pt[0, 1]
|
y_top_pt = p_top_pt[0, 1]
|
||||||
|
|
||||||
# Smart arrowhead scaling based on line_width_pt and font size
|
arrow_len_pt = leg_sizing.arrow_len_pt
|
||||||
font_size_pt = font_size_pt if font_size_pt is not None and font_size_pt > 0 else max(8.0, line_width_pt * 4.0)
|
arrow_half_w_pt = leg_sizing.arrow_half_w_pt
|
||||||
arrow_len_pt = max(3.5 * MM_TO_PT, line_width_pt * 4.5)
|
|
||||||
arrow_half_w_pt = arrow_len_pt * 0.40
|
|
||||||
|
|
||||||
# Exact same stroke thickness as object wireframe lines
|
# Exact same stroke thickness as object wireframe lines
|
||||||
stream_cmds.append(f"{line_width_pt:.2f} w")
|
stream_cmds.append(f"{line_width_pt:.2f} w")
|
||||||
stream_cmds.append("1 j 1 J")
|
stream_cmds.append("1 j 1 J")
|
||||||
|
|
||||||
# 6a. Main horizontal X-axis line (spans 10 main grid steps, extends 5 minor grid lines past last tick + right arrow + X label)
|
# 6a. Main horizontal X-axis line (spans main grid steps, extends past last tick + right arrow + X label)
|
||||||
x_arrow_base_mm = x_grid_end_mm + 5.0 * minor_step
|
x_arrow_base_mm = x_grid_end_mm + leg_sizing.x_axis_extension_mm
|
||||||
p_arrow_base_pt = to_pdf_pt(np.array([[x_arrow_base_mm, grid_min_y]]))
|
p_arrow_base_pt = to_pdf_pt(np.array([[x_arrow_base_mm, grid_min_y]]))
|
||||||
x_arrow_base_pt = p_arrow_base_pt[0, 0]
|
x_arrow_base_pt = p_arrow_base_pt[0, 0]
|
||||||
x_arrow_tip_pt = x_arrow_base_pt + arrow_len_pt
|
x_arrow_tip_pt = x_arrow_base_pt + arrow_len_pt
|
||||||
|
|
@ -388,30 +376,31 @@ def export_mesh_to_pdf(
|
||||||
# Horizontal main scale bar line
|
# Horizontal main scale bar line
|
||||||
stream_cmds.append(f"{x_start_pt:.2f} {y_bar_pt:.2f} m {x_arrow_tip_pt:.2f} {y_bar_pt:.2f} l S")
|
stream_cmds.append(f"{x_start_pt:.2f} {y_bar_pt:.2f} m {x_arrow_tip_pt:.2f} {y_bar_pt:.2f} l S")
|
||||||
|
|
||||||
# Pointy arrowhead (+X) extending past 5th minor grid line beyond x_end_pt
|
# Pointy arrowhead (+X) extending past last tick
|
||||||
stream_cmds.append(
|
stream_cmds.append(
|
||||||
f"{x_arrow_base_pt:.2f} {y_bar_pt + arrow_half_w_pt:.2f} m "
|
f"{x_arrow_base_pt:.2f} {y_bar_pt + arrow_half_w_pt:.2f} m "
|
||||||
f"{x_arrow_tip_pt:.2f} {y_bar_pt:.2f} l "
|
f"{x_arrow_tip_pt:.2f} {y_bar_pt:.2f} l "
|
||||||
f"{x_arrow_base_pt:.2f} {y_bar_pt - arrow_half_w_pt:.2f} l S"
|
f"{x_arrow_base_pt:.2f} {y_bar_pt - arrow_half_w_pt:.2f} l S"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ticks directly overlapping vertical main grid lines on X-axis (full 10 main grid steps including k=0)
|
# Ticks directly overlapping vertical main grid lines on X-axis
|
||||||
tick_cmds = []
|
tick_cmds = []
|
||||||
for k in range(num_main_steps + 1):
|
for k in range(num_main_steps + 1):
|
||||||
xk_mm = x_grid_start_mm + k * main_step
|
xk_mm = x_grid_start_mm + k * main_step
|
||||||
pk_pt = to_pdf_pt(np.array([[xk_mm, grid_min_y]]))
|
pk_pt = to_pdf_pt(np.array([[xk_mm, grid_min_y]]))
|
||||||
xk_pt = pk_pt[0, 0]
|
xk_pt = pk_pt[0, 0]
|
||||||
tick_h_pt = (5.0 if k in (0, 5, num_main_steps) else 3.0) * MM_TO_PT
|
tick_h_mm = leg_sizing.major_tick_mm if k in (0, 5, num_main_steps) else leg_sizing.minor_tick_mm
|
||||||
|
tick_h_pt = tick_h_mm * MM_TO_PT
|
||||||
tick_cmds.append(f"{xk_pt:.2f} {y_bar_pt:.2f} m {xk_pt:.2f} {y_bar_pt + tick_h_pt:.2f} l")
|
tick_cmds.append(f"{xk_pt:.2f} {y_bar_pt:.2f} m {xk_pt:.2f} {y_bar_pt + tick_h_pt:.2f} l")
|
||||||
stream_cmds.extend(tick_cmds)
|
stream_cmds.extend(tick_cmds)
|
||||||
stream_cmds.append("S")
|
stream_cmds.append("S")
|
||||||
|
|
||||||
# 6b. Main vertical Y-axis line (5 minor grid lines up with NO ticks) + top arrow (+Y)
|
# 6b. Main vertical Y-axis line + top arrow (+Y)
|
||||||
y_arrow_base_pt = y_top_pt
|
y_arrow_base_pt = y_top_pt
|
||||||
y_arrow_tip_pt = y_arrow_base_pt + arrow_len_pt
|
y_arrow_tip_pt = y_arrow_base_pt + arrow_len_pt
|
||||||
stream_cmds.append(f"{x_start_pt:.2f} {y_bar_pt:.2f} m {x_start_pt:.2f} {y_arrow_tip_pt:.2f} l S")
|
stream_cmds.append(f"{x_start_pt:.2f} {y_bar_pt:.2f} m {x_start_pt:.2f} {y_arrow_tip_pt:.2f} l S")
|
||||||
|
|
||||||
# Pointy arrowhead (+Y) extending past 5th minor grid line (y_top_pt)
|
# Pointy arrowhead (+Y) extending past y_top_pt
|
||||||
stream_cmds.append(
|
stream_cmds.append(
|
||||||
f"{x_start_pt - arrow_half_w_pt:.2f} {y_arrow_base_pt:.2f} m "
|
f"{x_start_pt - arrow_half_w_pt:.2f} {y_arrow_base_pt:.2f} m "
|
||||||
f"{x_start_pt:.2f} {y_arrow_tip_pt:.2f} l "
|
f"{x_start_pt:.2f} {y_arrow_tip_pt:.2f} l "
|
||||||
|
|
@ -427,7 +416,8 @@ def export_mesh_to_pdf(
|
||||||
xk_mm = x_grid_start_mm + k * main_step
|
xk_mm = x_grid_start_mm + k * main_step
|
||||||
pk_pt = to_pdf_pt(np.array([[xk_mm, grid_min_y]]))
|
pk_pt = to_pdf_pt(np.array([[xk_mm, grid_min_y]]))
|
||||||
xk_pt = pk_pt[0, 0]
|
xk_pt = pk_pt[0, 0]
|
||||||
tick_h_pt = (5.0 if k in (0, 5, num_main_steps) else 3.0) * MM_TO_PT
|
tick_h_mm = leg_sizing.major_tick_mm if k in (0, 5, num_main_steps) else leg_sizing.minor_tick_mm
|
||||||
|
tick_h_pt = tick_h_mm * MM_TO_PT
|
||||||
val_mm = k * main_step
|
val_mm = k * main_step
|
||||||
|
|
||||||
if main_step >= 1000.0:
|
if main_step >= 1000.0:
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,16 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `geometry.py`:
|
- `geometry.py`:
|
||||||
- Geometry utility functions.
|
- Geometry utility functions.
|
||||||
|
|
||||||
|
## `/core/pdf_exporter` (PDF Export & Technical Marking Engine)
|
||||||
|
|
||||||
|
- `sa2_measures.py`:
|
||||||
|
- `render_sa2_measures_pdf()`: ISO 129-1 technical marking engine.
|
||||||
|
- `extract_slicing_object_features()`: Feature detection for circular bores (Ø), arcs (R), chamfers (C), and steps.
|
||||||
|
- `depth_rasterizer.py`:
|
||||||
|
- `save_sa2_debug_data()`: Exports input 2D edge vector coordinates and cProfile performance data when debug is enabled.
|
||||||
|
- `writer.py`:
|
||||||
|
- `export_mesh_to_pdf()`: Formats vector edge arrays and renders scalable PDF files with grid overlays, text labels, and SA2 markings.
|
||||||
|
|
||||||
## `/infra` (Blender Integration)
|
## `/infra` (Blender Integration)
|
||||||
|
|
||||||
- `blender_mesh_capture.py`:
|
- `blender_mesh_capture.py`:
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ from NodeSlicer.core.pdf_exporter import (
|
||||||
extract_unique_top_view_edges,
|
extract_unique_top_view_edges,
|
||||||
extract_lineart_edges,
|
extract_lineart_edges,
|
||||||
extract_outer_contour_edges,
|
extract_outer_contour_edges,
|
||||||
|
extract_slicing_object_features,
|
||||||
export_wireframe_to_pdf,
|
export_wireframe_to_pdf,
|
||||||
export_mesh_to_pdf,
|
export_mesh_to_pdf,
|
||||||
MM_TO_PT
|
MM_TO_PT
|
||||||
|
|
@ -153,6 +154,21 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
# Contour should exclude the internal diagonal edge and return only 4 outer boundary edges
|
# Contour should exclude the internal diagonal edge and return only 4 outer boundary edges
|
||||||
self.assertEqual(len(contour), 4)
|
self.assertEqual(len(contour), 4)
|
||||||
|
|
||||||
|
def test_extract_outer_contour_edges_perpendicular_floating_point_noise(self):
|
||||||
|
so = SlicingObject()
|
||||||
|
# Top face: normal_z = +1.0
|
||||||
|
tri_top = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0], [0.0, 10.0, 10.0], [0.0, 0.0, 1.0]], dtype=np.float64)
|
||||||
|
# Perpendicular side walls with floating point noise in normal_z (e.g. 1.5e-5, -2.0e-5)
|
||||||
|
tri_side1 = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0], [10.0, 0.0, 0.0], [0.0, -1.0, 1.5e-5]], dtype=np.float64)
|
||||||
|
tri_side2 = np.array([[10.0, 0.0, 10.0], [0.0, 10.0, 10.0], [0.0, 10.0, 0.0], [0.707, 0.707, -2.0e-5]], dtype=np.float64)
|
||||||
|
tri_side3 = np.array([[0.0, 10.0, 10.0], [0.0, 0.0, 10.0], [0.0, 0.0, 0.0], [-1.0, 0.0, 4.0e-5]], dtype=np.float64)
|
||||||
|
|
||||||
|
so.mesh = np.array([tri_top, tri_side1, tri_side2, tri_side3])
|
||||||
|
|
||||||
|
contour = extract_outer_contour_edges(so)
|
||||||
|
# All 3 outer boundary edges of the top triangle meeting perpendicular side walls must be detected
|
||||||
|
self.assertEqual(len(contour), 3)
|
||||||
|
|
||||||
def test_export_pdf_true_1_to_1_scale(self):
|
def test_export_pdf_true_1_to_1_scale(self):
|
||||||
edges_2d = np.array([
|
edges_2d = np.array([
|
||||||
[[0.0, 0.0], [100.0, 0.0]],
|
[[0.0, 0.0], [100.0, 0.0]],
|
||||||
|
|
@ -736,6 +752,57 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
self.assertIn("LeagueMono-Regular", content)
|
self.assertIn("LeagueMono-Regular", content)
|
||||||
self.assertIn("/Subtype /OpenType", content)
|
self.assertIn("/Subtype /OpenType", content)
|
||||||
|
|
||||||
|
def test_sa2_source_geometry_matches_visible_selected(self):
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "VisibilityTestPart"
|
||||||
|
tris = [
|
||||||
|
[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0]],
|
||||||
|
[[10.0, 0.0, 0.0], [10.0, 10.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0]]
|
||||||
|
]
|
||||||
|
so.mesh = np.array(tris, dtype=np.float64)
|
||||||
|
|
||||||
|
# 1. When no geometry is selected as visible (all False), extract_slicing_object_features returns empty
|
||||||
|
h, a, c, s = extract_slicing_object_features(
|
||||||
|
so, draw_wireframe=False, draw_boundary=False, draw_contour=False, draw_crease=False
|
||||||
|
)
|
||||||
|
self.assertEqual((h, a, c, s), ([], [], [], []))
|
||||||
|
|
||||||
|
# 2. When draw_boundary=True, features are extracted from boundary edges
|
||||||
|
h_b, a_b, c_b, s_b = extract_slicing_object_features(
|
||||||
|
so, draw_wireframe=False, draw_boundary=True, draw_contour=False, draw_crease=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. When draw_wireframe=True, features are extracted from top-view wireframe edges
|
||||||
|
h_w, a_w, c_w, s_w = extract_slicing_object_features(
|
||||||
|
so, draw_wireframe=True, draw_boundary=False, draw_contour=False, draw_crease=False
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(h_w)
|
||||||
|
def test_export_pdf_debug_sa2_output_files(self):
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "DebugPart"
|
||||||
|
so.mesh = np.array([
|
||||||
|
[[0.0, 0.0, 0.0], [50.0, 0.0, 0.0], [0.0, 50.0, 0.0], [0.0, 0.0, 1.0]],
|
||||||
|
[[50.0, 0.0, 0.0], [50.0, 50.0, 0.0], [0.0, 50.0, 0.0], [0.0, 0.0, 1.0]]
|
||||||
|
], dtype=np.float64)
|
||||||
|
|
||||||
|
edges_2d = extract_unique_top_view_edges(so)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
pdf_path = os.path.join(tmpdir, "debug_test.pdf")
|
||||||
|
export_mesh_to_pdf(
|
||||||
|
edges_2d,
|
||||||
|
pdf_path,
|
||||||
|
debug_sa2=True,
|
||||||
|
raw_geometry=so
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(os.path.exists(pdf_path))
|
||||||
|
txt_debug = os.path.join(tmpdir, "debug_test_sa2_debug_edges.txt")
|
||||||
|
prof_debug = os.path.join(tmpdir, "debug_test_sa2_debug_cprofile.txt")
|
||||||
|
|
||||||
|
self.assertTrue(os.path.exists(txt_debug))
|
||||||
|
self.assertTrue(os.path.exists(prof_debug))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
49
tests/test_sizing.py
Normal file
49
tests/test_sizing.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from NodeSlicer.core.pdf_exporter.sizing import (
|
||||||
|
compute_grid_steps,
|
||||||
|
compute_effective_stroke_and_font,
|
||||||
|
LegendSizing,
|
||||||
|
SA2Sizing,
|
||||||
|
MM_TO_PT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSizingLogic(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_compute_grid_steps_small_object(self):
|
||||||
|
# 20mm x 15mm object
|
||||||
|
main_step, minor_step = compute_grid_steps(20.0, 15.0)
|
||||||
|
self.assertEqual(main_step, 1.0)
|
||||||
|
self.assertEqual(minor_step, 0.1)
|
||||||
|
|
||||||
|
def test_compute_grid_steps_medium_object(self):
|
||||||
|
# 120mm x 80mm object
|
||||||
|
main_step, minor_step = compute_grid_steps(120.0, 80.0)
|
||||||
|
self.assertEqual(main_step, 10.0)
|
||||||
|
self.assertEqual(minor_step, 1.0)
|
||||||
|
|
||||||
|
def test_compute_effective_stroke_and_font_defaults(self):
|
||||||
|
# 2mm object with minor_step = 0.01mm
|
||||||
|
line_w_pt, font_size_pt = compute_effective_stroke_and_font(0.0, 0.0, 0.01)
|
||||||
|
# Should scale line width down proportionally for millimetric scale
|
||||||
|
self.assertAlmostEqual(line_w_pt, 0.01 * 0.05 * MM_TO_PT, places=6)
|
||||||
|
self.assertAlmostEqual(font_size_pt, 0.01 * MM_TO_PT, places=6)
|
||||||
|
|
||||||
|
def test_legend_sizing_scaling(self):
|
||||||
|
leg = LegendSizing(10.0, 1.0, 1.0, 8.0)
|
||||||
|
self.assertGreater(leg.y_axis_height_mm, leg.major_tick_mm)
|
||||||
|
self.assertGreater(leg.arrow_len_pt, 0)
|
||||||
|
self.assertGreater(leg.arrow_half_w_pt, 0)
|
||||||
|
|
||||||
|
def test_sa2_sizing_millimetric_scale(self):
|
||||||
|
# 2mm x 1.5mm millimetric scale object
|
||||||
|
sa2 = SA2Sizing(2.0, 1.5, 0.0014, 0.028)
|
||||||
|
# For a 1.5mm/2mm millimetric object, offset should be fine (0.15mm)
|
||||||
|
self.assertAlmostEqual(sa2.offset_mm, 0.15, places=3)
|
||||||
|
self.assertAlmostEqual(sa2.ext_overhang_mm, 0.03, places=3)
|
||||||
|
|
||||||
|
slant, leader = sa2.get_hole_callout_lengths(0.5)
|
||||||
|
self.assertAlmostEqual(slant, 0.15, places=3)
|
||||||
|
self.assertAlmostEqual(leader, 0.20, places=3)
|
||||||
Loading…
Add table
Reference in a new issue