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
+124 -2
View File
@@ -5,7 +5,9 @@ from pathlib import Path
import sys
import tempfile
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.gp import gp_Pnt
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
@@ -20,6 +22,29 @@ def _write_plate_model(path: Path) -> None:
_write_step(shape, path)
def _fuse_shapes(left, right):
op = BRepAlgoAPI_Fuse(left, right)
op.SetFuzzyValue(1e-6)
op.Build()
if not op.IsDone():
raise RuntimeError("open shell fixture fuse failed")
return op.Shape()
def _write_open_thin_wall_box_model(path: Path) -> None:
bottom = BRepPrimAPI_MakeBox(30.0, 20.0, 2.0).Shape()
walls = (
BRepPrimAPI_MakeBox(gp_Pnt(0.0, 0.0, 2.0), 2.0, 20.0, 10.0).Shape(),
BRepPrimAPI_MakeBox(gp_Pnt(28.0, 0.0, 2.0), 2.0, 20.0, 10.0).Shape(),
BRepPrimAPI_MakeBox(gp_Pnt(2.0, 0.0, 2.0), 26.0, 2.0, 10.0).Shape(),
BRepPrimAPI_MakeBox(gp_Pnt(2.0, 18.0, 2.0), 26.0, 2.0, 10.0).Shape(),
)
shape = bottom
for wall in walls:
shape = _fuse_shapes(shape, wall)
_write_step(shape, path)
def _first_shell_face(model: StepModel, thickness: float, tolerance: float) -> int:
candidates: list[tuple[int, int]] = []
for face_id in range(len(model.faces)):
@@ -258,9 +283,101 @@ def _run_case(mode: str, source_thickness: float, target_thickness: float, toler
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _first_open_shell_wall_face(model: StepModel, thickness: float, tolerance: float) -> int:
open_shell_faces: list[int] = []
candidates: list[tuple[int, int]] = []
for face_id in range(len(model.faces)):
info = model.feature_info(face_id)
if info.get("open_shell_context_status") == "limited":
open_shell_faces.append(face_id)
if info.get("surface") != "plane":
continue
if info.get("shell_region_status") != "candidate":
continue
if info.get("open_shell_context_status") != "limited":
continue
current = float(info.get("shell_thickness_estimate") or 0.0)
if abs(current - thickness) > tolerance:
continue
confidence_rank = {"high": 0, "medium": 1, "low": 2}.get(str(info.get("shell_confidence")), 3)
candidates.append((confidence_rank, face_id))
if not open_shell_faces:
raise SystemExit("open thin-wall box should expose an open_shell limited context")
if not candidates:
raise SystemExit(f"no open shell wall face with thickness near {thickness:g}")
candidates.sort()
return candidates[0][1]
def _assert_no_open_shell_context(model: StepModel, label: str) -> None:
leaked = [
face_id
for face_id in range(len(model.faces))
if model.feature_info(face_id).get("open_shell_context_status") == "limited"
]
if leaked:
raise SystemExit(f"{label} should not be recognized as open shell context: {leaked}")
def _run_open_shell_context_case(source_thickness: float, target_thickness: float, tolerance: float) -> None:
with tempfile.TemporaryDirectory(prefix="geom_param_open_shell_context_") as temp_dir:
root = Path(temp_dir)
open_path = root / "open_thin_wall_box.step"
_write_open_thin_wall_box_model(open_path)
model = StepModel.load(open_path)
face_id = _first_open_shell_wall_face(model, source_thickness, tolerance)
info = model.feature_info(face_id)
limited_actions = str(info.get("recognition_limited_actions") or "")
if "完整抽壳/开口面编辑" not in limited_actions:
raise SystemExit(f"open shell should show full shell/opening edit as limited: {info}")
ready_actions = str(info.get("recognition_ready_actions") or "")
if "壳体厚度" not in ready_actions:
raise SystemExit(f"open shell wall should keep local thickness editable: {info}")
evidence_keys = tuple(info.get("recognition_evidence_keys") or ())
if "open_shell_context" not in evidence_keys:
raise SystemExit(f"open shell recognition evidence should be explicit: {info}")
local_plan = model.shell_thickness_plan(face_id, target_thickness)
if local_plan.get("status") == "blocked":
raise SystemExit(f"open shell local thickness should remain available: {local_plan}")
if local_plan.get("open_shell_context_status") != "limited":
raise SystemExit(f"open shell fields should be present in local plan: {local_plan}")
if "完整抽壳/开口面" not in str(local_plan.get("message") or ""):
raise SystemExit(f"open shell local plan should explain the limitation: {local_plan}")
owning_plan = model.shell_thickness_owning_scale_plan(face_id, target_thickness)
if owning_plan.get("status") == "blocked":
raise SystemExit(f"open shell owning thickness should remain available: {owning_plan}")
if owning_plan.get("open_shell_context_status") != "limited":
raise SystemExit(f"open shell fields should be present in owning plan: {owning_plan}")
editable_candidates = model.editable_feature_candidates(limit=30, detailed=False)
shell_entries = [
item
for item in editable_candidates
if item.get("operation_key") == "resize_shell_thickness" and int(item.get("target_id", -1)) == face_id
]
if not shell_entries:
raise SystemExit(f"open shell wall should be visible as editable shell thickness candidate: {editable_candidates}")
shell_note = str(shell_entries[0].get("note") or "")
if "完整抽壳/开口面" not in shell_note:
raise SystemExit(f"open shell candidate should explain full shell limitation: {shell_entries[0]}")
closed_path = root / "closed_plate.step"
_write_plate_model(closed_path)
closed_model = StepModel.load(closed_path)
_assert_no_open_shell_context(closed_model, "closed plate")
print("open_shell_context=limited")
print(f"face_id={face_id}")
print(f"limited_actions={limited_actions}")
print(f"local_plan_status={local_plan.get('status')}")
print(f"owning_plan_status={owning_plan.get('status')}")
def main() -> int:
parser = argparse.ArgumentParser(description="Verify shell/thin-wall thickness edit operations.")
parser.add_argument("--mode", default="all", choices=["all", "local", "owning"])
parser.add_argument("--mode", default="all", choices=["all", "local", "owning", "open-shell-context"])
parser.add_argument("--source-thickness", type=float, default=2.0)
parser.add_argument("--target-thickness", type=float, default=3.0)
parser.add_argument("--tolerance", type=float, default=2e-4)
@@ -268,7 +385,12 @@ def main() -> int:
modes = ["local", "owning"] if args.mode == "all" else [args.mode]
for mode in modes:
_run_case(mode, args.source_thickness, args.target_thickness, args.tolerance)
if mode == "open-shell-context":
_run_open_shell_context_case(args.source_thickness, args.target_thickness, args.tolerance)
else:
_run_case(mode, args.source_thickness, args.target_thickness, args.tolerance)
if args.mode == "all":
_run_open_shell_context_case(args.source_thickness, args.target_thickness, args.tolerance)
return 0