344 lines
15 KiB
Python
344 lines
15 KiB
Python
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())
|