feat: 增强一级关系识别与保持关系策略

This commit is contained in:
2026-08-10 14:56:50 +08:00
parent 12250603dd
commit 83c44265ba
24 changed files with 3460 additions and 241 deletions
+100 -6
View File
@@ -45,6 +45,32 @@ def _write_obround_slot_model(path: Path) -> None:
_write_step(shape, path)
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)
def _slot_face_ids(model: StepModel) -> list[int]:
face_ids: list[int] = []
for face_id in range(len(model.faces)):
@@ -151,6 +177,69 @@ def _obround_total_lengths(model: StepModel) -> list[tuple[int, int, float, floa
return rows
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}")
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):
@@ -404,6 +493,7 @@ def main() -> int:
"obround_axis_center",
"total_length",
"center_distance",
"complex_slot_guard",
],
help="Slot metric to verify.",
)
@@ -418,8 +508,8 @@ def main() -> int:
parser.add_argument("--tolerance", type=float, default=2e-4)
args = parser.parse_args()
cases = (
[
if args.mode == "all":
cases = [
("width", args.width),
("depth", args.depth),
("arc_length", args.arc_length),
@@ -428,12 +518,16 @@ def main() -> int:
("obround_axis_center", args.obround_axis_center),
("total_length", args.total_length),
("center_distance", args.center_distance),
("complex_slot_guard", 0.0),
]
if args.mode == "all"
else [(args.mode, getattr(args, args.mode.replace("-", "_")))]
)
elif args.mode == "complex_slot_guard":
cases = [("complex_slot_guard", 0.0)]
else:
cases = [(args.mode, getattr(args, args.mode.replace("-", "_")))]
for mode, target in cases:
if mode == "total_length":
if mode == "complex_slot_guard":
_run_complex_slot_guard()
elif mode == "total_length":
_run_total_length_case(float(target), args.tolerance)
elif mode == "center_distance":
_run_center_distance_case(float(target), args.tolerance)