sa2 redo
This commit is contained in:
parent
7b8c54c2a5
commit
5b38682b93
5 changed files with 38 additions and 382 deletions
|
|
@ -363,10 +363,7 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
draw_crease: NfBoolProperty(name="Crease Angle", default=False) # pyright: ignore[reportInvalidTypeForm]
|
draw_crease: NfBoolProperty(name="Crease Angle", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||||
crease_angle: NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0) # pyright: ignore[reportInvalidTypeForm]
|
crease_angle: NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0) # pyright: ignore[reportInvalidTypeForm]
|
||||||
draw_bbox: NfBoolProperty(name="Bounding Box", default=False) # pyright: ignore[reportInvalidTypeForm]
|
draw_bbox: NfBoolProperty(name="Bounding Box", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||||
draw_sa2_holes: NfBoolProperty(name="Holes", default=False) # pyright: ignore[reportInvalidTypeForm]
|
draw_sa2_holes: NfBoolProperty(name="Holes (Ø)", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||||
draw_sa2_arcs: NfBoolProperty(name="Arcs", default=False) # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
draw_sa2_chamfers: NfBoolProperty(name="Chamfers", default=False) # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
draw_sa2_step_features: NfBoolProperty(name="Step Features", default=False) # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
show_lineart: NfBoolProperty(name="Lineart Visibility", default=True) # pyright: ignore[reportInvalidTypeForm]
|
show_lineart: NfBoolProperty(name="Lineart Visibility", default=True) # pyright: ignore[reportInvalidTypeForm]
|
||||||
show_sa2: NfBoolProperty(name="SA2 Settings", default=True) # pyright: ignore[reportInvalidTypeForm]
|
show_sa2: NfBoolProperty(name="SA2 Settings", default=True) # pyright: ignore[reportInvalidTypeForm]
|
||||||
show_name: NfBoolProperty(name="Show Name", default=False) # pyright: ignore[reportInvalidTypeForm]
|
show_name: NfBoolProperty(name="Show Name", default=False) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
@ -428,9 +425,6 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
if self.show_sa2:
|
if self.show_sa2:
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.prop(self, "draw_sa2_holes")
|
box.prop(self, "draw_sa2_holes")
|
||||||
box.prop(self, "draw_sa2_arcs")
|
|
||||||
box.prop(self, "draw_sa2_chamfers")
|
|
||||||
box.prop(self, "draw_sa2_step_features")
|
|
||||||
|
|
||||||
# Display settings
|
# Display settings
|
||||||
layout.prop(self, "draw_bbox")
|
layout.prop(self, "draw_bbox")
|
||||||
|
|
@ -500,9 +494,6 @@ class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
draw_crease=self.draw_crease,
|
draw_crease=self.draw_crease,
|
||||||
draw_bbox=self.draw_bbox,
|
draw_bbox=self.draw_bbox,
|
||||||
draw_sa2_holes=self.draw_sa2_holes,
|
draw_sa2_holes=self.draw_sa2_holes,
|
||||||
draw_sa2_arcs=self.draw_sa2_arcs,
|
|
||||||
draw_sa2_chamfers=self.draw_sa2_chamfers,
|
|
||||||
draw_sa2_step_features=self.draw_sa2_step_features,
|
|
||||||
show_name=self.show_name,
|
show_name=self.show_name,
|
||||||
show_bbox_size=self.show_bbox_size,
|
show_bbox_size=self.show_bbox_size,
|
||||||
crease_angle=self.crease_angle,
|
crease_angle=self.crease_angle,
|
||||||
|
|
|
||||||
|
|
@ -104,19 +104,10 @@ def extract_slicing_object_features(
|
||||||
draw_crease: bool = False,
|
draw_crease: bool = False,
|
||||||
crease_angle: float = 30.0,
|
crease_angle: float = 30.0,
|
||||||
bounds: tuple[float, float, float, float] | None = None,
|
bounds: tuple[float, float, float, float] | None = None,
|
||||||
) -> tuple[
|
) -> list[tuple[float, float, float]]:
|
||||||
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:
|
Extracts circular bore / hole features (center_x, center_y, diameter_mm) from a SlicingObject
|
||||||
Source geometry is derived from the visible edge layers (wireframe, boundary, contour, crease).
|
per ISO 129-1 standards based on visible edge layers.
|
||||||
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)
|
|
||||||
"""
|
"""
|
||||||
edge_list: list[NDArray[np.float64]] = []
|
edge_list: list[NDArray[np.float64]] = []
|
||||||
|
|
||||||
|
|
@ -125,8 +116,6 @@ def extract_slicing_object_features(
|
||||||
if len(w_edges) > 0:
|
if len(w_edges) > 0:
|
||||||
edge_list.append(w_edges)
|
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:
|
if draw_boundary and not draw_crease:
|
||||||
b_edges_layer = extract_boundary_edges(so_item)
|
b_edges_layer = extract_boundary_edges(so_item)
|
||||||
if len(b_edges_layer) > 0:
|
if len(b_edges_layer) > 0:
|
||||||
|
|
@ -149,56 +138,16 @@ def extract_slicing_object_features(
|
||||||
b_edges = np.empty((0, 2, 2), dtype=np.float64)
|
b_edges = np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
|
||||||
if len(b_edges) == 0:
|
if len(b_edges) == 0:
|
||||||
return [], [], [], []
|
return []
|
||||||
|
|
||||||
adj: dict[tuple[float, float], list[tuple[float, float]]] = {}
|
from shapely.geometry import LineString
|
||||||
for p0, p1 in b_edges:
|
from shapely.ops import polygonize
|
||||||
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():
|
lines = [LineString([(float(p0[0]), float(p0[1])), (float(p1[0]), float(p1[1]))]) for p0, p1 in b_edges]
|
||||||
nbrs.sort(key=lambda n: (n[0] - pt[0]) ** 2 + (n[1] - pt[1]) ** 2)
|
polygons = list(polygonize(lines))
|
||||||
|
loops: list[NDArray[np.float64]] = [np.array(poly.exterior.coords, dtype=np.float64) for poly in polygons]
|
||||||
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]] = []
|
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 bounds is not None:
|
if bounds is not None:
|
||||||
min_x, max_x, min_y, max_y = bounds
|
min_x, max_x, min_y, max_y = bounds
|
||||||
|
|
@ -207,7 +156,7 @@ def extract_slicing_object_features(
|
||||||
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:
|
else:
|
||||||
return holes, arcs, chamfers, step_features
|
return holes
|
||||||
|
|
||||||
overall_w = max_x - min_x
|
overall_w = max_x - min_x
|
||||||
overall_h = max_y - min_y
|
overall_h = max_y - min_y
|
||||||
|
|
@ -223,97 +172,18 @@ def extract_slicing_object_features(
|
||||||
r_mean_full = float(dists_full.mean())
|
r_mean_full = float(dists_full.mean())
|
||||||
r_std_full = float(dists_full.std())
|
r_std_full = float(dists_full.std())
|
||||||
|
|
||||||
# 1. ISO Full Circular Bore (Ø)
|
# ISO Full Circular Bore (Ø) Detection
|
||||||
if N >= 5 and r_mean_full > 0.5 and (r_std_full / r_mean_full) < 0.20:
|
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:
|
if r_mean_full < min(overall_w, overall_h) * 0.45:
|
||||||
holes.append((cx_full, cy_full, r_mean_full * 2.0))
|
holes.append((cx_full, cy_full, r_mean_full * 2.0))
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Trace ISO Arc Cutouts & Fillets (R)
|
return holes
|
||||||
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(
|
def render_sa2_measures_pdf(
|
||||||
stream_cmds: list[str],
|
stream_cmds: list[str],
|
||||||
raw_geometry: Any,
|
raw_geometry: Any,
|
||||||
draw_sa2_holes: bool = False,
|
draw_sa2_holes: bool = True,
|
||||||
draw_sa2_arcs: bool = False,
|
|
||||||
draw_sa2_chamfers: bool = False,
|
|
||||||
draw_sa2_step_features: bool = False,
|
|
||||||
minor_step_mm: float = 1.0,
|
minor_step_mm: float = 1.0,
|
||||||
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,
|
||||||
|
|
@ -326,13 +196,9 @@ def render_sa2_measures_pdf(
|
||||||
crease_angle: float = 30.0,
|
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 hole measurements (bores, concentric steps, Z-depth, pitch) on PDF stream.
|
||||||
"""
|
"""
|
||||||
if raw_geometry is None:
|
if raw_geometry is None or not draw_sa2_holes:
|
||||||
return
|
|
||||||
|
|
||||||
# Check if any SA2 feature is enabled
|
|
||||||
if not (draw_sa2_holes or draw_sa2_arcs or draw_sa2_chamfers or draw_sa2_step_features):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
sa2_objects = extract_slicing_objects_bounds(raw_geometry)
|
sa2_objects = extract_slicing_objects_bounds(raw_geometry)
|
||||||
|
|
@ -366,6 +232,12 @@ def render_sa2_measures_pdf(
|
||||||
arrow_len_pt = sizing.arrow_len_pt
|
arrow_len_pt = sizing.arrow_len_pt
|
||||||
arrow_half_w_pt = sizing.arrow_half_w_pt
|
arrow_half_w_pt = sizing.arrow_half_w_pt
|
||||||
|
|
||||||
|
mesh_verts_3d = None
|
||||||
|
total_z_span = 0.0
|
||||||
|
if hasattr(so_item, 'mesh') and so_item.mesh.size > 0:
|
||||||
|
mesh_verts_3d = so_item.mesh[:, :3, :3].reshape(-1, 3)
|
||||||
|
total_z_span = float(mesh_verts_3d[:, 2].max() - mesh_verts_3d[:, 2].min())
|
||||||
|
|
||||||
# --- Width Dimension (Horizontal below SlicingObject) ---
|
# --- Width Dimension (Horizontal below SlicingObject) ---
|
||||||
y_dim_mm = min_y - offset_mm
|
y_dim_mm = min_y - offset_mm
|
||||||
|
|
||||||
|
|
@ -440,11 +312,10 @@ def render_sa2_measures_pdf(
|
||||||
y_h_text = (y_h1 + y_h2) / 2.0 - (font_size_pt * 0.35)
|
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"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(f"({escape_pdf_text(h_str)}) Tj")
|
||||||
|
|
||||||
stream_cmds.append("ET")
|
stream_cmds.append("ET")
|
||||||
|
|
||||||
# --- Additional SA2 Feature Measures: Bores (Ø), Arcs (R), Chamfers (C) & Pitch ---
|
# --- SA2 Hole Features Extraction (Ø) ---
|
||||||
holes, arcs, chamfers, step_features = extract_slicing_object_features(
|
holes = extract_slicing_object_features(
|
||||||
so_item,
|
so_item,
|
||||||
draw_wireframe=draw_wireframe,
|
draw_wireframe=draw_wireframe,
|
||||||
draw_boundary=draw_boundary,
|
draw_boundary=draw_boundary,
|
||||||
|
|
@ -452,11 +323,9 @@ def render_sa2_measures_pdf(
|
||||||
draw_crease=draw_crease,
|
draw_crease=draw_crease,
|
||||||
crease_angle=crease_angle,
|
crease_angle=crease_angle,
|
||||||
bounds=(min_x, max_x, min_y, max_y),
|
bounds=(min_x, max_x, min_y, max_y),
|
||||||
debug_sa2=debug_sa2,
|
|
||||||
filepath=filepath,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. Concentric Bores Grouping & ISO 129-1 Callouts with Z-depth Specs (Electric Blue)
|
# 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")
|
||||||
|
|
@ -475,13 +344,6 @@ def render_sa2_measures_pdf(
|
||||||
if not added:
|
if not added:
|
||||||
concentric_groups.append([(cx, cy, dia)])
|
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:
|
for grp in concentric_groups:
|
||||||
grp.sort(key=lambda h: h[2], reverse=True)
|
grp.sort(key=lambda h: h[2], reverse=True)
|
||||||
gcx, gcy, max_dia = grp[0]
|
gcx, gcy, max_dia = grp[0]
|
||||||
|
|
@ -529,84 +391,7 @@ def render_sa2_measures_pdf(
|
||||||
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")
|
||||||
|
|
||||||
# 2. ISO 129-1 Arc Cutouts & Corner Fillets (R - Amber Orange)
|
# Orthogonal Hole Center Pitch & Datum Offset Baselines (Teal Green)
|
||||||
if draw_sa2_arcs and 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]:
|
|
||||||
arc_slant, arc_leader = sizing.get_arc_callout_lengths(r_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_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 + arc_slant + arc_leader, mid_y_mm + arc_slant]]))
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
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)
|
|
||||||
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 & Rib Angles (C / θ - Violet Purple)
|
|
||||||
if draw_sa2_chamfers and 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
|
|
||||||
dx_ch = abs(ch_x2 - ch_x1)
|
|
||||||
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_sh1 = to_pdf_pt(np.array([[sh_x, 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_cs1, y_cs1 = p_ch_sh1[0, 0], p_ch_sh1[0, 1]
|
|
||||||
|
|
||||||
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(
|
|
||||||
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_str = f"C{c_val:.1f}mm ({angle_deg:.0f}°)"
|
|
||||||
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. Orthogonal Hole Center Pitch & Datum Offset Baselines (Teal Green)
|
|
||||||
if draw_sa2_holes and 'concentric_groups' in locals() and len(concentric_groups) >= 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")
|
||||||
|
|
@ -658,28 +443,3 @@ def render_sa2_measures_pdf(
|
||||||
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. 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")
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,6 @@ def export_mesh_to_pdf(
|
||||||
draw_crease: bool = False,
|
draw_crease: bool = False,
|
||||||
draw_bbox: bool = False,
|
draw_bbox: bool = False,
|
||||||
draw_sa2_holes: bool = False,
|
draw_sa2_holes: bool = False,
|
||||||
draw_sa2_arcs: bool = False,
|
|
||||||
draw_sa2_chamfers: bool = False,
|
|
||||||
draw_sa2_step_features: bool = False,
|
|
||||||
show_name: bool = False,
|
show_name: bool = False,
|
||||||
show_bbox_size: bool = False,
|
show_bbox_size: bool = False,
|
||||||
crease_angle: float = 30.0,
|
crease_angle: float = 30.0,
|
||||||
|
|
@ -317,15 +314,12 @@ def export_mesh_to_pdf(
|
||||||
stream_cmds.extend(lines_str[start_idx:start_idx + 5000])
|
stream_cmds.extend(lines_str[start_idx:start_idx + 5000])
|
||||||
stream_cmds.append("S")
|
stream_cmds.append("S")
|
||||||
|
|
||||||
# 5.6. SA2 Automated Technical Measures Block
|
# 5.6. SA2 Automated Technical Measures Block (Holes & Concentric Bores)
|
||||||
if raw_geometry is not None and (draw_sa2_holes or draw_sa2_arcs or draw_sa2_chamfers or draw_sa2_step_features):
|
if raw_geometry is not None and draw_sa2_holes:
|
||||||
render_sa2_measures_pdf(
|
render_sa2_measures_pdf(
|
||||||
stream_cmds,
|
stream_cmds,
|
||||||
raw_geometry,
|
raw_geometry,
|
||||||
draw_sa2_holes=draw_sa2_holes,
|
draw_sa2_holes=draw_sa2_holes,
|
||||||
draw_sa2_arcs=draw_sa2_arcs,
|
|
||||||
draw_sa2_chamfers=draw_sa2_chamfers,
|
|
||||||
draw_sa2_step_features=draw_sa2_step_features,
|
|
||||||
minor_step_mm=minor_step,
|
minor_step_mm=minor_step,
|
||||||
main_step=main_step,
|
main_step=main_step,
|
||||||
to_pdf_pt=to_pdf_pt,
|
to_pdf_pt=to_pdf_pt,
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,6 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `sa2_measures.py`:
|
- `sa2_measures.py`:
|
||||||
- `render_sa2_measures_pdf()`: ISO 129-1 technical marking engine.
|
- `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.
|
- `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`:
|
- `writer.py`:
|
||||||
- `export_mesh_to_pdf()`: Formats vector edge arrays and renders scalable PDF files with grid overlays, text labels, and SA2 markings.
|
- `export_mesh_to_pdf()`: Formats vector edge arrays and renders scalable PDF files with grid overlays, text labels, and SA2 markings.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -586,9 +586,6 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
edges_2d,
|
edges_2d,
|
||||||
pdf_path,
|
pdf_path,
|
||||||
draw_sa2_holes=True,
|
draw_sa2_holes=True,
|
||||||
draw_sa2_arcs=True,
|
|
||||||
draw_sa2_chamfers=True,
|
|
||||||
draw_sa2_step_features=True,
|
|
||||||
raw_geometry=so
|
raw_geometry=so
|
||||||
)
|
)
|
||||||
self.assertTrue(os.path.exists(pdf_path))
|
self.assertTrue(os.path.exists(pdf_path))
|
||||||
|
|
@ -634,73 +631,16 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
edges_2d,
|
edges_2d,
|
||||||
pdf_path,
|
pdf_path,
|
||||||
draw_sa2_holes=True,
|
draw_sa2_holes=True,
|
||||||
draw_sa2_arcs=True,
|
|
||||||
draw_sa2_chamfers=True,
|
|
||||||
draw_sa2_step_features=True,
|
|
||||||
raw_geometry=so
|
raw_geometry=so
|
||||||
)
|
)
|
||||||
self.assertTrue(os.path.exists(pdf_path))
|
self.assertTrue(os.path.exists(pdf_path))
|
||||||
with open(pdf_path, "rb") as f:
|
with open(pdf_path, "rb") as f:
|
||||||
content = f.read().decode("utf-8", errors="ignore")
|
raw_bytes = f.read()
|
||||||
|
|
||||||
# ISO circular callouts (Electric Blue 0.10,0.45,0.90 for Ø or Amber Orange 0.90,0.45,0.10 for R)
|
content = raw_bytes.decode("utf-8", errors="ignore")
|
||||||
self.assertTrue("0.100 0.450 0.900 RG" in content or "0.900 0.450 0.100 RG" in content)
|
# ISO circular bore callouts (Electric Blue 0.100 0.450 0.900 RG)
|
||||||
|
self.assertIn("0.100 0.450 0.900 RG", content)
|
||||||
def test_export_pdf_iso_arc_and_chamfer_detection(self):
|
self.assertIn("THRU", content)
|
||||||
# Create mesh with a chamfer (10mm) and an arc cutout (R15mm)
|
|
||||||
t = np.linspace(0, np.pi/2, 8)
|
|
||||||
arc_pts = np.column_stack([15 - 15*np.cos(t), 15 - 15*np.sin(t)])
|
|
||||||
|
|
||||||
pts = [
|
|
||||||
[0, 15],
|
|
||||||
[0, 60],
|
|
||||||
[90, 60], # Chamfer start
|
|
||||||
[100, 50], # Chamfer end
|
|
||||||
[100, 0],
|
|
||||||
[15, 0]
|
|
||||||
]
|
|
||||||
polygon_pts = pts[:5] + list(arc_pts[::-1])
|
|
||||||
|
|
||||||
# Triangulate top face & extruded 3D mesh
|
|
||||||
tris = []
|
|
||||||
for i in range(len(polygon_pts)):
|
|
||||||
i_next = (i + 1) % len(polygon_pts)
|
|
||||||
p1_2d = polygon_pts[i]
|
|
||||||
p2_2d = polygon_pts[i_next]
|
|
||||||
p1_top = [p1_2d[0], p1_2d[1], 10.0]
|
|
||||||
p2_top = [p2_2d[0], p2_2d[1], 10.0]
|
|
||||||
p1_bot = [p1_2d[0], p1_2d[1], 0.0]
|
|
||||||
p2_bot = [p2_2d[0], p2_2d[1], 0.0]
|
|
||||||
# Side wall tris
|
|
||||||
tris.append([p1_bot, p1_top, p2_top, [0, 0, 1]])
|
|
||||||
tris.append([p1_bot, p2_top, p2_bot, [0, 0, 1]])
|
|
||||||
|
|
||||||
so = SlicingObject()
|
|
||||||
so.name = "ArcChamferPart"
|
|
||||||
so.mesh = np.array(tris, dtype=np.float64)
|
|
||||||
|
|
||||||
edges_2d = extract_unique_top_view_edges(so)
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
pdf_path = os.path.join(tmpdir, "test_arc_chamfer.pdf")
|
|
||||||
export_mesh_to_pdf(
|
|
||||||
edges_2d,
|
|
||||||
pdf_path,
|
|
||||||
draw_sa2_holes=True,
|
|
||||||
draw_sa2_arcs=True,
|
|
||||||
draw_sa2_chamfers=True,
|
|
||||||
draw_sa2_step_features=True,
|
|
||||||
raw_geometry=so
|
|
||||||
)
|
|
||||||
self.assertTrue(os.path.exists(pdf_path))
|
|
||||||
with open(pdf_path, "rb") as f:
|
|
||||||
content = f.read().decode("utf-8", errors="ignore")
|
|
||||||
|
|
||||||
# Verify ISO R (radius amber 0.900 0.450 0.100) and ISO C (chamfer purple 0.600 0.200 0.850)
|
|
||||||
self.assertIn("0.900 0.450 0.100 RG", content)
|
|
||||||
self.assertIn("(R15", content)
|
|
||||||
self.assertIn("0.600 0.200 0.850 RG", content)
|
|
||||||
self.assertIn("(C10mm) Tj", content)
|
|
||||||
|
|
||||||
def test_export_pdf_font_size(self):
|
def test_export_pdf_font_size(self):
|
||||||
so = SlicingObject()
|
so = SlicingObject()
|
||||||
|
|
@ -762,49 +702,22 @@ class TestPdfExporter(unittest.TestCase):
|
||||||
so.mesh = np.array(tris, dtype=np.float64)
|
so.mesh = np.array(tris, dtype=np.float64)
|
||||||
|
|
||||||
# 1. When no geometry is selected as visible (all False), extract_slicing_object_features returns empty
|
# 1. When no geometry is selected as visible (all False), extract_slicing_object_features returns empty
|
||||||
h, a, c, s = extract_slicing_object_features(
|
h_none = extract_slicing_object_features(
|
||||||
so, draw_wireframe=False, draw_boundary=False, draw_contour=False, draw_crease=False
|
so, draw_wireframe=False, draw_boundary=False, draw_contour=False, draw_crease=False
|
||||||
)
|
)
|
||||||
self.assertEqual((h, a, c, s), ([], [], [], []))
|
self.assertEqual(h_none, [])
|
||||||
|
|
||||||
# 2. When draw_boundary=True, features are extracted from boundary edges
|
# 2. When draw_boundary=True, features are extracted from boundary edges
|
||||||
h_b, a_b, c_b, s_b = extract_slicing_object_features(
|
h_b = extract_slicing_object_features(
|
||||||
so, draw_wireframe=False, draw_boundary=True, draw_contour=False, draw_crease=False
|
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
|
# 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(
|
h_w = extract_slicing_object_features(
|
||||||
so, draw_wireframe=True, draw_boundary=False, draw_contour=False, draw_crease=False
|
so, draw_wireframe=True, draw_boundary=False, draw_contour=False, draw_crease=False
|
||||||
)
|
)
|
||||||
self.assertIsNotNone(h_w)
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue