from __future__ import annotations import sys import tempfile from pathlib import Path from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut 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 from step_editor.window_state import WindowStateMixin class _PropertySpecProbe(WindowStateMixin): def __init__(self, face_id: int, info: dict[str, object]) -> None: self.model = object() self.operation_in_progress = False self.scan_in_progress = False self.load_in_progress = False self.selected_face_id = face_id self.selected_edge_id = None self.selected_kind = "face" self.selected_part_id = int(info.get("part_id", 1)) self.selected_solid_id = int(info.get("solid_id", 1)) self.manual_bottom_face_id = None self.manual_slot_pair_face_id = None def _write_cylinder(path: Path) -> None: cylinder = BRepPrimAPI_MakeCylinder(4.0, 8.0).Shape() _write_step(cylinder, path) def _write_hollow_cylinder(path: Path) -> None: outer = BRepPrimAPI_MakeCylinder(8.0, 78.0).Shape() inner = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(0.0, 0.0, -1.0), gp_Dir(0.0, 0.0, 1.0)), 3.0, 80.0, ).Shape() cut = BRepAlgoAPI_Cut(outer, inner) _write_step(_finalize_boolean_result(cut, "verify hollow cylinder cut", use_glue=False), path) def _write_hollow_cylinder_with_side_feature(path: Path) -> None: outer = BRepPrimAPI_MakeCylinder(8.0, 78.0).Shape() inner = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(0.0, 0.0, -1.0), gp_Dir(0.0, 0.0, 1.0)), 3.0, 80.0, ).Shape() hollow = _finalize_boolean_result(BRepAlgoAPI_Cut(outer, inner), "verify complex hollow cylinder cut", use_glue=False) notch = BRepPrimAPI_MakeBox(gp_Pnt(5.0, -3.0, 20.0), gp_Pnt(12.0, 3.0, 35.0)).Shape() cut = BRepAlgoAPI_Cut(hollow, notch) _write_step(_finalize_boolean_result(cut, "verify complex hollow cylinder side notch", use_glue=False), path) def _write_hollow_cylinder_with_top_boundary_feature(path: Path) -> None: outer = BRepPrimAPI_MakeCylinder(8.0, 78.0).Shape() inner = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(0.0, 0.0, -1.0), gp_Dir(0.0, 0.0, 1.0)), 3.0, 80.0, ).Shape() hollow = _finalize_boolean_result( BRepAlgoAPI_Cut(outer, inner), "verify hollow cylinder top-boundary base cut", use_glue=False, ) notch = BRepPrimAPI_MakeBox(gp_Pnt(4.0, -1.0, 60.0), gp_Pnt(5.5, 1.0, 90.0)).Shape() cut = BRepAlgoAPI_Cut(hollow, notch) _write_step(_finalize_boolean_result(cut, "verify hollow cylinder top-boundary notch", use_glue=False), path) def _write_solid_cylinder_with_top_boundary_feature(path: Path) -> None: cylinder = BRepPrimAPI_MakeCylinder(8.0, 78.0).Shape() notch = BRepPrimAPI_MakeBox(gp_Pnt(2.0, -1.0, 60.0), gp_Pnt(4.5, 1.0, 90.0)).Shape() cut = BRepAlgoAPI_Cut(cylinder, notch) _write_step(_finalize_boolean_result(cut, "verify solid cylinder top-boundary notch", use_glue=False), path) def _first_planar_cap_face(model: StepModel) -> int: for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") == "plane": return face_id raise SystemExit("no planar cylinder cap Face was found") def _top_planar_cap_face(model: StepModel) -> int: best: tuple[float, int] | None = None for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") != "plane": continue center = info.get("area_center") or info.get("bbox_center") if not isinstance(center, tuple) or len(center) != 3: continue z_value = float(center[2]) if best is None or z_value > best[0]: best = (z_value, face_id) if best is None: raise SystemExit("no top planar cylinder cap Face was found") return best[1] def _cylinder_radii(model: StepModel) -> list[float]: radii: list[float] = [] for face_id in range(len(model.faces)): info = model.face_info(face_id) if info.get("surface") != "cylinder": continue radius = float(info.get("radius") or 0.0) if radius <= 0.0: continue if not any(abs(radius - existing) <= max(radius, existing, 1.0) * 1e-5 for existing in radii): radii.append(radius) return sorted(radii) def _assert_blocked(plan: dict[str, object], label: str) -> None: if plan.get("status") != "blocked": raise SystemExit(f"{label} should be blocked for a planar Face on a curved Solid: {plan}") message = str(plan.get("message") or plan.get("blockers") or "") if "曲面" not in message: raise SystemExit(f"{label} should explain that the owning Solid contains curved faces: {plan}") def _specs(face_id: int, info: dict[str, object]) -> list[dict[str, object]]: probe = _PropertySpecProbe(face_id, info) specs, _used = probe._editable_property_specs(info) return specs def _spec(specs: list[dict[str, object]], key: str) -> dict[str, object]: for item in specs: if item.get("key") == key: return item raise SystemExit(f"{key} spec was not found") def _scope_mode(specs: list[dict[str, object]], key: str, mode: str) -> dict[str, object]: spec = _spec(specs, key) modes = spec.get("scope_modes") if not isinstance(modes, dict) or mode not in modes: raise SystemExit(f"{key} has no scope mode {mode}") selected = modes[mode] if not isinstance(selected, dict): raise SystemExit(f"{key} scope mode {mode} is invalid") return selected def _assert_local_scope_explains_curved_owner(specs: list[dict[str, object]], key: str) -> None: local_mode = _scope_mode(specs, key, "local") if bool(local_mode.get("enabled", True)): raise SystemExit(f"{key}/local should be disabled for a planar Face on a curved Solid") disabled_tip = str(local_mode.get("disabled_tip") or "") if "曲面" not in disabled_tip: raise SystemExit(f"{key}/local disabled tip should mention curved owner: {disabled_tip}") def _assert_keep_relations_blocked(plan: dict[str, object], label: str, *expected_fragments: str) -> None: if plan.get("status") != "blocked": raise SystemExit(f"{label} should be blocked before execution: {plan}") if plan.get("face_push_pull_planar_relation_constraint_requested") is not True: raise SystemExit(f"{label} should record the requested keep-relation constraint: {plan}") if plan.get("face_push_pull_planar_constraint_status") != "blocked": raise SystemExit(f"{label} should expose blocked keep-relation constraint status: {plan}") message = str(plan.get("message") or "") + " " + str(plan.get("blockers") or "") for fragment in expected_fragments: if fragment not in message: raise SystemExit(f"{label} blocker should mention {fragment!r}: {plan}") def _bbox_height(model: StepModel) -> float: info = model.part_info(1) bbox_size = info.get("bbox_size") if not isinstance(bbox_size, tuple) or len(bbox_size) != 3: raise SystemExit(f"part bbox_size is missing: {info}") return float(bbox_size[2]) def main() -> int: with tempfile.TemporaryDirectory(prefix="geom_param_face_mixed_surface_") as temp_dir: path = Path(temp_dir) / "cylinder.step" _write_cylinder(path) model = StepModel.load(path) face_id = _first_planar_cap_face(model) info = model.face_info(face_id) if bool(info.get("local_face_deform_ready", True)): raise SystemExit(f"planar cylinder cap should not allow local Face deformation: {info}") blocker = str(info.get("local_face_deform_blocker") or "") if "曲面" not in blocker: raise SystemExit(f"planar cylinder cap blocker should mention curved owner: {info}") center = info.get("area_center") or info.get("bbox_center") if not isinstance(center, tuple) or len(center) != 3: raise SystemExit(f"Face {face_id} does not expose a stable center") target_center = (float(center[0]), float(center[1]), float(center[2]) + 1.0) area = float(info.get("area") or 0.0) if area <= 0: raise SystemExit(f"Face {face_id} does not expose a stable area") _assert_blocked(model.face_center_local_move_plan(face_id, target_center), "Face center local move") _assert_blocked(model.face_area_local_resize_plan(face_id, area * 1.1), "Face area local resize") _assert_blocked(model.face_plane_offset_local_plan(face_id, 1.0), "Face plane offset local move") _assert_keep_relations_blocked( model.push_pull_keep_relations_plan(face_id, 1.0), "planar cylinder cap keep-relations push/pull", "非平面", "平面邻域", ) specs = _specs(face_id, info) semantics = _spec(specs, "face_edit_semantics") if "不能局部重建" not in str(semantics.get("current_text") or ""): raise SystemExit(f"Face edit semantics should summarize the local blocker: {semantics}") if "曲面" not in str(semantics.get("disabled_tip") or ""): raise SystemExit(f"Face edit semantics tip should include the curved-owner blocker: {semantics}") for key in ("face_center_position", "face_target_normal_position"): _assert_local_scope_explains_curved_owner(specs, key) push_pull_mode = _scope_mode(specs, "face_target_normal_position", "push_pull") if not bool(push_pull_mode.get("enabled", False)): raise SystemExit("planar cylinder cap push/pull scope should remain available") keep_relations_mode = _scope_mode(specs, "face_target_normal_position", "keep_relations") if bool(keep_relations_mode.get("enabled", True)): raise SystemExit(f"planar cylinder cap keep-relations scope should be disabled: {keep_relations_mode}") keep_relations_tip = str(keep_relations_mode.get("disabled_tip") or "") if "非平面" not in keep_relations_tip and "曲面" not in keep_relations_tip: raise SystemExit( "planar cylinder cap keep-relations disabled tip should mention non-planar adjacency: " f"{keep_relations_tip}" ) huge_push_plan = model.push_pull_plan(face_id, 50.0) if huge_push_plan.get("status") == "blocked": raise SystemExit(f"huge simple cylinder cap push/pull should use analytic rebuild: {huge_push_plan}") old_height = float(huge_push_plan.get("cylindrical_cap_extension_old_height") or 0.0) new_height = float(huge_push_plan.get("cylindrical_cap_extension_new_height") or 0.0) if old_height <= 0.0 or new_height <= old_height * 3.0: raise SystemExit(f"huge cap push/pull should expose the large height growth: {huge_push_plan}") if huge_push_plan.get("cylindrical_cap_extension_kind") != "solid-cylinder": raise SystemExit(f"huge simple cylinder cap push/pull should expose solid-cylinder kind: {huge_push_plan}") huge_message = str(huge_push_plan.get("message") or "") if "解析" not in huge_message and "analytic" not in huge_message.lower(): raise SystemExit(f"huge cap push/pull should explain analytic rebuild: {huge_push_plan}") large_model = StepModel.load(path) large_face_id = _first_planar_cap_face(large_model) large_result = large_model.push_pull_face(large_face_id, 50.0) if "analytic rebuild" not in large_result: raise SystemExit(f"large simple cylinder cap push/pull should use analytic rebuild: {large_result}") if abs(_bbox_height(large_model) - 58.0) > 1e-4: raise SystemExit(f"large simple cylinder cap push/pull should rebuild height to 58: {_bbox_height(large_model)}") shrink_model = StepModel.load(path) shrink_face_id = _first_planar_cap_face(shrink_model) shrink_plan = shrink_model.push_pull_plan(shrink_face_id, -3.0) if shrink_plan.get("status") == "blocked": raise SystemExit(f"simple cylinder cap inward push/pull should use analytic shrink: {shrink_plan}") if shrink_plan.get("cylindrical_cap_operation") != "retract": raise SystemExit(f"simple cylinder cap inward push/pull should expose retract operation: {shrink_plan}") shrink_result = shrink_model.push_pull_face(shrink_face_id, -3.0) if "analytic rebuild" not in shrink_result or "cap_operation=retract" not in shrink_result: raise SystemExit(f"simple cylinder cap inward push/pull should use analytic retract: {shrink_result}") if abs(_bbox_height(shrink_model) - 5.0) > 1e-4: raise SystemExit(f"simple cylinder cap inward push/pull should shrink height to 5: {_bbox_height(shrink_model)}") before_height = _bbox_height(model) push_plan = model.push_pull_plan(face_id, 1.0) if push_plan.get("status") == "blocked": raise SystemExit(f"planar cylinder cap push/pull should remain available: {push_plan}") push_result = model.push_pull_face(face_id, 1.0) stats = model.stats() if stats.solids != 1: raise SystemExit(f"planar cylinder cap push/pull should keep one solid: {stats}") after_height = _bbox_height(model) if after_height <= before_height: raise SystemExit(f"planar cylinder cap push/pull should increase bbox height: {before_height} -> {after_height}") print( "mixed-surface planar Face guard ok: " f"face_id={face_id}, blocker={blocker}, height={before_height:g}->{after_height:g}, " f"push_pull={push_result}" ) hollow_path = Path(temp_dir) / "hollow_cylinder.step" _write_hollow_cylinder(hollow_path) hollow = StepModel.load(hollow_path) hollow_face_id = _top_planar_cap_face(hollow) hollow_plan = hollow.push_pull_plan(hollow_face_id, 89.0) if hollow_plan.get("status") == "blocked": raise SystemExit(f"hollow cylinder cap push/pull should not be blocked: {hollow_plan}") if hollow_plan.get("cylindrical_cap_extension_kind") != "coaxial-tube": raise SystemExit(f"hollow cylinder cap should be recognized as coaxial-tube: {hollow_plan}") if abs(float(hollow_plan.get("cylindrical_cap_extension_inner_radius") or 0.0) - 3.0) > 1e-4: raise SystemExit(f"hollow cylinder cap should preserve inner radius in plan: {hollow_plan}") hollow_result = hollow.push_pull_face(hollow_face_id, 89.0) if "analytic rebuild" not in hollow_result or "coaxial-tube" not in hollow_result: raise SystemExit(f"hollow cylinder cap push/pull should use tube analytic rebuild: {hollow_result}") if abs(_bbox_height(hollow) - 167.0) > 1e-4: raise SystemExit(f"hollow cylinder cap push/pull should rebuild height to 167: {_bbox_height(hollow)}") radii = _cylinder_radii(hollow) if len(radii) != 2 or abs(radii[0] - 3.0) > 1e-4 or abs(radii[1] - 8.0) > 1e-4: raise SystemExit(f"hollow cylinder cap push/pull should preserve inner/outer cylindrical walls: {radii}") print(f"hollow cylinder cap analytic push/pull ok: radii={radii}, result={hollow_result}") hollow_shrink = StepModel.load(hollow_path) hollow_shrink_face_id = _top_planar_cap_face(hollow_shrink) hollow_shrink_plan = hollow_shrink.push_pull_plan(hollow_shrink_face_id, -39.0) if hollow_shrink_plan.get("status") == "blocked": raise SystemExit(f"hollow cylinder cap inward push/pull should not be blocked: {hollow_shrink_plan}") if hollow_shrink_plan.get("cylindrical_cap_operation") != "retract": raise SystemExit(f"hollow cylinder cap inward push/pull should expose retract operation: {hollow_shrink_plan}") hollow_shrink_result = hollow_shrink.push_pull_face(hollow_shrink_face_id, -39.0) if "analytic rebuild" not in hollow_shrink_result or "coaxial-tube" not in hollow_shrink_result: raise SystemExit(f"hollow cylinder cap inward push/pull should use tube analytic shrink: {hollow_shrink_result}") if abs(_bbox_height(hollow_shrink) - 39.0) > 1e-4: raise SystemExit(f"hollow cylinder cap inward push/pull should shrink height to 39: {_bbox_height(hollow_shrink)}") shrink_radii = _cylinder_radii(hollow_shrink) if len(shrink_radii) != 2 or abs(shrink_radii[0] - 3.0) > 1e-4 or abs(shrink_radii[1] - 8.0) > 1e-4: raise SystemExit(f"hollow cylinder cap inward push/pull should preserve radii: {shrink_radii}") print(f"hollow cylinder cap analytic retraction ok: radii={shrink_radii}, result={hollow_shrink_result}") complex_hollow_path = Path(temp_dir) / "hollow_cylinder_with_side_feature.step" _write_hollow_cylinder_with_side_feature(complex_hollow_path) complex_hollow = StepModel.load(complex_hollow_path) complex_hollow_face_id = _top_planar_cap_face(complex_hollow) complex_plan = complex_hollow.push_pull_plan(complex_hollow_face_id, 89.0) if complex_plan.get("status") == "blocked": raise SystemExit(f"complex hollow cylinder cap push/pull should not be blocked: {complex_plan}") if complex_plan.get("cylindrical_cap_extension_kind") != "coaxial-tube": raise SystemExit(f"complex hollow cylinder cap should be recognized as coaxial-tube: {complex_plan}") if not complex_plan.get("cylindrical_cap_extension_uses_local_segment"): raise SystemExit(f"complex hollow cylinder cap should expose local extension segment: {complex_plan}") complex_result = complex_hollow.push_pull_face(complex_hollow_face_id, 89.0) if "local extension" not in complex_result or "coaxial-tube" not in complex_result: raise SystemExit(f"complex hollow cylinder cap should use local extension segment: {complex_result}") if abs(_bbox_height(complex_hollow) - 167.0) > 1e-4: raise SystemExit( f"complex hollow cylinder cap push/pull should extend height to 167: {_bbox_height(complex_hollow)}" ) complex_radii = _cylinder_radii(complex_hollow) if len(complex_radii) < 2 or abs(complex_radii[0] - 3.0) > 1e-4 or abs(complex_radii[-1] - 8.0) > 1e-4: raise SystemExit( f"complex hollow cylinder cap should preserve inner/outer cylindrical walls: {complex_radii}" ) print( "complex hollow cylinder cap local extension ok: " f"radii={complex_radii}, result={complex_result}" ) complex_shrink = StepModel.load(complex_hollow_path) complex_shrink_face_id = _top_planar_cap_face(complex_shrink) complex_shrink_plan = complex_shrink.push_pull_plan(complex_shrink_face_id, -20.0) if complex_shrink_plan.get("status") == "blocked": raise SystemExit(f"complex hollow cylinder cap inward push/pull should not be blocked: {complex_shrink_plan}") if complex_shrink_plan.get("cylindrical_cap_operation") != "retract": raise SystemExit(f"complex hollow cylinder cap inward push/pull should expose retract: {complex_shrink_plan}") complex_shrink_result = complex_shrink.push_pull_face(complex_shrink_face_id, -20.0) if "local retraction" not in complex_shrink_result or "coaxial-tube" not in complex_shrink_result: raise SystemExit(f"complex hollow cylinder cap inward push/pull should use local retraction: {complex_shrink_result}") if abs(_bbox_height(complex_shrink) - 58.0) > 1e-4: raise SystemExit( f"complex hollow cylinder cap inward push/pull should shrink height to 58: {_bbox_height(complex_shrink)}" ) complex_shrink_radii = _cylinder_radii(complex_shrink) if ( len(complex_shrink_radii) < 2 or abs(complex_shrink_radii[0] - 3.0) > 1e-4 or abs(complex_shrink_radii[-1] - 8.0) > 1e-4 ): raise SystemExit( f"complex hollow cylinder cap inward push/pull should preserve radii: {complex_shrink_radii}" ) print( "complex hollow cylinder cap local retraction ok: " f"radii={complex_shrink_radii}, result={complex_shrink_result}" ) top_boundary_path = Path(temp_dir) / "hollow_cylinder_with_top_boundary_feature.step" _write_hollow_cylinder_with_top_boundary_feature(top_boundary_path) top_boundary = StepModel.load(top_boundary_path) top_boundary_face_id = _top_planar_cap_face(top_boundary) top_boundary_plan = top_boundary.push_pull_plan(top_boundary_face_id, 89.0) if top_boundary_plan.get("status") == "blocked": raise SystemExit( f"top-boundary hollow cylinder cap push/pull should preserve extra first-level openings: {top_boundary_plan}" ) if top_boundary_plan.get("cylindrical_cap_extension_kind") != "coaxial-tube": raise SystemExit( f"top-boundary hollow cylinder cap should be recognized as coaxial-tube: {top_boundary_plan}" ) if top_boundary_plan.get("cylindrical_cap_extension_method") != "cap-profile-prism": raise SystemExit( f"top-boundary hollow cylinder cap should use cap profile prism method: {top_boundary_plan}" ) if int(top_boundary_plan.get("cap_extra_adjacent_face_count") or 0) <= 0: raise SystemExit( f"top-boundary hollow cylinder cap should expose extra adjacent faces: {top_boundary_plan}" ) top_boundary_message = str(top_boundary_plan.get("message") or "") if "真实轮廓拉伸" not in top_boundary_message: raise SystemExit( f"top-boundary hollow cylinder cap should explain profile-prism extension: {top_boundary_plan}" ) try: top_boundary.push_pull_face(top_boundary_face_id, 89.0) except ValueError as exc: blocker_text = str(exc) if "解析重建" not in blocker_text and "OCCT" not in blocker_text: raise SystemExit( "top-boundary hollow cylinder large cap push/pull should explain the fast blocker: " f"{blocker_text}" ) else: raise SystemExit( "top-boundary hollow cylinder large cap push/pull should be blocked instead of entering slow Boolean." ) print( "top-boundary hollow cylinder cap large profile-prism blocker ok: " f"extra_adjacent={top_boundary_plan.get('cap_extra_adjacent_face_count')}, " f"distance=89" ) top_boundary_shallow_cut = StepModel.load(top_boundary_path) shallow_face_id = _top_planar_cap_face(top_boundary_shallow_cut) shallow_plan = top_boundary_shallow_cut.push_pull_plan(shallow_face_id, -10.0) if shallow_plan.get("status") == "blocked": raise SystemExit( f"top-boundary hollow cylinder shallow inward push/pull should stay available: {shallow_plan}" ) if shallow_plan.get("cylindrical_cap_extension_method") != "cap-profile-prism": raise SystemExit( f"top-boundary hollow cylinder shallow inward push/pull should use cap profile prism: {shallow_plan}" ) shallow_result = top_boundary_shallow_cut.push_pull_face(shallow_face_id, -10.0) if "profile-prism retraction" not in shallow_result or "coaxial-tube" not in shallow_result: raise SystemExit( f"top-boundary hollow cylinder shallow inward push/pull should use profile-prism retraction: {shallow_result}" ) if abs(_bbox_height(top_boundary_shallow_cut) - 68.0) > 1e-4: raise SystemExit( "top-boundary hollow cylinder shallow inward push/pull should lower height to 68: " f"{_bbox_height(top_boundary_shallow_cut)}" ) shallow_top_face = _top_planar_cap_face(top_boundary_shallow_cut) shallow_info = top_boundary_shallow_cut.face_info(shallow_top_face) if int(shallow_info.get("boundary_wires") or 0) < 3: raise SystemExit( "top-boundary hollow cylinder shallow inward push/pull should preserve openings: " f"face_id={shallow_top_face}, info={shallow_info}" ) print( "top-boundary hollow cylinder cap profile-prism shallow retraction ok: " f"result={shallow_result}" ) top_boundary_deep_cut = StepModel.load(top_boundary_path) deep_face_id = _top_planar_cap_face(top_boundary_deep_cut) deep_plan = top_boundary_deep_cut.push_pull_plan(deep_face_id, -20.0) if deep_plan.get("status") != "blocked": raise SystemExit( f"top-boundary hollow cylinder deep inward push/pull should be blocked: {deep_plan}" ) deep_message = str(deep_plan.get("message") or "") if "越过" not in deep_message or "内侧终点" not in deep_message: raise SystemExit( f"top-boundary hollow cylinder deep inward blocker should explain the retract limit: {deep_plan}" ) print( "top-boundary hollow cylinder cap deep retraction blocker ok: " f"message={deep_message}" ) solid_top_boundary_path = Path(temp_dir) / "solid_cylinder_with_top_boundary_feature.step" _write_solid_cylinder_with_top_boundary_feature(solid_top_boundary_path) solid_top_boundary = StepModel.load(solid_top_boundary_path) solid_top_face_id = _top_planar_cap_face(solid_top_boundary) solid_extend_plan = solid_top_boundary.push_pull_plan(solid_top_face_id, 20.0) if solid_extend_plan.get("status") == "blocked": raise SystemExit( f"solid top-boundary cylinder cap outward push/pull should stay available: {solid_extend_plan}" ) if solid_extend_plan.get("cylindrical_cap_extension_kind") != "solid-cylinder": raise SystemExit( f"solid top-boundary cylinder cap should be recognized as solid-cylinder: {solid_extend_plan}" ) if solid_extend_plan.get("cylindrical_cap_extension_method") != "cap-profile-prism": raise SystemExit( f"solid top-boundary cylinder cap should use profile prism extension: {solid_extend_plan}" ) solid_extend_result = solid_top_boundary.push_pull_face(solid_top_face_id, 20.0) if "profile-prism extension" not in solid_extend_result or "solid-cylinder" not in solid_extend_result: raise SystemExit( f"solid top-boundary cylinder cap should use profile-prism extension: {solid_extend_result}" ) if abs(_bbox_height(solid_top_boundary) - 98.0) > 1e-4: raise SystemExit( f"solid top-boundary cylinder cap outward push/pull should extend height to 98: {_bbox_height(solid_top_boundary)}" ) solid_extended_top = _top_planar_cap_face(solid_top_boundary) solid_extended_info = solid_top_boundary.face_info(solid_extended_top) if int(solid_extended_info.get("boundary_wires") or 0) < 2: raise SystemExit( "solid top-boundary cylinder cap outward push/pull should preserve the notch opening: " f"face_id={solid_extended_top}, info={solid_extended_info}" ) print( "solid top-boundary cylinder cap profile-prism extension ok: " f"result={solid_extend_result}" ) solid_shallow_cut = StepModel.load(solid_top_boundary_path) solid_shallow_face_id = _top_planar_cap_face(solid_shallow_cut) solid_shallow_plan = solid_shallow_cut.push_pull_plan(solid_shallow_face_id, -10.0) if solid_shallow_plan.get("status") == "blocked": raise SystemExit( f"solid top-boundary cylinder shallow inward push/pull should stay available: {solid_shallow_plan}" ) if solid_shallow_plan.get("cylindrical_cap_extension_method") != "cap-profile-prism": raise SystemExit( f"solid top-boundary cylinder shallow inward push/pull should use profile prism: {solid_shallow_plan}" ) solid_shallow_result = solid_shallow_cut.push_pull_face(solid_shallow_face_id, -10.0) if "profile-prism retraction" not in solid_shallow_result or "solid-cylinder" not in solid_shallow_result: raise SystemExit( f"solid top-boundary cylinder shallow inward push/pull should use profile-prism retraction: {solid_shallow_result}" ) if abs(_bbox_height(solid_shallow_cut) - 68.0) > 1e-4: raise SystemExit( f"solid top-boundary cylinder shallow inward push/pull should lower height to 68: {_bbox_height(solid_shallow_cut)}" ) print( "solid top-boundary cylinder cap profile-prism shallow retraction ok: " f"result={solid_shallow_result}" ) solid_deep_cut = StepModel.load(solid_top_boundary_path) solid_deep_face_id = _top_planar_cap_face(solid_deep_cut) solid_deep_plan = solid_deep_cut.push_pull_plan(solid_deep_face_id, -20.0) if solid_deep_plan.get("status") != "blocked": raise SystemExit( f"solid top-boundary cylinder deep inward push/pull should be blocked: {solid_deep_plan}" ) solid_deep_message = str(solid_deep_plan.get("message") or "") if "越过" not in solid_deep_message or "内侧终点" not in solid_deep_message: raise SystemExit( f"solid top-boundary cylinder deep inward blocker should explain the retract limit: {solid_deep_plan}" ) print( "solid top-boundary cylinder cap deep retraction blocker ok: " f"message={solid_deep_message}" ) return 0 if __name__ == "__main__": raise SystemExit(main())