2026-07-30 17:54:01 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import math
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse
|
|
|
|
|
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
|
|
|
|
|
from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Pnt
|
|
|
|
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
|
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
|
|
|
|
|
|
from step_editor.geometry_utils import _finalize_boolean_result
|
|
|
|
|
from step_editor.model import StepModel
|
|
|
|
|
from step_editor.step_io import _write_step
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_half_round_slot_model(path: Path) -> None:
|
|
|
|
|
box = BRepPrimAPI_MakeBox(30.0, 20.0, 10.0).Shape()
|
|
|
|
|
# A cylinder centered on the top face cuts a half-round groove open to Z+.
|
|
|
|
|
cutter_axis = gp_Ax2(gp_Pnt(-5.0, 10.0, 10.0), gp_Dir(1.0, 0.0, 0.0))
|
|
|
|
|
cutter = BRepPrimAPI_MakeCylinder(cutter_axis, 3.0, 40.0).Shape()
|
|
|
|
|
cut = BRepAlgoAPI_Cut(box, cutter)
|
|
|
|
|
shape = _finalize_boolean_result(cut, "verify slot model cut")
|
|
|
|
|
_write_step(shape, path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_obround_slot_model(path: Path) -> None:
|
|
|
|
|
plate = BRepPrimAPI_MakeBox(40.0, 24.0, 6.0).Shape()
|
|
|
|
|
axis_1 = gp_Ax2(gp_Pnt(15.0, 12.0, -1.0), gp_Dir(0.0, 0.0, 1.0))
|
|
|
|
|
axis_2 = gp_Ax2(gp_Pnt(25.0, 12.0, -1.0), gp_Dir(0.0, 0.0, 1.0))
|
|
|
|
|
cylinder_1 = BRepPrimAPI_MakeCylinder(axis_1, 3.0, 8.0).Shape()
|
|
|
|
|
cylinder_2 = BRepPrimAPI_MakeCylinder(axis_2, 3.0, 8.0).Shape()
|
|
|
|
|
connector = BRepPrimAPI_MakeBox(gp_Pnt(15.0, 9.0, -1.0), 10.0, 6.0, 8.0).Shape()
|
|
|
|
|
fuse_1 = BRepAlgoAPI_Fuse(cylinder_1, connector)
|
|
|
|
|
tool = _finalize_boolean_result(fuse_1, "verify obround slot tool fuse")
|
|
|
|
|
fuse_2 = BRepAlgoAPI_Fuse(tool, cylinder_2)
|
|
|
|
|
tool = _finalize_boolean_result(fuse_2, "verify obround slot tool second fuse")
|
|
|
|
|
cut = BRepAlgoAPI_Cut(plate, tool)
|
|
|
|
|
shape = _finalize_boolean_result(cut, "verify obround slot model cut")
|
|
|
|
|
_write_step(shape, path)
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 14:56:50 +08:00
|
|
|
def _write_cross_obround_slot_model(path: Path) -> None:
|
|
|
|
|
plate = BRepPrimAPI_MakeBox(40.0, 40.0, 6.0).Shape()
|
|
|
|
|
|
|
|
|
|
def capsule_tool_x() -> object:
|
|
|
|
|
axis_1 = gp_Ax2(gp_Pnt(14.0, 20.0, -1.0), gp_Dir(0.0, 0.0, 1.0))
|
|
|
|
|
axis_2 = gp_Ax2(gp_Pnt(26.0, 20.0, -1.0), gp_Dir(0.0, 0.0, 1.0))
|
|
|
|
|
cylinder_1 = BRepPrimAPI_MakeCylinder(axis_1, 3.0, 8.0).Shape()
|
|
|
|
|
cylinder_2 = BRepPrimAPI_MakeCylinder(axis_2, 3.0, 8.0).Shape()
|
|
|
|
|
connector = BRepPrimAPI_MakeBox(gp_Pnt(14.0, 17.0, -1.0), 12.0, 6.0, 8.0).Shape()
|
|
|
|
|
fuse_1 = _finalize_boolean_result(BRepAlgoAPI_Fuse(cylinder_1, connector), "verify cross slot x tool fuse")
|
|
|
|
|
return _finalize_boolean_result(BRepAlgoAPI_Fuse(fuse_1, cylinder_2), "verify cross slot x tool second fuse")
|
|
|
|
|
|
|
|
|
|
def capsule_tool_y() -> object:
|
|
|
|
|
axis_1 = gp_Ax2(gp_Pnt(20.0, 14.0, -1.0), gp_Dir(0.0, 0.0, 1.0))
|
|
|
|
|
axis_2 = gp_Ax2(gp_Pnt(20.0, 26.0, -1.0), gp_Dir(0.0, 0.0, 1.0))
|
|
|
|
|
cylinder_1 = BRepPrimAPI_MakeCylinder(axis_1, 3.0, 8.0).Shape()
|
|
|
|
|
cylinder_2 = BRepPrimAPI_MakeCylinder(axis_2, 3.0, 8.0).Shape()
|
|
|
|
|
connector = BRepPrimAPI_MakeBox(gp_Pnt(17.0, 14.0, -1.0), 6.0, 12.0, 8.0).Shape()
|
|
|
|
|
fuse_1 = _finalize_boolean_result(BRepAlgoAPI_Fuse(cylinder_1, connector), "verify cross slot y tool fuse")
|
|
|
|
|
return _finalize_boolean_result(BRepAlgoAPI_Fuse(fuse_1, cylinder_2), "verify cross slot y tool second fuse")
|
|
|
|
|
|
|
|
|
|
cut_x = _finalize_boolean_result(BRepAlgoAPI_Cut(plate, capsule_tool_x()), "verify cross slot x cut")
|
|
|
|
|
cut_xy = _finalize_boolean_result(BRepAlgoAPI_Cut(cut_x, capsule_tool_y()), "verify cross slot y cut")
|
|
|
|
|
_write_step(cut_xy, path)
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 17:54:01 +08:00
|
|
|
def _slot_face_ids(model: StepModel) -> list[int]:
|
|
|
|
|
face_ids: list[int] = []
|
|
|
|
|
for face_id in range(len(model.faces)):
|
|
|
|
|
info = model.face_info(face_id)
|
|
|
|
|
if info.get("surface") != "cylinder":
|
|
|
|
|
continue
|
|
|
|
|
feature = model.feature_info(face_id)
|
|
|
|
|
if feature.get("slot_kind") != "partial-cylindrical-groove":
|
|
|
|
|
continue
|
|
|
|
|
span = float(feature.get("slot_angular_span") or info.get("angular_span") or 0.0)
|
|
|
|
|
if 1e-6 < span < math.tau * 0.92:
|
|
|
|
|
face_ids.append(face_id)
|
|
|
|
|
return face_ids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _first_slot_face(model: StepModel) -> int:
|
|
|
|
|
candidates = _slot_face_ids(model)
|
|
|
|
|
if not candidates:
|
|
|
|
|
raise SystemExit("no partial cylindrical slot face was recognized")
|
|
|
|
|
return candidates[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _slot_metric(model: StepModel, face_id: int, key: str) -> float:
|
|
|
|
|
feature = model.feature_info(face_id)
|
|
|
|
|
info = model.face_info(face_id)
|
|
|
|
|
value = feature.get(key)
|
|
|
|
|
if value is None:
|
|
|
|
|
value = info.get(key)
|
|
|
|
|
if value is None:
|
|
|
|
|
raise SystemExit(f"slot metric {key} is missing on Face {face_id}")
|
|
|
|
|
return float(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _slot_axis_center(model: StepModel, face_id: int) -> tuple[float, float, float]:
|
|
|
|
|
info = model.face_info(face_id)
|
|
|
|
|
axis_point = info.get("axis_point")
|
|
|
|
|
axis = info.get("axis")
|
|
|
|
|
v_range = info.get("same_domain_v_range") or info.get("v_range")
|
|
|
|
|
if not isinstance(axis_point, tuple) or not isinstance(axis, tuple) or not isinstance(v_range, tuple):
|
|
|
|
|
raise SystemExit(f"slot axis center data is missing on Face {face_id}")
|
|
|
|
|
v_mid = (float(v_range[0]) + float(v_range[1])) * 0.5
|
|
|
|
|
return (
|
|
|
|
|
float(axis_point[0]) + float(axis[0]) * v_mid,
|
|
|
|
|
float(axis_point[1]) + float(axis[1]) * v_mid,
|
|
|
|
|
float(axis_point[2]) + float(axis[2]) * v_mid,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _distance(left: tuple[float, float, float], right: tuple[float, float, float]) -> float:
|
|
|
|
|
return math.sqrt(
|
|
|
|
|
(left[0] - right[0]) ** 2
|
|
|
|
|
+ (left[1] - right[1]) ** 2
|
|
|
|
|
+ (left[2] - right[2]) ** 2
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nearest_metric(model: StepModel, key: str, target: float) -> tuple[int, float, float]:
|
|
|
|
|
best: tuple[int, float, float] | None = None
|
|
|
|
|
for face_id in _slot_face_ids(model):
|
|
|
|
|
value = _slot_metric(model, face_id, key)
|
|
|
|
|
error = abs(value - target)
|
|
|
|
|
if best is None or error < best[2]:
|
|
|
|
|
best = (face_id, value, error)
|
|
|
|
|
if best is None:
|
|
|
|
|
raise SystemExit("no slot face remained after edit")
|
|
|
|
|
return best
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nearest_axis_center(model: StepModel, target: tuple[float, float, float]) -> tuple[int, tuple[float, float, float], float]:
|
|
|
|
|
best: tuple[int, tuple[float, float, float], float] | None = None
|
|
|
|
|
for face_id in _slot_face_ids(model):
|
|
|
|
|
center = _slot_axis_center(model, face_id)
|
|
|
|
|
error = _distance(center, target)
|
|
|
|
|
if best is None or error < best[2]:
|
|
|
|
|
best = (face_id, center, error)
|
|
|
|
|
if best is None:
|
|
|
|
|
raise SystemExit("no slot face remained after axis move")
|
|
|
|
|
return best
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _obround_total_lengths(model: StepModel) -> list[tuple[int, int, float, float]]:
|
|
|
|
|
rows: list[tuple[int, int, float, float]] = []
|
|
|
|
|
seen_pairs: set[tuple[int, int]] = set()
|
|
|
|
|
for face_id in _slot_face_ids(model):
|
|
|
|
|
info = model.face_info(face_id)
|
|
|
|
|
diameter = float(info.get("diameter") or 0.0)
|
|
|
|
|
if diameter <= 1e-9:
|
|
|
|
|
continue
|
|
|
|
|
feature = model.feature_info(face_id)
|
|
|
|
|
try:
|
|
|
|
|
cutter_plan = model._bounded_cylinder_cutter_plan(face_id, diameter, feature)
|
|
|
|
|
pair_plan = model._paired_obround_slot_plan(face_id, diameter, feature, cutter_plan)
|
|
|
|
|
except Exception:
|
|
|
|
|
continue
|
|
|
|
|
pair_face_id = pair_plan.get("slot_pair_face_id")
|
|
|
|
|
center_distance = pair_plan.get("slot_pair_axis_distance")
|
|
|
|
|
if pair_face_id in {None, ""} or center_distance in {None, ""}:
|
|
|
|
|
continue
|
|
|
|
|
pair_key = tuple(sorted((face_id, int(pair_face_id))))
|
|
|
|
|
if pair_key in seen_pairs:
|
|
|
|
|
continue
|
|
|
|
|
seen_pairs.add(pair_key)
|
|
|
|
|
rows.append((face_id, int(pair_face_id), float(center_distance) + diameter, float(center_distance)))
|
|
|
|
|
return rows
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 14:56:50 +08:00
|
|
|
def _complex_slot_face_ids(model: StepModel) -> list[int]:
|
|
|
|
|
face_ids: list[int] = []
|
|
|
|
|
for face_id in range(len(model.faces)):
|
|
|
|
|
info = model.face_info(face_id)
|
|
|
|
|
if info.get("surface") != "cylinder":
|
|
|
|
|
continue
|
|
|
|
|
feature = model.feature_info(face_id)
|
|
|
|
|
if feature.get("slot_kind") == "partial-cylindrical-groove" and feature.get("slot_status") == "blocked":
|
|
|
|
|
face_ids.append(face_id)
|
|
|
|
|
return face_ids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_complex_slot_guard() -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="geom_param_cross_slot_guard_") as temp_dir:
|
|
|
|
|
model_path = Path(temp_dir) / "cross_obround_slot.step"
|
|
|
|
|
_write_cross_obround_slot_model(model_path)
|
|
|
|
|
model = StepModel.load(model_path)
|
|
|
|
|
face_ids = _complex_slot_face_ids(model)
|
|
|
|
|
if len(face_ids) < 4:
|
|
|
|
|
raise SystemExit(f"cross obround slot should expose blocked slot ends, got {face_ids}")
|
|
|
|
|
face_id = face_ids[0]
|
|
|
|
|
feature = model.feature_info(face_id)
|
|
|
|
|
blockers = str(feature.get("slot_blockers") or feature.get("recognition_blockers") or "")
|
|
|
|
|
if "交叉槽" not in blockers and "多槽组" not in blockers and "复杂草图槽" not in blockers:
|
|
|
|
|
raise SystemExit(f"cross slot blocker should explain the unsupported reason: {feature}")
|
|
|
|
|
if "复杂槽" not in str(feature.get("feature_type") or ""):
|
|
|
|
|
raise SystemExit(f"cross slot should be labeled as a complex slot: {feature.get('feature_type')}")
|
|
|
|
|
|
|
|
|
|
candidates = model.editable_feature_candidates(limit=80, detailed=False)
|
|
|
|
|
forbidden_actions = {
|
|
|
|
|
"resize_cylinder",
|
|
|
|
|
"resize_slot_width",
|
|
|
|
|
"resize_slot_depth",
|
|
|
|
|
"resize_slot_arc_length",
|
|
|
|
|
"resize_slot_angular_span",
|
|
|
|
|
}
|
|
|
|
|
leaked = [
|
|
|
|
|
(item.get("operation_key"), item.get("target_id"))
|
|
|
|
|
for item in candidates
|
|
|
|
|
if int(item.get("target_id", -1)) in face_ids and item.get("operation_key") in forbidden_actions
|
|
|
|
|
]
|
|
|
|
|
if leaked:
|
|
|
|
|
raise SystemExit(f"cross slot should not leak editable slot entries: {leaked}")
|
|
|
|
|
|
|
|
|
|
width_plan = model.cylindrical_slot_resize_plan(face_id, 7.0, "width")
|
|
|
|
|
angle_plan = model.cylindrical_slot_angular_span_plan(face_id, math.pi * 0.75)
|
|
|
|
|
current_center = _slot_axis_center(model, face_id)
|
|
|
|
|
axis_plan = model.cylindrical_slot_axis_move_plan(face_id, (current_center[0] + 1.0, current_center[1], current_center[2]))
|
|
|
|
|
total_plan = model.cylindrical_slot_total_length_plan(face_id, 16.0)
|
|
|
|
|
for label, plan in (
|
|
|
|
|
("width", width_plan),
|
|
|
|
|
("angle", angle_plan),
|
|
|
|
|
("axis", axis_plan),
|
|
|
|
|
("total_length", total_plan),
|
|
|
|
|
):
|
|
|
|
|
if plan.get("status") != "blocked":
|
|
|
|
|
raise SystemExit(f"cross slot {label} plan should be blocked: {plan}")
|
|
|
|
|
text = f"{plan.get('message')} {plan.get('blockers')}"
|
|
|
|
|
if "交叉槽" not in text and "多槽组" not in text and "复杂草图槽" not in text:
|
|
|
|
|
raise SystemExit(f"cross slot {label} plan should explain unsupported complex slot: {plan}")
|
|
|
|
|
print(f"complex_slot_guard=blocked_faces {face_ids}")
|
|
|
|
|
|
|
|
|
|
|
2026-07-30 17:54:01 +08:00
|
|
|
def _nearest_obround_total_length(model: StepModel, target_total_length: float) -> tuple[int, int, float, float, float]:
|
|
|
|
|
best: tuple[int, int, float, float, float] | None = None
|
|
|
|
|
for face_id, pair_face_id, total_length, center_distance in _obround_total_lengths(model):
|
|
|
|
|
error = abs(total_length - target_total_length)
|
|
|
|
|
if best is None or error < best[4]:
|
|
|
|
|
best = (face_id, pair_face_id, total_length, center_distance, error)
|
|
|
|
|
if best is None:
|
|
|
|
|
raise SystemExit("no paired obround slot ends were recognized")
|
|
|
|
|
return best
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nearest_obround_axis_pair(
|
|
|
|
|
model: StepModel,
|
|
|
|
|
target_center_1: tuple[float, float, float],
|
|
|
|
|
target_center_2: tuple[float, float, float],
|
|
|
|
|
) -> tuple[int, int, tuple[float, float, float], tuple[float, float, float], float]:
|
|
|
|
|
best: tuple[int, int, tuple[float, float, float], tuple[float, float, float], float] | None = None
|
|
|
|
|
for face_id, pair_face_id, _total_length, _center_distance in _obround_total_lengths(model):
|
|
|
|
|
center_1 = _slot_axis_center(model, face_id)
|
|
|
|
|
center_2 = _slot_axis_center(model, pair_face_id)
|
|
|
|
|
direct_error = max(_distance(center_1, target_center_1), _distance(center_2, target_center_2))
|
|
|
|
|
swapped_error = max(_distance(center_1, target_center_2), _distance(center_2, target_center_1))
|
|
|
|
|
if swapped_error < direct_error:
|
|
|
|
|
error = swapped_error
|
|
|
|
|
ordered_1, ordered_2 = center_2, center_1
|
|
|
|
|
else:
|
|
|
|
|
error = direct_error
|
|
|
|
|
ordered_1, ordered_2 = center_1, center_2
|
|
|
|
|
if best is None or error < best[4]:
|
|
|
|
|
best = (face_id, pair_face_id, ordered_1, ordered_2, error)
|
|
|
|
|
if best is None:
|
|
|
|
|
raise SystemExit("no paired obround slot ends were recognized after axis move")
|
|
|
|
|
return best
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _slot_candidate_summary(model: StepModel) -> list[tuple[int, float, float, str]]:
|
|
|
|
|
rows: list[tuple[int, float, float, str]] = []
|
|
|
|
|
for face_id in range(len(model.faces)):
|
|
|
|
|
info = model.face_info(face_id)
|
|
|
|
|
if info.get("surface") != "cylinder":
|
|
|
|
|
continue
|
|
|
|
|
feature = model.feature_info(face_id)
|
|
|
|
|
span = float(feature.get("slot_angular_span") or info.get("angular_span") or 0.0)
|
|
|
|
|
diameter = float(info.get("diameter") or feature.get("diameter") or 0.0)
|
|
|
|
|
rows.append((face_id, round(diameter, 6), round(span, 6), str(feature.get("slot_kind") or "")))
|
|
|
|
|
return rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_case(mode: str, target: float, tolerance: float) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="geom_param_slot_") as temp_dir:
|
|
|
|
|
model_path = Path(temp_dir) / "half_round_slot.step"
|
|
|
|
|
_write_half_round_slot_model(model_path)
|
|
|
|
|
model = StepModel.load(model_path)
|
|
|
|
|
face_id = _first_slot_face(model)
|
|
|
|
|
before = model.stats()
|
|
|
|
|
|
|
|
|
|
if mode == "width":
|
|
|
|
|
metric_key = "slot_chord_width_estimate"
|
|
|
|
|
plan = model.cylindrical_slot_resize_plan(face_id, target, "width")
|
|
|
|
|
result = model.resize_cylindrical_slot_width(face_id, target)
|
|
|
|
|
elif mode == "depth":
|
|
|
|
|
metric_key = "slot_sagitta_depth_estimate"
|
|
|
|
|
plan = model.cylindrical_slot_resize_plan(face_id, target, "depth")
|
|
|
|
|
result = model.resize_cylindrical_slot_depth(face_id, target)
|
|
|
|
|
elif mode == "arc_length":
|
|
|
|
|
metric_key = "slot_arc_length_estimate"
|
|
|
|
|
plan = model.cylindrical_slot_resize_plan(face_id, target, "arc_length")
|
|
|
|
|
result = model.resize_cylindrical_slot_arc_length(face_id, target)
|
|
|
|
|
elif mode == "angular_span":
|
|
|
|
|
metric_key = "slot_angular_span"
|
|
|
|
|
plan = model.cylindrical_slot_angular_span_plan(face_id, target)
|
|
|
|
|
result = model.resize_cylindrical_slot_angular_span(face_id, target)
|
|
|
|
|
elif mode == "axis_center":
|
|
|
|
|
current_center = _slot_axis_center(model, face_id)
|
|
|
|
|
target_center = (current_center[0], current_center[1] + target, current_center[2])
|
|
|
|
|
plan = model.cylindrical_slot_axis_move_plan(face_id, target_center)
|
|
|
|
|
if plan["status"] == "blocked":
|
|
|
|
|
raise SystemExit(f"axis_center plan was blocked: {plan['message']}")
|
|
|
|
|
result = model.move_cylindrical_slot_axis(face_id, target_center)
|
|
|
|
|
after = model.stats()
|
|
|
|
|
verified_face, center, error = _nearest_axis_center(model, target_center)
|
|
|
|
|
if error > tolerance:
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
f"axis_center verification failed: target={target_center}, nearest={center}, "
|
|
|
|
|
f"error={error:g}; candidates={_slot_candidate_summary(model)}"
|
|
|
|
|
)
|
|
|
|
|
print("mode=axis_center")
|
|
|
|
|
print(f"source_face={face_id}")
|
|
|
|
|
print(f"strategy={plan.get('resize_strategy')}")
|
|
|
|
|
print(f"before={before}")
|
|
|
|
|
print(f"after={after}")
|
|
|
|
|
print(f"verified_face={verified_face}")
|
|
|
|
|
print(f"target={target_center} value={center} error={error:.6g}")
|
|
|
|
|
print(result.encode("ascii", "backslashreplace").decode("ascii"))
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
raise SystemExit(f"unsupported mode: {mode}")
|
|
|
|
|
|
|
|
|
|
if plan["status"] == "blocked":
|
|
|
|
|
raise SystemExit(f"{mode} plan was blocked: {plan['message']}")
|
|
|
|
|
|
|
|
|
|
after = model.stats()
|
|
|
|
|
verified_face, value, error = _nearest_metric(model, metric_key, target)
|
|
|
|
|
if error > tolerance:
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
f"{mode} verification failed: target={target:g}, nearest={value:g}, error={error:g}; "
|
|
|
|
|
f"candidates={_slot_candidate_summary(model)}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print(f"mode={mode}")
|
|
|
|
|
print(f"source_face={face_id}")
|
|
|
|
|
print(f"strategy={plan.get('slot_resize_strategy')}")
|
|
|
|
|
print(f"before={before}")
|
|
|
|
|
print(f"after={after}")
|
|
|
|
|
print(f"verified_face={verified_face}")
|
|
|
|
|
print(f"target={target:.6f} value={value:.6f} error={error:.6g}")
|
|
|
|
|
print(result.encode("ascii", "backslashreplace").decode("ascii"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_total_length_case(target: float, tolerance: float) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="geom_param_obround_") as temp_dir:
|
|
|
|
|
model_path = Path(temp_dir) / "obround_slot.step"
|
|
|
|
|
_write_obround_slot_model(model_path)
|
|
|
|
|
model = StepModel.load(model_path)
|
|
|
|
|
face_id, pair_face_id, current_total_length, current_center_distance, _ = _nearest_obround_total_length(model, 16.0)
|
|
|
|
|
before = model.stats()
|
|
|
|
|
plan = model.cylindrical_slot_total_length_plan(face_id, target)
|
|
|
|
|
if plan["status"] == "blocked":
|
|
|
|
|
raise SystemExit(f"total_length plan was blocked: {plan['message']}")
|
|
|
|
|
result = model.resize_cylindrical_slot_total_length(face_id, target)
|
|
|
|
|
after = model.stats()
|
|
|
|
|
verified_face, verified_pair, total_length, center_distance, error = _nearest_obround_total_length(model, target)
|
|
|
|
|
if error > tolerance:
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
f"total_length verification failed: target={target:g}, nearest={total_length:g}, "
|
|
|
|
|
f"error={error:g}; pairs={_obround_total_lengths(model)}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print("mode=total_length")
|
|
|
|
|
print(f"source_face={face_id}")
|
|
|
|
|
print(f"source_pair_face={pair_face_id}")
|
|
|
|
|
print(f"strategy={plan.get('slot_resize_strategy')}")
|
|
|
|
|
print(f"before={before}")
|
|
|
|
|
print(f"after={after}")
|
|
|
|
|
print(f"current_total_length={current_total_length:.6f}")
|
|
|
|
|
print(f"current_center_distance={current_center_distance:.6f}")
|
|
|
|
|
print(f"verified_face={verified_face}")
|
|
|
|
|
print(f"verified_pair_face={verified_pair}")
|
|
|
|
|
print(f"target={target:.6f} value={total_length:.6f} center_distance={center_distance:.6f} error={error:.6g}")
|
|
|
|
|
print(result.encode("ascii", "backslashreplace").decode("ascii"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_center_distance_case(target: float, tolerance: float) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="geom_param_obround_center_distance_") as temp_dir:
|
|
|
|
|
model_path = Path(temp_dir) / "obround_slot.step"
|
|
|
|
|
_write_obround_slot_model(model_path)
|
|
|
|
|
model = StepModel.load(model_path)
|
|
|
|
|
face_id, pair_face_id, current_total_length, current_center_distance, _ = _nearest_obround_total_length(model, 16.0)
|
|
|
|
|
slot_diameter = current_total_length - current_center_distance
|
|
|
|
|
target_total_length = target + slot_diameter
|
|
|
|
|
before = model.stats()
|
|
|
|
|
plan = model.cylindrical_slot_center_distance_plan(face_id, target)
|
|
|
|
|
if plan["status"] == "blocked":
|
|
|
|
|
raise SystemExit(f"center_distance plan was blocked: {plan['message']}")
|
|
|
|
|
result = model.resize_cylindrical_slot_center_distance(face_id, target)
|
|
|
|
|
after = model.stats()
|
|
|
|
|
verified_face, verified_pair, total_length, center_distance, error = _nearest_obround_total_length(
|
|
|
|
|
model,
|
|
|
|
|
target_total_length,
|
|
|
|
|
)
|
|
|
|
|
center_error = abs(center_distance - target)
|
|
|
|
|
if center_error > tolerance:
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
f"center_distance verification failed: target={target:g}, nearest={center_distance:g}, "
|
|
|
|
|
f"error={center_error:g}; pairs={_obround_total_lengths(model)}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print("mode=center_distance")
|
|
|
|
|
print(f"source_face={face_id}")
|
|
|
|
|
print(f"source_pair_face={pair_face_id}")
|
|
|
|
|
print(f"strategy={plan.get('slot_resize_strategy')}")
|
|
|
|
|
print(f"before={before}")
|
|
|
|
|
print(f"after={after}")
|
|
|
|
|
print(f"current_total_length={current_total_length:.6f}")
|
|
|
|
|
print(f"current_center_distance={current_center_distance:.6f}")
|
|
|
|
|
print(f"slot_diameter={slot_diameter:.6f}")
|
|
|
|
|
print(f"verified_face={verified_face}")
|
|
|
|
|
print(f"verified_pair_face={verified_pair}")
|
|
|
|
|
print(
|
|
|
|
|
f"target_center_distance={target:.6f} value={center_distance:.6f} "
|
|
|
|
|
f"total_length={total_length:.6f} total_length_error={error:.6g} error={center_error:.6g}"
|
|
|
|
|
)
|
|
|
|
|
print(result.encode("ascii", "backslashreplace").decode("ascii"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_obround_axis_center_case(offset: float, tolerance: float) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="geom_param_obround_axis_") as temp_dir:
|
|
|
|
|
model_path = Path(temp_dir) / "obround_slot.step"
|
|
|
|
|
_write_obround_slot_model(model_path)
|
|
|
|
|
model = StepModel.load(model_path)
|
|
|
|
|
face_id, pair_face_id, current_total_length, current_center_distance, _ = _nearest_obround_total_length(model, 16.0)
|
|
|
|
|
current_center = _slot_axis_center(model, face_id)
|
|
|
|
|
pair_center = _slot_axis_center(model, pair_face_id)
|
|
|
|
|
target_center = (current_center[0], current_center[1] + offset, current_center[2])
|
|
|
|
|
target_pair_center = (pair_center[0], pair_center[1] + offset, pair_center[2])
|
|
|
|
|
before = model.stats()
|
|
|
|
|
plan = model.cylindrical_slot_axis_move_plan(face_id, target_center)
|
|
|
|
|
if plan["status"] == "blocked":
|
|
|
|
|
raise SystemExit(f"obround_axis_center plan was blocked: {plan['message']}")
|
|
|
|
|
if plan.get("resize_strategy") != "paired-obround-slot-axis-prism":
|
|
|
|
|
raise SystemExit(f"obround_axis_center did not choose paired strategy: {plan.get('resize_strategy')}")
|
|
|
|
|
result = model.move_cylindrical_slot_axis(face_id, target_center)
|
|
|
|
|
after = model.stats()
|
|
|
|
|
verified_face, verified_pair, center_1, center_2, error = _nearest_obround_axis_pair(
|
|
|
|
|
model,
|
|
|
|
|
target_center,
|
|
|
|
|
target_pair_center,
|
|
|
|
|
)
|
|
|
|
|
if error > tolerance:
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
f"obround_axis_center verification failed: target=({target_center}, {target_pair_center}), "
|
|
|
|
|
f"nearest=({center_1}, {center_2}), error={error:g}; pairs={_obround_total_lengths(model)}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print("mode=obround_axis_center")
|
|
|
|
|
print(f"source_face={face_id}")
|
|
|
|
|
print(f"source_pair_face={pair_face_id}")
|
|
|
|
|
print(f"strategy={plan.get('resize_strategy')}")
|
|
|
|
|
print(f"before={before}")
|
|
|
|
|
print(f"after={after}")
|
|
|
|
|
print(f"current_total_length={current_total_length:.6f}")
|
|
|
|
|
print(f"current_center_distance={current_center_distance:.6f}")
|
|
|
|
|
print(f"verified_face={verified_face}")
|
|
|
|
|
print(f"verified_pair_face={verified_pair}")
|
|
|
|
|
print(f"target=({target_center}, {target_pair_center}) value=({center_1}, {center_2}) error={error:.6g}")
|
|
|
|
|
print(result.encode("ascii", "backslashreplace").decode("ascii"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
parser = argparse.ArgumentParser(description="Verify partial cylindrical slot resize operations.")
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--mode",
|
|
|
|
|
default="all",
|
|
|
|
|
choices=[
|
|
|
|
|
"all",
|
|
|
|
|
"width",
|
|
|
|
|
"depth",
|
|
|
|
|
"arc_length",
|
|
|
|
|
"angular_span",
|
|
|
|
|
"axis_center",
|
|
|
|
|
"obround_axis_center",
|
|
|
|
|
"total_length",
|
|
|
|
|
"center_distance",
|
2026-08-10 14:56:50 +08:00
|
|
|
"complex_slot_guard",
|
2026-07-30 17:54:01 +08:00
|
|
|
],
|
|
|
|
|
help="Slot metric to verify.",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument("--width", type=float, default=8.0)
|
|
|
|
|
parser.add_argument("--depth", type=float, default=4.0)
|
|
|
|
|
parser.add_argument("--arc-length", type=float, default=12.0)
|
|
|
|
|
parser.add_argument("--angular-span", type=float, default=2.2)
|
|
|
|
|
parser.add_argument("--axis-center", type=float, default=1.0, help="Radial axis-center offset to verify.")
|
|
|
|
|
parser.add_argument("--obround-axis-center", type=float, default=1.0, help="Obround slot axis-center offset to verify.")
|
|
|
|
|
parser.add_argument("--total-length", type=float, default=20.0)
|
|
|
|
|
parser.add_argument("--center-distance", type=float, default=14.0)
|
|
|
|
|
parser.add_argument("--tolerance", type=float, default=2e-4)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
2026-08-10 14:56:50 +08:00
|
|
|
if args.mode == "all":
|
|
|
|
|
cases = [
|
2026-07-30 17:54:01 +08:00
|
|
|
("width", args.width),
|
|
|
|
|
("depth", args.depth),
|
|
|
|
|
("arc_length", args.arc_length),
|
|
|
|
|
("angular_span", args.angular_span),
|
|
|
|
|
("axis_center", args.axis_center),
|
|
|
|
|
("obround_axis_center", args.obround_axis_center),
|
|
|
|
|
("total_length", args.total_length),
|
|
|
|
|
("center_distance", args.center_distance),
|
2026-08-10 14:56:50 +08:00
|
|
|
("complex_slot_guard", 0.0),
|
2026-07-30 17:54:01 +08:00
|
|
|
]
|
2026-08-10 14:56:50 +08:00
|
|
|
elif args.mode == "complex_slot_guard":
|
|
|
|
|
cases = [("complex_slot_guard", 0.0)]
|
|
|
|
|
else:
|
|
|
|
|
cases = [(args.mode, getattr(args, args.mode.replace("-", "_")))]
|
2026-07-30 17:54:01 +08:00
|
|
|
for mode, target in cases:
|
2026-08-10 14:56:50 +08:00
|
|
|
if mode == "complex_slot_guard":
|
|
|
|
|
_run_complex_slot_guard()
|
|
|
|
|
elif mode == "total_length":
|
2026-07-30 17:54:01 +08:00
|
|
|
_run_total_length_case(float(target), args.tolerance)
|
|
|
|
|
elif mode == "center_distance":
|
|
|
|
|
_run_center_distance_case(float(target), args.tolerance)
|
|
|
|
|
elif mode == "obround_axis_center":
|
|
|
|
|
_run_obround_axis_center_case(float(target), args.tolerance)
|
|
|
|
|
else:
|
|
|
|
|
_run_case(mode, float(target), args.tolerance)
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|