from __future__ import annotations import json from pathlib import Path import sys import tempfile 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.isolated_edit_worker import run_request from step_editor.model import StepModel from step_editor.window_core import WindowCoreMixin from step_editor.window_state import WindowStateMixin MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "ICEPAK-NATURAL.stp" class _SelectionHarness(WindowCoreMixin, WindowStateMixin): def __init__(self, model: StepModel) -> None: self.model = model self.feature_detection_level = "current-only" self.operation_in_progress = False self.scan_in_progress = False self.load_in_progress = False self.selected_face_id = None self.selected_edge_id = None self.selected_kind = None self.selected_part_id = None self.selected_solid_id = None self.manual_bottom_face_id = None self.manual_slot_pair_face_id = None self.multi_selected_feature_face_ids = [] self.multi_selected_hole_entries = [] self.multi_selection_active = False self.current_info_values = {} def _current_feature_detection_level(self) -> str: return str(self.feature_detection_level) def select_feature_face(self, face_id: int) -> None: self.selected_face_id = face_id self.selected_edge_id = None self.selected_kind = "feature" self.selected_part_id = self.model.face_part_ids[face_id] self.selected_solid_id = self.model.face_solid_ids[face_id] def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def _isolated_hole_resize(model: StepModel, face_id: int, diameter: float, root: Path) -> str: input_path = root / "face87_input.brep" output_path = root / "face87_output.brep" request_path = root / "face87_resize_request.json" model.export_internal_brep(input_path) request_path.write_text( json.dumps( { "input_path": str(input_path), "output_path": str(output_path), "input_format": "brep", "output_format": "brep", "operation": "resize_cylindrical_hole", "args": [face_id, diameter], }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) code = run_request(request_path) response = json.loads(request_path.with_suffix(".response.json").read_text(encoding="utf-8")) _assert(code == 0 and response.get("ok") is True, f"isolated Face {face_id} resize should pass: {response}") _assert(output_path.exists(), f"isolated Face {face_id} resize should write output BREP") return str(response.get("message") or "") def _isolated_hole_axis_move( model: StepModel, face_id: int, target_center: tuple[float, float, float], root: Path, ) -> str: input_path = root / "face85_axis_input.brep" output_path = root / "face85_axis_output.brep" request_path = root / "face85_axis_request.json" model.export_internal_brep(input_path) request_path.write_text( json.dumps( { "input_path": str(input_path), "output_path": str(output_path), "input_format": "brep", "output_format": "brep", "operation": "move_cylindrical_hole_axis", "args": [face_id, list(target_center)], }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) code = run_request(request_path) response = json.loads(request_path.with_suffix(".response.json").read_text(encoding="utf-8")) _assert(code == 0 and response.get("ok") is True, f"isolated Face {face_id} axis move should pass: {response}") _assert(output_path.exists(), f"isolated Face {face_id} axis move should write output BREP") return str(response.get("message") or "") def _edge_ids_from_polydata(polydata) -> set[int]: edge_arr = polydata.GetCellData().GetArray("edge_id") if polydata is not None else None if edge_arr is None: return set() return {int(edge_arr.GetValue(index)) for index in range(edge_arr.GetNumberOfTuples())} def _polydata_edge_sample_counts(polydata) -> dict[int, list[int]]: edge_arr = polydata.GetCellData().GetArray("edge_id") if polydata is not None else None if edge_arr is None: return {} counts: dict[int, list[int]] = {} for cell_id in range(polydata.GetNumberOfCells()): edge_id = int(edge_arr.GetValue(cell_id)) counts.setdefault(edge_id, []).append(int(polydata.GetCell(cell_id).GetNumberOfPoints())) return counts def _assert_default_edges_hide_same_domain_internal_edges(model: StepModel, label: str) -> None: visible_edge_ids = _edge_ids_from_polydata(model.build_edge_polydata(show_same_domain_internal_edges=False)) hidden_internal_ids = set(model._same_domain_internal_edge_ids()) # noqa: SLF001 leaked = sorted(visible_edge_ids & hidden_internal_ids) _assert(not leaked, f"{label} default edge display should hide same-domain/internal duplicate edges: {leaked[:12]}") def _assert_small_circle_edges_are_smooth(model: StepModel, label: str) -> None: edge_polydata = model.build_edge_polydata(deflection=1.6, show_same_domain_internal_edges=False) sample_counts = _polydata_edge_sample_counts(edge_polydata) rough_edges: list[tuple[int, int, float]] = [] for edge_id, counts in sample_counts.items(): info = model.edge_info(edge_id) if info.get("curve") != "circle": continue radius = float(info.get("radius") or 0.0) if radius <= 0.0 or radius > 0.35: continue point_count = max(counts or [0]) if point_count < 9: rough_edges.append((edge_id, point_count, radius)) _assert(not rough_edges, f"{label} small circular display edges should not be drawn as triangles: {rough_edges[:12]}") def _assert_rectangular_face_parameters(model: StepModel, harness: _SelectionHarness) -> None: face_id = 9 info = model.feature_info(face_id) _assert(info.get("surface") == "plane", "ICEPAK Face 9 should be a planar rectangular face") _assert(info.get("prismatic_profile_status") == "candidate", "ICEPAK Face 9 should be a rectangular profile") _assert(bool(info.get("local_face_size_edit_ready")), f"ICEPAK Face 9 size parameters should be visible: {info}") width = float(info.get("local_face_width") or 0.0) height = float(info.get("local_face_height") or 0.0) _assert(abs(width - 8.0) <= 1e-7, f"ICEPAK Face 9 length should be read from rectangle edges, got {width:g}") _assert(abs(height - 4.9) <= 1e-7, f"ICEPAK Face 9 width should be read from rectangle edges, got {height:g}") harness.select_feature_face(face_id) specs = harness._property_editor_specs(info, info) editable_keys = {str(spec.get("key") or "") for spec in specs if bool(spec.get("editable"))} expected = {"local_face_width", "local_face_height", "face_target_normal_position"} missing = sorted(expected - editable_keys) _assert(not missing, f"ICEPAK Face 9 feature parameters should expose length, width and offset: {missing}") def _assert_face85_hole_axis_move(model: StepModel, harness: _SelectionHarness) -> tuple[float, float, float]: face_id = 85 info = model.quick_face_info(face_id) _assert(info.get("surface") == "cylinder", "ICEPAK Face 85 should be cylindrical") _assert( list(info.get("feature_highlight_face_ids") or []) == [85, 96], f"ICEPAK Face 85 should highlight the complete split hole region: {info.get('feature_highlight_face_ids')}", ) harness.select_feature_face(face_id) feature = model.feature_info(face_id) specs = harness._property_editor_specs(feature, feature) editable_keys = {str(spec.get("key") or "") for spec in specs if bool(spec.get("editable"))} _assert("hole_axis_center" in editable_keys, "ICEPAK Face 85 should expose the hole axis-center move parameter") current_center = info.get("axis_center") or feature.get("axis_center") if not (isinstance(current_center, (list, tuple)) and len(current_center) == 3): axis_point = feature.get("axis_point") or info.get("axis_point") axis_direction = feature.get("axis") or info.get("axis") axis_range = feature.get("same_domain_v_range") or info.get("same_domain_v_range") or feature.get("v_range") if ( isinstance(axis_point, (list, tuple)) and len(axis_point) == 3 and isinstance(axis_direction, (list, tuple)) and len(axis_direction) == 3 and isinstance(axis_range, (list, tuple)) and len(axis_range) >= 2 ): axis_mid = (float(axis_range[0]) + float(axis_range[1])) * 0.5 current_center = tuple(float(axis_point[index]) + float(axis_direction[index]) * axis_mid for index in range(3)) _assert(isinstance(current_center, (list, tuple)) and len(current_center) == 3, "Face 85 should have an axis center") near_target_center = (float(current_center[0]) + 0.1, float(current_center[1]), float(current_center[2])) plan = model.cylindrical_axis_move_plan(face_id, near_target_center) _assert(plan.get("status") != "blocked", f"Face 85 axis move should not be blocked as a half cylinder: {plan}") _assert(list(plan.get("same_domain_face_ids") or []) == [85, 96], "Face 85 axis move should use both side fragments") _assert(float(plan.get("angular_span") or 0.0) > 6.0, "Face 85 axis move should use full same-domain span") _assert( abs(float(plan.get("selected_angular_span") or 0.0) - 3.141592653589793) <= 1e-6, "Face 85 selected fragment span should still be recorded separately", ) target_center = (float(current_center[0]), float(current_center[1]), 6.0) high_risk_plan = model.cylindrical_axis_move_plan(face_id, target_center) _assert( high_risk_plan.get("status") != "blocked", f"Face 85 axis move to Z=6 should be high-risk but still plannable: {high_risk_plan}", ) _assert(high_risk_plan.get("risk") == "high", f"Face 85 far axis move should be classified high risk: {high_risk_plan}") _assert( bool(high_risk_plan.get("supports_isolation")), f"Face 85 high-risk axis move should be routed to isolated execution: {high_risk_plan}", ) _assert( tuple(round(float(item), 7) for item in high_risk_plan.get("target_axis_center", ())) == (0.5, 1.0, 6.0), f"Face 85 high-risk axis move target should match the requested location: {high_risk_plan}", ) return target_center def _assert_face85_face87_multi_hole_specs(model: StepModel, harness: _SelectionHarness) -> None: entry85 = harness._hole_multi_select_entry(85) entry87 = harness._hole_multi_select_entry(87) _assert(entry85 is not None, "Face 85 should be eligible for multi-hole selection") _assert(entry87 is not None, "Face 87 should be eligible for multi-hole selection") _assert(entry85["logical_id"] != entry87["logical_id"], "Face 85 and Face 87 should be separate hole groups") harness.multi_selected_hole_entries = [entry85, entry87] harness.multi_selected_feature_face_ids = [85, 87] harness.multi_selection_active = True harness.selected_kind = "multi_feature" harness.selected_face_id = 87 info = harness._selected_action_info() _assert(info.get("multi_selection_kind") == "holes", f"multi selection info should describe holes: {info}") _assert(int(info.get("multi_selected_count") or 0) == 2, f"multi selection should include two holes: {info}") _assert( set(info.get("feature_highlight_face_ids") or ()) == {85, 96, 87, 94}, f"multi-hole highlight should include both split-cylinder regions: {info.get('feature_highlight_face_ids')}", ) specs = harness._property_editor_specs(info, info) keys = [str(spec.get("key") or "") for spec in specs] _assert( keys == ["multi_hole_diameter", "multi_hole_radius", "multi_hole_position_delta", "multi_hole_suppress"], f"multi-hole specs should expose batch hole dimensions and commands: {keys}", ) actions = {str(spec.get("action") or "") for spec in specs} _assert( actions == {"resize_multi_selected_holes", "move_multi_selected_holes_by_offset", "suppress_multi_selected_holes"}, f"multi-hole specs should expose batch edit actions: {actions}", ) def main() -> int: if not MODEL_PATH.exists(): print("icepak cylindrical region selection skipped: local ICEPAK-NATURAL.stp is not present") return 0 model = StepModel.load(MODEL_PATH) _assert_default_edges_hide_same_domain_internal_edges(model, "ICEPAK before edit") harness = _SelectionHarness(model) _assert_rectangular_face_parameters(model, harness) _assert_face85_hole_axis_move(model, harness) _assert_face85_face87_multi_hole_specs(model, harness) expected_regions = { 85: [85, 96], 96: [85, 96], 87: [87, 94], 94: [87, 94], 84: [84, 97], 97: [84, 97], } for face_id, expected in expected_regions.items(): info = model.quick_face_info(face_id) _assert(info.get("surface") == "cylinder", f"Face {face_id} should be cylindrical") _assert(model.face_region_ids(face_id) == expected, f"Face {face_id} region should be {expected}") _assert( list(info.get("feature_highlight_face_ids") or []) == expected, f"Face {face_id} quick info highlight should include the whole same-domain region: {info.get('feature_highlight_face_ids')}", ) _assert( harness._selection_same_domain_face_ids(face_id) == expected, f"Face {face_id} UI selection highlight should include the whole same-domain region", ) context = harness._feature_context_info(face_id) _assert( list(context.get("feature_highlight_face_ids") or []) == expected, f"Face {face_id} feature context highlight should include the whole same-domain region: {context.get('feature_highlight_face_ids')}", ) feature = model.feature_info(face_id) _assert( list(feature.get("same_domain_face_ids") or []) == expected, f"Face {face_id} feature info should use the whole same-domain region", ) _assert(bool(feature.get("is_full_cylinder")), f"Face {face_id} should be treated as a full cylinder") axis_move_model = StepModel.load(MODEL_PATH) axis_move_target = _assert_face85_hole_axis_move(axis_move_model, _SelectionHarness(axis_move_model)) axis_move_result = axis_move_model.move_cylindrical_hole_axis(85, axis_move_target) _assert( "Cylindrical hole axis move completed" in axis_move_result, f"Face 85 axis move should complete: {axis_move_result}", ) _assert_default_edges_hide_same_domain_internal_edges(axis_move_model, "ICEPAK after Face 85 axis move") resize_model = StepModel.load(MODEL_PATH) plan = resize_model.cylindrical_resize_plan(87, 0.3) _assert(plan.get("feature_type") == "圆柱孔候选", "Face 87 should plan as a cylindrical hole") _assert(list(plan.get("same_domain_face_ids") or []) == [87, 94], "Face 87 resize plan should use both side fragments") _assert(bool(plan.get("is_full_cylinder")), "Face 87 resize plan should preserve full-cylinder semantics") _assert(float(plan.get("angular_span") or 0.0) > 6.0, "Face 87 resize plan should not use the selected half-face span") result = resize_model.resize_cylindrical_hole(87, 0.3) _assert("diameter 0.5 -> 0.3" in result, "Face 87 diameter shrink should complete") _assert_default_edges_hide_same_domain_internal_edges(resize_model, "ICEPAK after Face 87 shrink") _assert_small_circle_edges_are_smooth(resize_model, "ICEPAK after Face 87 shrink") with tempfile.TemporaryDirectory(prefix="icepak_face87_isolated_") as temp_dir: worker_message = _isolated_hole_resize(model, 87, 0.3, Path(temp_dir)) _assert("diameter 0.5 -> 0.3" in worker_message, "Face 87 isolated diameter shrink should complete") with tempfile.TemporaryDirectory(prefix="icepak_face85_axis_isolated_") as temp_dir: worker_message = _isolated_hole_axis_move(model, 85, axis_move_target, Path(temp_dir)) _assert("Cylindrical hole axis move completed" in worker_message, "Face 85 isolated axis move should complete") print("icepak cylindrical region selection ok") return 0 if __name__ == "__main__": raise SystemExit(main())