from __future__ import annotations import json from pathlib import Path import subprocess 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)) SCRIPTS_ROOT = PROJECT_ROOT / "scripts" if str(SCRIPTS_ROOT) not in sys.path: sys.path.insert(0, str(SCRIPTS_ROOT)) from step_editor.model import StepModel from verify_hole_resize import ( _axis_center, _depth, _first_hole_face, _hole_face_ids, _nearest_blind_depth, _nearest_hole_by_diameter, _volume, _write_blind_hole_model, _write_through_hole_model, ) from verify_slot_resize import ( _first_slot_face, _nearest_axis_center, _nearest_metric, _nearest_obround_axis_pair, _nearest_obround_total_length, _slot_axis_center, _slot_metric, _write_half_round_slot_model, _write_obround_slot_model, ) def _run_worker(request_path: Path) -> dict[str, object]: completed = subprocess.run( [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)], cwd=PROJECT_ROOT, text=True, capture_output=True, check=False, ) response_path = request_path.with_suffix(".response.json") if completed.returncode != 0: stderr = completed.stderr.strip() response = response_path.read_text(encoding="utf-8-sig") if response_path.exists() else "" raise SystemExit( f"isolated worker failed: returncode={completed.returncode}, stderr={stderr}, response={response}" ) if not response_path.exists(): raise SystemExit("isolated worker did not write a response JSON file") response = json.loads(response_path.read_text(encoding="utf-8-sig")) if not response.get("ok"): raise SystemExit(f"isolated worker returned an error response: {response}") return response def _run_worker_allow_failure(request_path: Path) -> tuple[int, dict[str, object]]: completed = subprocess.run( [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)], cwd=PROJECT_ROOT, text=True, capture_output=True, check=False, ) response_path = request_path.with_suffix(".response.json") if not response_path.exists(): raise SystemExit( f"isolated worker did not write a response JSON file; " f"returncode={completed.returncode}, stderr={completed.stderr.strip()}" ) response = json.loads(response_path.read_text(encoding="utf-8-sig")) return completed.returncode, response def _write_request( request_path: Path, input_path: Path, output_path: Path, operation: str, args: list[object], ) -> None: request_path.write_text( json.dumps( { "input_path": str(input_path), "output_path": str(output_path), "operation": operation, "args": args, }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) def _assert_one_solid(model: StepModel, label: str) -> None: if model.stats().solids != 1: raise SystemExit(f"{label} should keep one Solid: {model.stats()}") def _logical_region_has_diameter(model: StepModel, logical_id: int, target_diameter: float) -> bool: tolerance = max(abs(target_diameter) * 1e-4, 1e-5) for face_id in model.face_ids_for_logical_id(logical_id): try: diameter = float(model.face_info(face_id).get("diameter")) except (TypeError, ValueError): continue if abs(diameter - target_diameter) <= tolerance: return True return False def _logical_region_has_slot_metric(model: StepModel, logical_id: int, key: str, target_value: float) -> bool: tolerance = max(abs(target_value) * 1e-4, 1e-5) for face_id in model.face_ids_for_logical_id(logical_id): try: feature = model.feature_info(face_id) info = model.face_info(face_id) value = feature.get(key) if value is None: value = info.get(key) value = float(value) except BaseException: continue if abs(value - target_value) <= tolerance: return True return False def _logical_region_has_blind_depth(model: StepModel, logical_id: int, target_depth: float) -> bool: tolerance = max(abs(target_depth) * 1e-4, 1e-5) for face_id in model.face_ids_for_logical_id(logical_id): try: depth = _depth(model, face_id) except BaseException: continue if abs(depth - target_depth) <= tolerance: return True return False def _logical_region_has_hole_axis( model: StepModel, logical_id: int, target_center: tuple[float, float, float], target_diameter: float, ) -> bool: for face_id in model.face_ids_for_logical_id(logical_id): try: center = _axis_center(model, face_id) diameter = float(model.face_info(face_id).get("diameter")) except BaseException: continue center_error = sum((center[index] - target_center[index]) ** 2 for index in range(3)) ** 0.5 diameter_error = abs(diameter - target_diameter) if center_error <= 1e-4 and diameter_error <= max(abs(target_diameter) * 1e-4, 1e-5): return True return False def _logical_region_has_slot_axis( model: StepModel, logical_id: int, target_center: tuple[float, float, float], ) -> bool: for face_id in model.face_ids_for_logical_id(logical_id): try: center = _slot_axis_center(model, face_id) except BaseException: continue center_error = sum((center[index] - target_center[index]) ** 2 for index in range(3)) ** 0.5 if center_error <= 1e-4: return True return False def _logical_region_contains_any(model: StepModel, logical_id: int, face_ids: tuple[int, ...]) -> bool: logical_faces = set(model.face_ids_for_logical_id(logical_id)) return any(face_id in logical_faces for face_id in face_ids) def _window_probe_for(input_path: Path): from step_editor.window_actions import WindowActionMixin class _WindowProbe(WindowActionMixin): def __init__(self) -> None: self.model = StepModel.load(input_path) self.step_path = input_path self.current_info_values = {} self.selected_pick_position = None self.isolated_edit_cancel_requested = False self.active_isolated_edit_process = None return _WindowProbe() def _verify_hole_diameter_isolated(temp_dir: Path) -> None: input_path = temp_dir / "through_hole.step" output_path = temp_dir / "through_hole_out.step" request_path = temp_dir / "hole_request.json" _write_through_hole_model(input_path) model = StepModel.load(input_path) face_id = _first_hole_face(model, blind=False) target_center = _axis_center(model, face_id) target_diameter = 8.0 _write_request(request_path, input_path, output_path, "resize_cylindrical_hole", [face_id, target_diameter]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, diameter, center, error = _nearest_hole_by_diameter( result_model, target_diameter, target_center, blind=False, ) if error > 1e-4 or abs(diameter - target_diameter) > 1e-4: raise SystemExit( f"isolated hole resize verification failed: face={verified_face}, " f"diameter={diameter:g}, center={center}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated hole resize") print(f"isolated hole resize ok: face={verified_face}, diameter={diameter:g}") def _verify_hole_owning_scale_isolated(temp_dir: Path) -> None: input_path = temp_dir / "through_hole_scale.step" output_path = temp_dir / "through_hole_scale_out.step" request_path = temp_dir / "hole_scale_request.json" _write_through_hole_model(input_path) model = StepModel.load(input_path) face_id = _first_hole_face(model, blind=False) target_center = _axis_center(model, face_id) target_diameter = 8.0 _write_request(request_path, input_path, output_path, "resize_cylindrical_owning_scale", [face_id, target_diameter]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, diameter, center, error = _nearest_hole_by_diameter( result_model, target_diameter, target_center, blind=False, ) if error > 1e-4 or abs(diameter - target_diameter) > 1e-4: raise SystemExit( f"isolated hole owning-scale verification failed: face={verified_face}, " f"diameter={diameter:g}, center={center}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated hole owning-scale") print(f"isolated hole owning-scale ok: face={verified_face}, diameter={diameter:g}") def _verify_hole_axis_isolated(temp_dir: Path) -> None: input_path = temp_dir / "through_hole_axis.step" output_path = temp_dir / "through_hole_axis_out.step" request_path = temp_dir / "hole_axis_request.json" _write_through_hole_model(input_path) model = StepModel.load(input_path) face_id = _first_hole_face(model, blind=False) current_center = _axis_center(model, face_id) target_center = (current_center[0] + 2.0, current_center[1], current_center[2]) current_diameter = float(model.face_info(face_id)["diameter"]) _write_request(request_path, input_path, output_path, "move_cylindrical_hole_axis", [face_id, target_center]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, diameter, center, error = _nearest_hole_by_diameter( result_model, current_diameter, target_center, blind=False, ) if error > 1e-4 or abs(diameter - current_diameter) > 1e-4: raise SystemExit( f"isolated hole axis move verification failed: face={verified_face}, " f"diameter={diameter:g}, center={center}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated hole axis move") print(f"isolated hole axis move ok: face={verified_face}, center={center}") def _verify_hole_suppress_isolated(temp_dir: Path) -> None: input_path = temp_dir / "through_hole_suppress.step" output_path = temp_dir / "through_hole_suppress_out.step" request_path = temp_dir / "hole_suppress_request.json" _write_through_hole_model(input_path) model = StepModel.load(input_path) face_id = _first_hole_face(model, blind=False) before_volume = _volume(model) _write_request(request_path, input_path, output_path, "suppress_cylindrical_hole", [face_id]) response = _run_worker(request_path) result_model = StepModel.load(output_path) after_volume = _volume(result_model) remaining_holes = _hole_face_ids(result_model) full_box_volume = 30.0 * 20.0 * 8.0 if remaining_holes: raise SystemExit(f"isolated hole suppress left hole faces: {remaining_holes}, response={response}") if abs(after_volume - full_box_volume) > 1e-3: raise SystemExit( f"isolated hole suppress volume failed: before={before_volume:g}, " f"after={after_volume:g}, target={full_box_volume:g}, response={response}" ) _assert_one_solid(result_model, "isolated hole suppress") print(f"isolated hole suppress ok: volume={after_volume:g}") def _verify_blind_depth_isolated(temp_dir: Path) -> None: input_path = temp_dir / "blind_hole.step" output_path = temp_dir / "blind_hole_out.step" request_path = temp_dir / "blind_depth_request.json" _write_blind_hole_model(input_path) model = StepModel.load(input_path) face_id = _first_hole_face(model, blind=True) feature = model.feature_info(face_id) bottom_face_ids = tuple(feature.get("feature_bottom_face_ids", ())) bottom_face_id = int(bottom_face_ids[0]) if bottom_face_ids else None target_depth = 8.0 _write_request(request_path, input_path, output_path, "resize_cylindrical_depth", [face_id, target_depth, bottom_face_id]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, depth, error = _nearest_blind_depth(result_model, target_depth) if error > 1e-4: raise SystemExit( f"isolated blind depth verification failed: face={verified_face}, " f"depth={depth:g}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated blind depth") print(f"isolated blind depth ok: face={verified_face}, depth={depth:g}") def _verify_blind_depth_owning_scale_isolated(temp_dir: Path) -> None: input_path = temp_dir / "blind_hole_scale.step" output_path = temp_dir / "blind_hole_scale_out.step" request_path = temp_dir / "blind_depth_scale_request.json" _write_blind_hole_model(input_path) model = StepModel.load(input_path) face_id = _first_hole_face(model, blind=True) feature = model.feature_info(face_id) bottom_face_ids = tuple(feature.get("feature_bottom_face_ids", ())) bottom_face_id = int(bottom_face_ids[0]) if bottom_face_ids else None target_depth = 8.0 _write_request( request_path, input_path, output_path, "resize_cylindrical_depth_owning_scale", [face_id, target_depth, bottom_face_id], ) returncode, response = _run_worker_allow_failure(request_path) if returncode != 0: error = str(response.get("error", "")) if "rolled back" not in error and "回滚" not in error: raise SystemExit(f"isolated blind depth owning-scale failed without rollback message: {response}") print("isolated blind depth owning-scale guarded ok: result rejected and rolled back") return if not response.get("ok"): raise SystemExit(f"isolated blind depth owning-scale returned an error response: {response}") result_model = StepModel.load(output_path) verified_face, depth, error = _nearest_blind_depth(result_model, target_depth) if error > 1e-4: raise SystemExit( f"isolated blind depth owning-scale verification failed: face={verified_face}, " f"depth={depth:g}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated blind depth owning-scale") print(f"isolated blind depth owning-scale ok: face={verified_face}, depth={depth:g}") def _verify_slot_width_isolated(temp_dir: Path) -> None: input_path = temp_dir / "half_round_slot.step" output_path = temp_dir / "half_round_slot_out.step" request_path = temp_dir / "slot_request.json" _write_half_round_slot_model(input_path) model = StepModel.load(input_path) face_id = _first_slot_face(model) target_width = 8.0 _write_request(request_path, input_path, output_path, "resize_cylindrical_slot_width", [face_id, target_width, None]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, width, error = _nearest_metric(result_model, "slot_chord_width_estimate", target_width) if error > 1e-4: raise SystemExit( f"isolated slot width verification failed: face={verified_face}, " f"width={width:g}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated slot width resize") print(f"isolated slot width resize ok: face={verified_face}, width={width:g}") def _verify_slot_owning_scale_isolated(temp_dir: Path) -> None: input_path = temp_dir / "half_round_slot_scale.step" output_path = temp_dir / "half_round_slot_scale_out.step" request_path = temp_dir / "slot_scale_request.json" _write_half_round_slot_model(input_path) model = StepModel.load(input_path) face_id = _first_slot_face(model) target_diameter = 8.0 target_width = 8.0 _write_request(request_path, input_path, output_path, "resize_cylindrical_owning_scale", [face_id, target_diameter]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, width, error = _nearest_metric(result_model, "slot_chord_width_estimate", target_width) if error > 1e-4: raise SystemExit( f"isolated slot owning-scale verification failed: face={verified_face}, " f"width={width:g}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated slot owning-scale") print(f"isolated slot owning-scale ok: face={verified_face}, width={width:g}") def _verify_slot_metric_isolated( temp_dir: Path, stem: str, operation: str, target_value: float, metric_key: str, ) -> None: input_path = temp_dir / f"{stem}.step" output_path = temp_dir / f"{stem}_out.step" request_path = temp_dir / f"{stem}_request.json" _write_half_round_slot_model(input_path) model = StepModel.load(input_path) face_id = _first_slot_face(model) args: list[object] = [face_id, target_value] if operation in { "resize_cylindrical_slot_width", "resize_cylindrical_slot_depth", "resize_cylindrical_slot_arc_length", }: args.append(None) _write_request(request_path, input_path, output_path, operation, args) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, value, error = _nearest_metric(result_model, metric_key, target_value) if error > 1e-4: raise SystemExit( f"isolated {stem} verification failed: face={verified_face}, " f"value={value:g}, error={error:g}, response={response}" ) _assert_one_solid(result_model, f"isolated {stem}") print(f"isolated {stem} ok: face={verified_face}, value={value:g}") def _verify_slot_axis_isolated(temp_dir: Path) -> None: input_path = temp_dir / "half_round_slot_axis.step" output_path = temp_dir / "half_round_slot_axis_out.step" request_path = temp_dir / "slot_axis_request.json" _write_half_round_slot_model(input_path) model = StepModel.load(input_path) face_id = _first_slot_face(model) current_center = _slot_axis_center(model, face_id) target_center = (current_center[0], current_center[1] + 1.0, current_center[2]) _write_request(request_path, input_path, output_path, "move_cylindrical_slot_axis", [face_id, target_center]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, center, error = _nearest_axis_center(result_model, target_center) if error > 1e-4: raise SystemExit( f"isolated slot axis move verification failed: face={verified_face}, " f"center={center}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated slot axis move") print(f"isolated slot axis move ok: face={verified_face}, center={center}") def _verify_obround_slot_total_length_isolated(temp_dir: Path) -> None: input_path = temp_dir / "obround_total_length.step" output_path = temp_dir / "obround_total_length_out.step" request_path = temp_dir / "obround_total_length_request.json" _write_obround_slot_model(input_path) model = StepModel.load(input_path) face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length(model, 16.0) target_total_length = 20.0 _write_request( request_path, input_path, output_path, "resize_cylindrical_slot_total_length", [face_id, target_total_length, pair_face_id], ) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, verified_pair, total_length, center_distance, error = _nearest_obround_total_length( result_model, target_total_length, ) if error > 1e-4: raise SystemExit( f"isolated obround total length verification failed: faces=({verified_face}, {verified_pair}), " f"total_length={total_length:g}, center_distance={center_distance:g}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated obround total length") print(f"isolated obround total length ok: faces=({verified_face}, {verified_pair}), total_length={total_length:g}") def _verify_obround_slot_center_distance_isolated(temp_dir: Path) -> None: input_path = temp_dir / "obround_center_distance.step" output_path = temp_dir / "obround_center_distance_out.step" request_path = temp_dir / "obround_center_distance_request.json" _write_obround_slot_model(input_path) model = StepModel.load(input_path) face_id, pair_face_id, current_total_length, current_center_distance, _ = _nearest_obround_total_length(model, 16.0) slot_diameter = current_total_length - current_center_distance target_center_distance = 14.0 target_total_length = target_center_distance + slot_diameter _write_request( request_path, input_path, output_path, "resize_cylindrical_slot_center_distance", [face_id, target_center_distance, pair_face_id], ) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, verified_pair, total_length, center_distance, total_error = _nearest_obround_total_length( result_model, target_total_length, ) center_error = abs(center_distance - target_center_distance) if center_error > 1e-4 or total_error > 1e-4: raise SystemExit( f"isolated obround center distance verification failed: faces=({verified_face}, {verified_pair}), " f"total_length={total_length:g}, center_distance={center_distance:g}, " f"center_error={center_error:g}, total_error={total_error:g}, response={response}" ) _assert_one_solid(result_model, "isolated obround center distance") print( "isolated obround center distance ok: " f"faces=({verified_face}, {verified_pair}), center_distance={center_distance:g}" ) def _verify_obround_slot_axis_isolated(temp_dir: Path) -> None: input_path = temp_dir / "obround_axis.step" output_path = temp_dir / "obround_axis_out.step" request_path = temp_dir / "obround_axis_request.json" _write_obround_slot_model(input_path) model = StepModel.load(input_path) face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length(model, 16.0) current_center = _slot_axis_center(model, face_id) pair_center = _slot_axis_center(model, pair_face_id) target_center = (current_center[0], current_center[1] + 1.0, current_center[2]) target_pair_center = (pair_center[0], pair_center[1] + 1.0, pair_center[2]) _write_request(request_path, input_path, output_path, "move_cylindrical_slot_axis", [face_id, target_center]) response = _run_worker(request_path) result_model = StepModel.load(output_path) verified_face, verified_pair, center_1, center_2, error = _nearest_obround_axis_pair( result_model, target_center, target_pair_center, ) if error > 1e-4: raise SystemExit( f"isolated obround axis move verification failed: faces=({verified_face}, {verified_pair}), " f"centers=({center_1}, {center_2}), error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated obround axis move") print(f"isolated obround axis move ok: faces=({verified_face}, {verified_pair}), error={error:g}") def _verify_hole_window_isolation_keeps_logical_id(temp_dir: Path) -> None: input_path = temp_dir / "through_hole_window.step" _write_through_hole_model(input_path) probe = _window_probe_for(input_path) face_id = _first_hole_face(probe.model, blind=False) logical_id = probe.model.face_logical_id(face_id) target_diameter = 8.0 target_center = _axis_center(probe.model, face_id) plan = probe.model.cylindrical_resize_plan(face_id, target_diameter) context = { "operation_name": "调整圆柱孔径", "target": f"Face {face_id}", "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "face_id": face_id, "new_diameter": target_diameter, "old_diameter": plan.get("current_diameter"), "resize_strategy": plan.get("resize_strategy"), }, "target_kind": "face", "target_id": face_id, "target_logical_id": logical_id, "pick_position": None, "show_same_domain_internal_edges": False, "edit_result_deflection": 1.6, } if probe._context_is_face_parameter_edit(context): raise SystemExit("hole resize should not be treated as a generic Face parameter edit") if not probe._context_should_preserve_face_logical_id(context): raise SystemExit("hole resize should still preserve the selected logical Face ID") result = probe._run_isolated_edit_job( context=context, isolation={ "operation": "resize_cylindrical_hole", "args": [face_id, target_diameter], "timeout_seconds": 120.0, }, snapshot=probe.model.snapshot(), before_stats=probe.model.stats(), before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)), before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)), before_geometry={}, ) after_model = result.get("after_model") if not isinstance(after_model, StepModel): raise SystemExit(f"window hole isolation should return after_model: {result}") verified_face, diameter, center, error = _nearest_hole_by_diameter( after_model, target_diameter, target_center, blind=False, ) if error > 1e-4 or abs(diameter - target_diameter) > 1e-4: raise SystemExit( f"window hole isolation verification failed: face={verified_face}, " f"diameter={diameter:g}, center={center}, error={error:g}, result={result}" ) if not _logical_region_has_diameter(after_model, logical_id, target_diameter): raise SystemExit( f"window hole isolation did not preserve logical Face {logical_id} on the resized hole" ) _assert_one_solid(after_model, "window hole isolation") print(f"window hole isolation ok: logical={logical_id}, face={verified_face}, diameter={diameter:g}") def _verify_hole_axis_window_isolation_keeps_logical_id(temp_dir: Path) -> None: input_path = temp_dir / "through_hole_axis_window.step" _write_through_hole_model(input_path) probe = _window_probe_for(input_path) face_id = _first_hole_face(probe.model, blind=False) logical_id = probe.model.face_logical_id(face_id) current_center = _axis_center(probe.model, face_id) current_diameter = float(probe.model.face_info(face_id)["diameter"]) target_center = (current_center[0] + 2.0, current_center[1], current_center[2]) plan = probe.model.cylindrical_axis_move_plan(face_id, target_center) context = { "operation_name": "move hole axis", "target": f"Face {face_id}", "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "face_id": face_id, "current_center": current_center, "target_center": target_center, "current_diameter": current_diameter, "resize_strategy": plan.get("resize_strategy"), }, "target_kind": "face", "target_id": face_id, "target_logical_id": logical_id, "pick_position": None, "show_same_domain_internal_edges": False, "edit_result_deflection": 1.6, } if probe._context_is_face_parameter_edit(context): raise SystemExit("hole axis move should not be treated as a generic Face parameter edit") if not probe._context_should_preserve_face_logical_id(context): raise SystemExit("hole axis move should still preserve the selected logical Face ID") result = probe._run_isolated_edit_job( context=context, isolation={ "operation": "move_cylindrical_hole_axis", "args": [face_id, target_center], "timeout_seconds": 120.0, }, snapshot=probe.model.snapshot(), before_stats=probe.model.stats(), before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)), before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)), before_geometry={}, ) after_model = result.get("after_model") if not isinstance(after_model, StepModel): raise SystemExit(f"window hole-axis isolation should return after_model: {result}") verified_face, diameter, center, error = _nearest_hole_by_diameter( after_model, current_diameter, target_center, blind=False, ) if error > 1e-4 or abs(diameter - current_diameter) > 1e-4: raise SystemExit( f"window hole-axis isolation verification failed: face={verified_face}, " f"diameter={diameter:g}, center={center}, error={error:g}, result={result}" ) if not _logical_region_has_hole_axis(after_model, logical_id, target_center, current_diameter): raise SystemExit( f"window hole-axis isolation did not preserve logical Face {logical_id} on the moved hole" ) _assert_one_solid(after_model, "window hole-axis isolation") print(f"window hole-axis isolation ok: logical={logical_id}, face={verified_face}, center={center}") def _verify_slot_window_isolation_keeps_logical_id(temp_dir: Path) -> None: input_path = temp_dir / "slot_window.step" _write_half_round_slot_model(input_path) probe = _window_probe_for(input_path) face_id = _first_slot_face(probe.model) logical_id = probe.model.face_logical_id(face_id) target_width = 8.0 plan = probe.model.cylindrical_slot_resize_plan(face_id, target_width, "width") context = { "operation_name": "resize slot width", "target": f"Face {face_id}", "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "face_id": face_id, "target_width": target_width, "current_width": plan.get("slot_current_width"), "resize_strategy": plan.get("resize_strategy"), "slot_pair_face_id": plan.get("slot_pair_face_id"), }, "target_kind": "face", "target_id": face_id, "target_logical_id": logical_id, "pick_position": None, "show_same_domain_internal_edges": False, "edit_result_deflection": 1.6, } if probe._context_is_face_parameter_edit(context): raise SystemExit("slot width should not be treated as a generic Face parameter edit") if not probe._context_should_preserve_face_logical_id(context): raise SystemExit("slot width should still preserve the selected logical Face ID") result = probe._run_isolated_edit_job( context=context, isolation={ "operation": "resize_cylindrical_slot_width", "args": [face_id, target_width, None], "timeout_seconds": 120.0, }, snapshot=probe.model.snapshot(), before_stats=probe.model.stats(), before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)), before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)), before_geometry={}, ) after_model = result.get("after_model") if not isinstance(after_model, StepModel): raise SystemExit(f"window slot isolation should return after_model: {result}") verified_face, width, error = _nearest_metric(after_model, "slot_chord_width_estimate", target_width) if error > 1e-4: raise SystemExit( f"window slot isolation verification failed: face={verified_face}, " f"width={width:g}, error={error:g}, result={result}" ) if not _logical_region_has_slot_metric(after_model, logical_id, "slot_chord_width_estimate", target_width): raise SystemExit( f"window slot isolation did not preserve logical Face {logical_id} on the resized slot" ) _assert_one_solid(after_model, "window slot isolation") print(f"window slot isolation ok: logical={logical_id}, face={verified_face}, width={width:g}") def _verify_slot_axis_window_isolation_keeps_logical_id(temp_dir: Path) -> None: input_path = temp_dir / "slot_axis_window.step" _write_half_round_slot_model(input_path) probe = _window_probe_for(input_path) face_id = _first_slot_face(probe.model) logical_id = probe.model.face_logical_id(face_id) current_center = _slot_axis_center(probe.model, face_id) target_center = (current_center[0], current_center[1] + 1.0, current_center[2]) plan = probe.model.cylindrical_slot_axis_move_plan(face_id, target_center) context = { "operation_name": "move slot axis", "target": f"Face {face_id}", "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "face_id": face_id, "current_center": current_center, "target_center": target_center, "resize_strategy": plan.get("resize_strategy"), }, "target_kind": "face", "target_id": face_id, "target_logical_id": logical_id, "pick_position": None, "show_same_domain_internal_edges": False, "edit_result_deflection": 1.6, } if probe._context_is_face_parameter_edit(context): raise SystemExit("slot axis move should not be treated as a generic Face parameter edit") if not probe._context_should_preserve_face_logical_id(context): raise SystemExit("slot axis move should still preserve the selected logical Face ID") result = probe._run_isolated_edit_job( context=context, isolation={ "operation": "move_cylindrical_slot_axis", "args": [face_id, target_center], "timeout_seconds": 120.0, }, snapshot=probe.model.snapshot(), before_stats=probe.model.stats(), before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)), before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)), before_geometry={}, ) after_model = result.get("after_model") if not isinstance(after_model, StepModel): raise SystemExit(f"window slot-axis isolation should return after_model: {result}") verified_face, center, error = _nearest_axis_center(after_model, target_center) if error > 1e-4: raise SystemExit( f"window slot-axis isolation verification failed: face={verified_face}, " f"center={center}, error={error:g}, result={result}" ) if not _logical_region_has_slot_axis(after_model, logical_id, target_center): raise SystemExit( f"window slot-axis isolation did not preserve logical Face {logical_id} on the moved slot" ) _assert_one_solid(after_model, "window slot-axis isolation") print(f"window slot-axis isolation ok: logical={logical_id}, face={verified_face}, center={center}") def _verify_blind_depth_window_isolation_keeps_logical_id(temp_dir: Path) -> None: input_path = temp_dir / "blind_depth_window.step" _write_blind_hole_model(input_path) probe = _window_probe_for(input_path) face_id = _first_hole_face(probe.model, blind=True) logical_id = probe.model.face_logical_id(face_id) feature = probe.model.feature_info(face_id) bottom_face_ids = tuple(feature.get("feature_bottom_face_ids", ())) bottom_face_id = int(bottom_face_ids[0]) if bottom_face_ids else None target_depth = 8.0 plan = probe.model.cylindrical_depth_plan(face_id, target_depth, bottom_face_id=bottom_face_id) context = { "operation_name": "resize blind hole depth", "target": f"Face {face_id}", "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "face_id": face_id, "target_depth": target_depth, "current_depth": plan.get("current_depth"), "resize_strategy": plan.get("resize_strategy"), "manual_bottom_face_id": bottom_face_id, }, "target_kind": "face", "target_id": face_id, "target_logical_id": logical_id, "pick_position": None, "show_same_domain_internal_edges": False, "edit_result_deflection": 1.6, } if probe._context_is_face_parameter_edit(context): raise SystemExit("blind depth should not be treated as a generic Face parameter edit") if not probe._context_should_preserve_face_logical_id(context): raise SystemExit("blind depth should still preserve the selected logical Face ID") result = probe._run_isolated_edit_job( context=context, isolation={ "operation": "resize_cylindrical_depth", "args": [face_id, target_depth, bottom_face_id], "timeout_seconds": 120.0, }, snapshot=probe.model.snapshot(), before_stats=probe.model.stats(), before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)), before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)), before_geometry={}, ) after_model = result.get("after_model") if not isinstance(after_model, StepModel): raise SystemExit(f"window blind-depth isolation should return after_model: {result}") verified_face, depth, error = _nearest_blind_depth(after_model, target_depth) if error > 1e-4: raise SystemExit( f"window blind-depth isolation verification failed: face={verified_face}, " f"depth={depth:g}, error={error:g}, result={result}" ) if not _logical_region_has_blind_depth(after_model, logical_id, target_depth): raise SystemExit( f"window blind-depth isolation did not preserve logical Face {logical_id} on the resized feature" ) _assert_one_solid(after_model, "window blind-depth isolation") print(f"window blind-depth isolation ok: logical={logical_id}, face={verified_face}, depth={depth:g}") def _verify_obround_total_length_window_isolation_keeps_logical_id(temp_dir: Path) -> None: input_path = temp_dir / "obround_total_length_window.step" _write_obround_slot_model(input_path) probe = _window_probe_for(input_path) face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length( probe.model, 16.0, ) logical_id = probe.model.face_logical_id(face_id) target_total_length = 20.0 plan = probe.model.cylindrical_slot_total_length_plan(face_id, target_total_length, pair_face_id=pair_face_id) context = { "operation_name": "resize obround slot total length", "target": f"Face {face_id}", "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "face_id": face_id, "target_total_length": target_total_length, "slot_pair_face_id": pair_face_id, "resize_strategy": plan.get("resize_strategy"), }, "target_kind": "face", "target_id": face_id, "target_logical_id": logical_id, "pick_position": None, "show_same_domain_internal_edges": False, "edit_result_deflection": 1.6, } if probe._context_is_face_parameter_edit(context): raise SystemExit("obround total length should not be treated as a generic Face parameter edit") if not probe._context_should_preserve_face_logical_id(context): raise SystemExit("obround total length should still preserve the selected logical Face ID") result = probe._run_isolated_edit_job( context=context, isolation={ "operation": "resize_cylindrical_slot_total_length", "args": [face_id, target_total_length, pair_face_id], "timeout_seconds": 120.0, }, snapshot=probe.model.snapshot(), before_stats=probe.model.stats(), before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)), before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)), before_geometry={}, ) after_model = result.get("after_model") if not isinstance(after_model, StepModel): raise SystemExit(f"window obround-total-length isolation should return after_model: {result}") verified_face, verified_pair, total_length, center_distance, error = _nearest_obround_total_length( after_model, target_total_length, ) if error > 1e-4: raise SystemExit( f"window obround-total-length isolation verification failed: " f"faces=({verified_face}, {verified_pair}), total_length={total_length:g}, " f"center_distance={center_distance:g}, error={error:g}, result={result}" ) if not _logical_region_contains_any(after_model, logical_id, (verified_face, verified_pair)): raise SystemExit( f"window obround-total-length isolation did not preserve logical Face {logical_id} " "on the resized slot end" ) _assert_one_solid(after_model, "window obround-total-length isolation") print( "window obround-total-length isolation ok: " f"logical={logical_id}, faces=({verified_face}, {verified_pair}), total_length={total_length:g}" ) def _verify_obround_axis_window_isolation_keeps_logical_id(temp_dir: Path) -> None: input_path = temp_dir / "obround_axis_window.step" _write_obround_slot_model(input_path) probe = _window_probe_for(input_path) face_id, pair_face_id, _current_total_length, _current_center_distance, _ = _nearest_obround_total_length( probe.model, 16.0, ) logical_id = probe.model.face_logical_id(face_id) current_center = _slot_axis_center(probe.model, face_id) pair_center = _slot_axis_center(probe.model, pair_face_id) target_center = (current_center[0], current_center[1] + 1.0, current_center[2]) target_pair_center = (pair_center[0], pair_center[1] + 1.0, pair_center[2]) plan = probe.model.cylindrical_slot_axis_move_plan(face_id, target_center) context = { "operation_name": "move obround slot axis", "target": f"Face {face_id}", "parameters": { "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "face_id": face_id, "slot_pair_face_id": pair_face_id, "target_center": target_center, "target_pair_center": target_pair_center, "resize_strategy": plan.get("resize_strategy"), }, "target_kind": "face", "target_id": face_id, "target_logical_id": logical_id, "pick_position": None, "show_same_domain_internal_edges": False, "edit_result_deflection": 1.6, } if probe._context_is_face_parameter_edit(context): raise SystemExit("obround axis move should not be treated as a generic Face parameter edit") if not probe._context_should_preserve_face_logical_id(context): raise SystemExit("obround axis move should still preserve the selected logical Face ID") result = probe._run_isolated_edit_job( context=context, isolation={ "operation": "move_cylindrical_slot_axis", "args": [face_id, target_center], "timeout_seconds": 120.0, }, snapshot=probe.model.snapshot(), before_stats=probe.model.stats(), before_part_stats=probe.model.part_topology_stats(int(plan.get("part_id") or 1)), before_quality=probe._edit_quality_info_or_none(probe.model, context, int(plan.get("part_id") or 1)), before_geometry={}, ) after_model = result.get("after_model") if not isinstance(after_model, StepModel): raise SystemExit(f"window obround-axis isolation should return after_model: {result}") verified_face, verified_pair, center_1, center_2, error = _nearest_obround_axis_pair( after_model, target_center, target_pair_center, ) if error > 1e-4: raise SystemExit( f"window obround-axis isolation verification failed: faces=({verified_face}, {verified_pair}), " f"centers=({center_1}, {center_2}), error={error:g}, result={result}" ) if not _logical_region_contains_any(after_model, logical_id, (verified_face, verified_pair)): raise SystemExit( f"window obround-axis isolation did not preserve logical Face {logical_id} on the moved slot end" ) _assert_one_solid(after_model, "window obround-axis isolation") print(f"window obround-axis isolation ok: logical={logical_id}, faces=({verified_face}, {verified_pair})") def main() -> int: with tempfile.TemporaryDirectory(prefix="geom_param_hole_slot_isolated_") as temp_name: temp_dir = Path(temp_name) _verify_hole_diameter_isolated(temp_dir) _verify_hole_owning_scale_isolated(temp_dir) _verify_hole_axis_isolated(temp_dir) _verify_hole_suppress_isolated(temp_dir) _verify_blind_depth_isolated(temp_dir) _verify_blind_depth_owning_scale_isolated(temp_dir) _verify_slot_width_isolated(temp_dir) _verify_slot_owning_scale_isolated(temp_dir) _verify_slot_metric_isolated( temp_dir, "slot_depth", "resize_cylindrical_slot_depth", 4.0, "slot_sagitta_depth_estimate", ) _verify_slot_metric_isolated( temp_dir, "slot_arc_length", "resize_cylindrical_slot_arc_length", 12.0, "slot_arc_length_estimate", ) _verify_slot_metric_isolated( temp_dir, "slot_angular_span", "resize_cylindrical_slot_angular_span", 2.2, "slot_angular_span", ) _verify_slot_axis_isolated(temp_dir) _verify_obround_slot_total_length_isolated(temp_dir) _verify_obround_slot_center_distance_isolated(temp_dir) _verify_obround_slot_axis_isolated(temp_dir) _verify_hole_window_isolation_keeps_logical_id(temp_dir) _verify_hole_axis_window_isolation_keeps_logical_id(temp_dir) _verify_slot_window_isolation_keeps_logical_id(temp_dir) _verify_slot_axis_window_isolation_keeps_logical_id(temp_dir) _verify_blind_depth_window_isolation_keeps_logical_id(temp_dir) _verify_obround_total_length_window_isolation_keeps_logical_id(temp_dir) _verify_obround_axis_window_isolation_keeps_logical_id(temp_dir) print("hole/slot isolated edit worker ok") return 0 if __name__ == "__main__": raise SystemExit(main())