feat: 增强一级关系识别与保持关系策略
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user