feat: 完善 STEP/B-Rep 一级关系参数化编辑

This commit is contained in:
2026-08-07 18:08:32 +08:00
parent eef9efcc1e
commit 12250603dd
26 changed files with 4254 additions and 1270 deletions
+188 -2
View File
@@ -41,6 +41,28 @@ def _first_line_edge(shape: TopoDS_Shape, minimum_length: float = 5.0) -> TopoDS
raise SystemExit("no line edge found in generated box")
def _line_edge_endpoint_records(shape: TopoDS_Shape) -> list[dict[str, object]]:
records: list[dict[str, object]] = []
for edge in TopologyExplorer(shape, ignore_orientation=True).edges():
curve = BRepAdaptor_Curve(edge)
if curve.GetType() != GeomAbs_Line:
continue
props = GProp_GProps()
brepgprop.LinearProperties(edge, props)
start = curve.Value(curve.FirstParameter())
end = curve.Value(curve.LastParameter())
records.append(
{
"edge": topods.Edge(edge),
"length": float(props.Mass()),
"start": (round(start.X(), 6), round(start.Y(), 6), round(start.Z(), 6)),
"end": (round(end.X(), 6), round(end.Y(), 6), round(end.Z(), 6)),
}
)
records.sort(key=lambda item: (float(item["length"]), item["start"], item["end"]))
return records
def _write_filleted_box_model(path: Path, radius: float) -> None:
shape = BRepPrimAPI_MakeBox(20.0, 14.0, 10.0).Shape()
maker = BRepFilletAPI_MakeFillet(shape)
@@ -49,6 +71,27 @@ def _write_filleted_box_model(path: Path, radius: float) -> None:
_write_step(result, path)
def _write_chained_filleted_box_model(path: Path, radius: float) -> None:
shape = BRepPrimAPI_MakeBox(20.0, 14.0, 10.0).Shape()
records = _line_edge_endpoint_records(shape)
if not records:
raise SystemExit("no line edges found in generated box")
first = records[0]
first_endpoints = {first["start"], first["end"]}
second = None
for candidate in records[1:]:
if candidate["start"] in first_endpoints or candidate["end"] in first_endpoints:
second = candidate
break
if second is None:
raise SystemExit("no adjacent line Edge pair found for chained fillet source")
maker = BRepFilletAPI_MakeFillet(shape)
maker.Add(float(radius), first["edge"])
maker.Add(float(radius), second["edge"])
result = _finalize_builder_result(maker, "verify source box fillet chain")
_write_step(result, path)
def _first_editable_line_edge(model: StepModel) -> int:
candidates: list[tuple[float, int]] = []
for edge_id in range(len(model.edges)):
@@ -299,12 +342,138 @@ def _run_existing_fillet_case(source_radius: float, target_radius: float, tolera
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _run_existing_fillet_arc_length_case(
source_radius: float,
target_arc_length: float,
tolerance: float,
) -> None:
with tempfile.TemporaryDirectory(prefix="geom_param_existing_fillet_arc_") as temp_dir:
model_path = Path(temp_dir) / "filleted_box.step"
_write_filleted_box_model(model_path, source_radius)
model = StepModel.load(model_path)
face_id = _first_existing_fillet_face(model, source_radius, tolerance)
source_info = model.feature_info(face_id)
angular_span = float(
source_info.get("existing_fillet_angular_span")
or source_info.get("angular_span")
or 0.0
)
if angular_span <= 1e-6:
raise SystemExit(f"existing fillet has no stable angular span: {source_info}")
target_radius = float(target_arc_length) / angular_span
before = model.stats()
plan = model.existing_fillet_resize_plan(face_id, target_radius)
if plan["status"] == "blocked":
raise SystemExit(f"existing fillet arc-length plan was blocked: {plan['message']}")
_assert_existing_fillet_plan_topology(plan)
result = model.resize_existing_fillet(face_id, target_radius)
after = model.stats()
matches = _cylindrical_faces_near_radius(model, target_radius, tolerance)
if not matches:
raise SystemExit(
f"existing fillet arc-length verification failed: no cylindrical face near radius {target_radius:g}"
)
verified_face_id = matches[0][0]
verified_info = model.feature_info(verified_face_id)
verified_arc = float(
verified_info.get("existing_fillet_arc_length_estimate")
or target_radius * angular_span
)
arc_error = abs(verified_arc - target_arc_length)
if after.solids != before.solids:
raise SystemExit(
f"existing fillet arc-length resize changed solid count: before={before.solids}, after={after.solids}"
)
if arc_error > max(tolerance * 4.0, target_arc_length * 5e-4):
raise SystemExit(
f"existing fillet arc-length verification failed: "
f"target={target_arc_length:g}, value={verified_arc:g}, error={arc_error:g}"
)
if "Existing fillet result check" not in result:
raise SystemExit(f"existing fillet arc-length result did not report result check: {result}")
if "first_level_topology_matched=True" not in result:
raise SystemExit(f"existing fillet arc-length result did not verify first-level topology: {result}")
print("mode=existing_fillet_arc_length")
print(f"face_id={face_id}")
print(f"strategy={plan.get('resize_strategy')}")
print(f"before={before}")
print(f"after={after}")
print(f"source_radius={source_radius:.6f}")
print(f"target_arc_length={target_arc_length:.6f} target_radius={target_radius:.6f}")
print(f"verified_face={verified_face_id} verified_arc_length={verified_arc:.6f} error={arc_error:.6g}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _run_existing_fillet_chain_guard_case(source_radius: float, target_radius: float, tolerance: float) -> None:
with tempfile.TemporaryDirectory(prefix="geom_param_existing_fillet_chain_") as temp_dir:
model_path = Path(temp_dir) / "fillet_chain_box.step"
_write_chained_filleted_box_model(model_path, source_radius)
model = StepModel.load(model_path)
face_id = _first_existing_fillet_face(model, source_radius, tolerance)
feature = model.feature_info(face_id)
chain_face_ids = tuple(feature.get("feature_existing_fillet_chain_face_ids") or ())
chain_adjacent_face_ids = tuple(feature.get("feature_existing_fillet_chain_adjacent_face_ids") or ())
support_face_ids = tuple(feature.get("feature_existing_fillet_support_face_ids") or ())
if feature.get("existing_fillet_status") != "blocked" or feature.get("existing_fillet_risk") != "blocked":
raise SystemExit(f"fillet chain should be blocked at recognition level: {feature}")
recognition_blockers = str(feature.get("recognition_blockers") or "")
recognition_ready_actions = str(feature.get("recognition_ready_actions") or "")
recognition_limited_actions = str(feature.get("recognition_limited_actions") or "")
if feature.get("recognition_decision") != "已阻止" or "圆角链" not in recognition_blockers:
raise SystemExit(f"fillet chain recognition summary should explain the blocker: {feature}")
if "已有圆角半径" in recognition_ready_actions:
raise SystemExit(f"fillet chain should not expose existing fillet radius as ready: {feature}")
if "已有圆角半径" not in recognition_limited_actions:
raise SystemExit(f"fillet chain should list existing fillet radius as a limited action: {feature}")
if len(chain_face_ids) < 2 or not chain_adjacent_face_ids:
raise SystemExit(f"fillet chain should expose connected fillet faces: {feature}")
if set(chain_adjacent_face_ids) & set(support_face_ids):
raise SystemExit(
"connected fillet faces should not be counted as support faces: "
f"chain_adjacent={chain_adjacent_face_ids}, support={support_face_ids}"
)
plan = model.existing_fillet_resize_plan(face_id, target_radius)
message = str(plan.get("message") or "")
blockers = str(plan.get("blockers") or "")
if plan.get("status") != "blocked":
raise SystemExit(f"existing fillet chain resize should be blocked before geometry execution: {plan}")
if "圆角链" not in f"{message} {blockers}" or "暂未实现" not in f"{message} {blockers}":
raise SystemExit(f"fillet chain blocker should explain the unsupported capability: {plan}")
if tuple(plan.get("feature_existing_fillet_chain_face_ids") or ()) != chain_face_ids:
raise SystemExit(f"fillet chain plan should retain chain face ids: {plan}")
scan_candidates = model.editable_feature_candidates(limit=20, detailed=False)
leaked_chain_candidates = [
item
for item in scan_candidates
if item.get("operation_key") == "inspect_existing_fillet" and int(item.get("target_id", -1)) in chain_face_ids
]
if leaked_chain_candidates:
raise SystemExit(f"fillet chain should not be listed as an editable fillet operation: {leaked_chain_candidates}")
print("mode=existing_fillet_chain_guard")
print(f"face_id={face_id}")
print(f"chain_face_ids={chain_face_ids}")
print(f"chain_adjacent_face_ids={chain_adjacent_face_ids}")
print(f"support_face_ids={support_face_ids}")
print(message.encode("ascii", "backslashreplace").decode("ascii"))
def main() -> int:
parser = argparse.ArgumentParser(description="Verify Edge fillet/chamfer and existing fillet resize operations.")
parser.add_argument(
"--mode",
default="all",
choices=["all", "fillet", "chamfer", "asymmetric_chamfer", "distance_angle_chamfer", "existing_fillet"],
choices=[
"all",
"fillet",
"chamfer",
"asymmetric_chamfer",
"distance_angle_chamfer",
"existing_fillet",
"existing_fillet_arc_length",
"existing_fillet_chain_guard",
],
help="Edge rounding/chamfering edit mode to verify.",
)
parser.add_argument("--fillet-radius", type=float, default=1.0)
@@ -315,11 +484,20 @@ def main() -> int:
parser.add_argument("--distance-angle-degrees", type=float, default=45.0)
parser.add_argument("--source-fillet-radius", type=float, default=1.0)
parser.add_argument("--target-fillet-radius", type=float, default=1.5)
parser.add_argument("--target-fillet-arc-length", type=float, default=2.356194490192345)
parser.add_argument("--tolerance", type=float, default=2e-4)
args = parser.parse_args()
modes = (
["fillet", "chamfer", "asymmetric_chamfer", "distance_angle_chamfer", "existing_fillet"]
[
"fillet",
"chamfer",
"asymmetric_chamfer",
"distance_angle_chamfer",
"existing_fillet",
"existing_fillet_arc_length",
"existing_fillet_chain_guard",
]
if args.mode == "all"
else [args.mode]
)
@@ -334,6 +512,14 @@ def main() -> int:
_run_distance_angle_chamfer_case(args.distance_angle_distance, args.distance_angle_degrees)
elif mode == "existing_fillet":
_run_existing_fillet_case(args.source_fillet_radius, args.target_fillet_radius, args.tolerance)
elif mode == "existing_fillet_arc_length":
_run_existing_fillet_arc_length_case(
args.source_fillet_radius,
args.target_fillet_arc_length,
args.tolerance,
)
elif mode == "existing_fillet_chain_guard":
_run_existing_fillet_chain_guard_case(args.source_fillet_radius, args.target_fillet_radius, args.tolerance)
else:
raise SystemExit(f"unsupported mode: {mode}")
return 0