from __future__ import annotations 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)) 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_edge_coordinate_edit import ( _add, _distance, _nearest_expected_edge, _source_edge_frame, _tuple3, ) from verify_edge_length_resize import _edge_length, _line_edge_ids_near_length from verify_edge_round_chamfer import ( _cylindrical_faces_near_radius, _first_adjacent_reference_face, _first_editable_line_edge, _first_existing_chamfer_face, _first_existing_fillet_face, _verify_chamfer_topology, _write_box_model, _write_chamfered_box_model, _write_chained_filleted_box_model, _write_filleted_box_model, _existing_chamfer_faces_near_distance, ) from verify_ellipse_edge_resize import _ellipse_edge_ids, _write_ellipse_face_model from verify_hole_resize import ( _first_hole_face, _nearest_hole_by_diameter, _write_through_hole_model, ) from verify_hole_slot_isolated_edit import _assert_one_solid, _run_worker, _write_request DEFAULT_CUBE = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" def _load_worker_output(output_path: Path, response: dict[str, object]) -> StepModel: if not output_path.exists(): raise SystemExit(f"isolated worker reported success but did not write output STEP: response={response}") return StepModel.load(output_path) def _verify_edge_length_isolated(temp_dir: Path) -> None: input_path = DEFAULT_CUBE output_path = temp_dir / "edge_length_out.step" request_path = temp_dir / "edge_length_request.json" model = StepModel.load(input_path) edge_ids = _line_edge_ids_near_length(model, 10.0, 1e-5) if not edge_ids: raise SystemExit("no cube line Edge near length 10 was found") edge_id = edge_ids[0] target_length = 15.0 _write_request( request_path, input_path, output_path, "resize_general_edge_length", [edge_id, target_length, "keep-start", "local-edge-only-deform"], ) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) lengths = [_edge_length(result_model, candidate) for candidate in range(len(result_model.edges))] nearest = min(lengths, key=lambda value: abs(value - target_length)) if abs(nearest - target_length) > 1e-5: raise SystemExit(f"isolated Edge length failed: nearest={nearest:g}, response={response}") _assert_one_solid(result_model, "isolated Edge length") print(f"isolated Edge length ok: edge={edge_id}, nearest={nearest:g}") def _verify_edge_endpoint_isolated(temp_dir: Path) -> None: input_path = DEFAULT_CUBE output_path = temp_dir / "edge_endpoint_out.step" request_path = temp_dir / "edge_endpoint_request.json" model = StepModel.load(input_path) edge_id, start, end, direction, _source_length = _source_edge_frame(model, 10.0, 1e-5) target_start = _add(start, (direction[0] * -3.0, direction[1] * -3.0, direction[2] * -3.0)) _write_request(request_path, input_path, output_path, "move_edge_endpoint", [edge_id, "start", target_start]) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) matched_edge, endpoint_error, length_error = _nearest_expected_edge(result_model, target_start, end) if endpoint_error > 1e-5 or length_error > 1e-5: raise SystemExit( f"isolated Edge endpoint failed: matched={matched_edge}, " f"endpoint_error={endpoint_error:g}, length_error={length_error:g}, response={response}" ) _assert_one_solid(result_model, "isolated Edge endpoint") print(f"isolated Edge endpoint ok: edge={edge_id}, matched={matched_edge}") def _verify_edge_center_isolated(temp_dir: Path) -> None: input_path = DEFAULT_CUBE output_path = temp_dir / "edge_center_out.step" request_path = temp_dir / "edge_center_request.json" model = StepModel.load(input_path) edge_id, start, end, _direction, _source_length = _source_edge_frame(model, 10.0, 1e-5) current_center = _tuple3(model.edge_info(edge_id).get("length_center"), "source center") delta = (0.0, 0.0, 2.0) target_center = _add(current_center, delta) target_start = _add(start, delta) target_end = _add(end, delta) _write_request(request_path, input_path, output_path, "move_edge_center", [edge_id, target_center]) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) matched_edge, endpoint_error, length_error = _nearest_expected_edge(result_model, target_start, target_end) if endpoint_error > 1e-5 or length_error > 1e-5: raise SystemExit( f"isolated Edge center failed: matched={matched_edge}, " f"endpoint_error={endpoint_error:g}, length_error={length_error:g}, response={response}" ) _assert_one_solid(result_model, "isolated Edge center") print(f"isolated Edge center ok: edge={edge_id}, matched={matched_edge}") def _verify_circular_edge_axis_isolated(temp_dir: Path) -> None: input_path = temp_dir / "circular_edge_axis.step" output_path = temp_dir / "circular_edge_axis_out.step" request_path = temp_dir / "circular_edge_axis_request.json" _write_through_hole_model(input_path) model = StepModel.load(input_path) hole_face_id = _first_hole_face(model, blind=False) current_diameter = float(model.face_info(hole_face_id)["diameter"]) edge_id = None target_edge_center = None target_axis_center = None for candidate in range(len(model.edges)): if model.edge_info(candidate).get("curve") != "circle": continue current_edge_center = _tuple3(model.edge_info(candidate).get("center"), f"circle Edge {candidate} center") candidate_target = (current_edge_center[0] + 2.0, current_edge_center[1], current_edge_center[2]) plan = model.circular_edge_axis_move_plan(candidate, candidate_target) if plan.get("status") != "blocked": edge_id = candidate target_edge_center = candidate_target target_axis_center = _tuple3(plan.get("target_axis_center"), "target axis center") break if edge_id is None or target_edge_center is None or target_axis_center is None: raise SystemExit("no circular Edge with editable adjacent cylinder axis was found") _write_request(request_path, input_path, output_path, "move_circular_edge_axis_center", [edge_id, target_edge_center]) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) verified_face, diameter, center, error = _nearest_hole_by_diameter( result_model, current_diameter, target_axis_center, blind=False, ) if error > 1e-4 or abs(diameter - current_diameter) > 1e-4: raise SystemExit( f"isolated circular Edge axis failed: face={verified_face}, " f"diameter={diameter:g}, center={center}, error={error:g}, response={response}" ) _assert_one_solid(result_model, "isolated circular Edge axis") print(f"isolated circular Edge axis ok: edge={edge_id}, face={verified_face}") def _verify_ellipse_edge_radius_isolated(temp_dir: Path) -> None: input_path = temp_dir / "ellipse_edge.step" output_path = temp_dir / "ellipse_edge_out.step" request_path = temp_dir / "ellipse_edge_request.json" _write_ellipse_face_model(input_path) model = StepModel.load(input_path) edge_ids = _ellipse_edge_ids(model) if not edge_ids: raise SystemExit("no ellipse Edge was recognized") edge_id = edge_ids[0] target_radius = 7.5 plan = model.ellipse_edge_axis_radius_plan(edge_id, target_radius, axis_kind="major") _write_request( request_path, input_path, output_path, "resize_ellipse_edge_axis_radius", [edge_id, target_radius, "major"], ) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) sampled = result_model._ellipse_edge_axis_radius_sampled_result(plan) if sampled is None: raise SystemExit(f"isolated ellipse Edge radius could not be sampled: response={response}") verified_edge, value, other, error = sampled if error > 5e-3 or abs(other - 2.0) > 5e-3: raise SystemExit( f"isolated ellipse Edge radius failed: edge={verified_edge}, " f"value={value:g}, other={other:g}, error={error:g}, response={response}" ) print(f"isolated ellipse Edge radius ok: edge={edge_id}, verified={verified_edge}, value={value:g}") def _verify_edge_fillet_isolated(temp_dir: Path) -> None: input_path = temp_dir / "edge_fillet.step" output_path = temp_dir / "edge_fillet_out.step" request_path = temp_dir / "edge_fillet_request.json" _write_box_model(input_path) model = StepModel.load(input_path) edge_id = _first_editable_line_edge(model) target_radius = 1.0 _write_request(request_path, input_path, output_path, "fillet_edge", [edge_id, target_radius]) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) matches = _cylindrical_faces_near_radius(result_model, target_radius, 2e-4) if not matches: raise SystemExit(f"isolated Edge fillet failed: response={response}") _assert_one_solid(result_model, "isolated Edge fillet") print(f"isolated Edge fillet ok: edge={edge_id}, matches={matches[:3]}") def _verify_edge_chamfers_isolated(temp_dir: Path) -> None: cases = ( ("chamfer_edge", [1.0], "chamfer"), ("chamfer_edge_asymmetric", [0.8, 1.2, None], "asymmetric_chamfer"), ("chamfer_edge_distance_angle", [1.0, 45.0, None], "distance_angle_chamfer"), ) for operation, tail_args, label in cases: input_path = temp_dir / f"{label}.step" output_path = temp_dir / f"{label}_out.step" request_path = temp_dir / f"{label}_request.json" _write_box_model(input_path) model = StepModel.load(input_path) edge_id = _first_editable_line_edge(model) reference_face_id = _first_adjacent_reference_face(model, edge_id) args = [edge_id, *tail_args] if operation != "chamfer_edge": args[-1] = reference_face_id before = model.stats() _write_request(request_path, input_path, output_path, operation, args) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) after = result_model.stats() _verify_chamfer_topology(label, edge_id, {"resize_strategy": operation}, before, after, str(response.get("message")), []) _assert_one_solid(result_model, f"isolated {label}") def _verify_existing_fillet_isolated(temp_dir: Path) -> None: input_path = temp_dir / "existing_fillet.step" output_path = temp_dir / "existing_fillet_out.step" request_path = temp_dir / "existing_fillet_request.json" source_radius = 1.0 target_radius = 1.5 _write_filleted_box_model(input_path, source_radius) model = StepModel.load(input_path) face_id = _first_existing_fillet_face(model, source_radius, 2e-4) _write_request(request_path, input_path, output_path, "resize_existing_fillet", [face_id, target_radius]) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) matches = _cylindrical_faces_near_radius(result_model, target_radius, 2e-4) old_matches = _cylindrical_faces_near_radius(result_model, source_radius, 2e-4) if not matches or old_matches: raise SystemExit( f"isolated existing fillet failed: matches={matches}, old_matches={old_matches}, response={response}" ) _assert_one_solid(result_model, "isolated existing fillet") print(f"isolated existing fillet ok: face={face_id}, matches={matches[:3]}") def _verify_existing_fillet_chain_isolated(temp_dir: Path) -> None: input_path = temp_dir / "existing_fillet_chain.step" output_path = temp_dir / "existing_fillet_chain_out.step" request_path = temp_dir / "existing_fillet_chain_request.json" source_radius = 1.0 target_radius = 1.5 _write_chained_filleted_box_model(input_path, source_radius) model = StepModel.load(input_path) face_id = _first_existing_fillet_face(model, source_radius, 2e-4) feature = model.feature_info(face_id) chain_face_ids = tuple(feature.get("feature_existing_fillet_chain_face_ids") or ()) if len(chain_face_ids) < 2: raise SystemExit(f"isolated existing fillet chain source did not expose a chain: {feature}") _write_request(request_path, input_path, output_path, "resize_existing_fillet", [face_id, target_radius]) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) matches = _cylindrical_faces_near_radius(result_model, target_radius, 2e-4) old_matches = _cylindrical_faces_near_radius(result_model, source_radius, 2e-4) if len(matches) < len(chain_face_ids) or old_matches: raise SystemExit( "isolated existing fillet chain failed: " f"matches={matches}, old_matches={old_matches}, chain={chain_face_ids}, response={response}" ) _assert_one_solid(result_model, "isolated existing fillet chain") print(f"isolated existing fillet chain ok: face={face_id}, chain={chain_face_ids}, matches={matches[:4]}") def _verify_existing_chamfer_isolated(temp_dir: Path) -> None: input_path = temp_dir / "existing_chamfer.step" output_path = temp_dir / "existing_chamfer_out.step" request_path = temp_dir / "existing_chamfer_request.json" source_distance = 1.5 target_distance = 2.0 _write_chamfered_box_model(input_path, source_distance) model = StepModel.load(input_path) face_id = _first_existing_chamfer_face(model, source_distance, 2e-4) _write_request(request_path, input_path, output_path, "resize_existing_chamfer", [face_id, target_distance]) response = _run_worker(request_path) result_model = _load_worker_output(output_path, response) matches = _existing_chamfer_faces_near_distance(result_model, target_distance, target_distance * 0.03) old_matches = _existing_chamfer_faces_near_distance(result_model, source_distance, source_distance * 0.02) if not matches or old_matches: raise SystemExit( f"isolated existing chamfer failed: matches={matches}, old_matches={old_matches}, response={response}" ) _assert_one_solid(result_model, "isolated existing chamfer") print(f"isolated existing chamfer ok: face={face_id}, matches={matches[:3]}") def main() -> int: with tempfile.TemporaryDirectory(prefix="geom_param_edge_isolated_") as temp: temp_dir = Path(temp) _verify_edge_length_isolated(temp_dir) _verify_edge_endpoint_isolated(temp_dir) _verify_edge_center_isolated(temp_dir) _verify_circular_edge_axis_isolated(temp_dir) _verify_ellipse_edge_radius_isolated(temp_dir) _verify_edge_fillet_isolated(temp_dir) _verify_edge_chamfers_isolated(temp_dir) _verify_existing_fillet_isolated(temp_dir) _verify_existing_fillet_chain_isolated(temp_dir) _verify_existing_chamfer_isolated(temp_dir) print("Edge isolated edit suite passed.") return 0 if __name__ == "__main__": raise SystemExit(main())