feat: 完善 Face 一级编辑与隔离计算

This commit is contained in:
2026-08-05 15:08:16 +08:00
parent a76282d7dd
commit ed1faf51d8
28 changed files with 3740 additions and 876 deletions
+286
View File
@@ -0,0 +1,286 @@
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_fillet_face,
_verify_chamfer_topology,
_write_box_model,
_write_filleted_box_model,
)
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 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)
print("Edge isolated edit suite passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())