Files
pythonocc-step-editor/scripts/verify_edge_coordinate_edit.py
T

197 lines
8.3 KiB
Python

from __future__ import annotations
import argparse
from pathlib import Path
import sys
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.model import StepModel
DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step"
def _tuple3(value: object, label: str) -> tuple[float, float, float]:
if not isinstance(value, (tuple, list)) or len(value) != 3:
raise SystemExit(f"{label} is missing or invalid: {value!r}")
return float(value[0]), float(value[1]), float(value[2])
def _sub(a: tuple[float, float, float], b: tuple[float, float, float]) -> tuple[float, float, float]:
return a[0] - b[0], a[1] - b[1], a[2] - b[2]
def _add(a: tuple[float, float, float], b: tuple[float, float, float]) -> tuple[float, float, float]:
return a[0] + b[0], a[1] + b[1], a[2] + b[2]
def _scale(a: tuple[float, float, float], factor: float) -> tuple[float, float, float]:
return a[0] * factor, a[1] * factor, a[2] * factor
def _length(a: tuple[float, float, float]) -> float:
return (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]) ** 0.5
def _distance(a: tuple[float, float, float], b: tuple[float, float, float]) -> float:
return _length(_sub(a, b))
def _first_line_edge_near_length(model: StepModel, length: float, tolerance: float) -> int:
for edge_id in range(len(model.edges)):
info = model.edge_info(edge_id)
if info.get("curve") != "line":
continue
if abs(float(info.get("length") or 0.0) - length) <= tolerance:
return edge_id
raise SystemExit(f"no line Edge near length {length:g}")
def _endpoint_pair_error(
model: StepModel,
edge_id: int,
expected_start: tuple[float, float, float],
expected_end: tuple[float, float, float],
) -> float:
info = model.edge_info(edge_id)
start = _tuple3(info.get("start_point"), f"Edge {edge_id} start")
end = _tuple3(info.get("end_point"), f"Edge {edge_id} end")
direct = max(_distance(start, expected_start), _distance(end, expected_end))
reversed_order = max(_distance(start, expected_end), _distance(end, expected_start))
return min(direct, reversed_order)
def _nearest_expected_edge(
model: StepModel,
expected_start: tuple[float, float, float],
expected_end: tuple[float, float, float],
) -> tuple[int, float, float]:
best: tuple[int, float, float] | None = None
target_length = _distance(expected_start, expected_end)
for edge_id in range(len(model.edges)):
info = model.edge_info(edge_id)
if info.get("curve") != "line":
continue
endpoint_error = _endpoint_pair_error(model, edge_id, expected_start, expected_end)
length_error = abs(float(info.get("length") or 0.0) - target_length)
if best is None or (endpoint_error, length_error, edge_id) < (best[1], best[2], best[0]):
best = (edge_id, endpoint_error, length_error)
if best is None:
raise SystemExit("edited model has no line Edge for endpoint verification")
return best
def _source_edge_frame(
model: StepModel,
source_length: float,
tolerance: float,
) -> tuple[int, tuple[float, float, float], tuple[float, float, float], tuple[float, float, float], float]:
edge_id = _first_line_edge_near_length(model, source_length, tolerance)
info = model.edge_info(edge_id)
start = _tuple3(info.get("start_point"), "source start")
end = _tuple3(info.get("end_point"), "source end")
vector = _sub(end, start)
length = _length(vector)
if length <= 1e-9:
raise SystemExit(f"source Edge {edge_id} has zero length")
direction = _scale(vector, 1.0 / length)
return edge_id, start, end, direction, length
def _run_endpoint_case(role: str, target_delta: float, tolerance: float) -> None:
model = StepModel.load(DEFAULT_MODEL)
edge_id, start, end, direction, source_length = _source_edge_frame(model, 10.0, tolerance)
if role == "start":
target_start = _add(start, _scale(direction, -target_delta))
target_end = end
target_point = target_start
elif role == "end":
target_start = start
target_end = _add(end, _scale(direction, target_delta))
target_point = target_end
else:
raise SystemExit(f"unsupported endpoint role: {role}")
before = model.stats()
plan = model.edge_endpoint_move_plan(edge_id, role, target_point)
if plan["status"] == "blocked":
raise SystemExit(f"{role} endpoint plan was blocked: {plan['message']}")
if plan.get("resize_strategy") != "local-edge-endpoint-deform":
raise SystemExit(f"{role} endpoint should use local-edge-endpoint-deform, got {plan.get('resize_strategy')}")
result = model.move_edge_endpoint(edge_id, role, target_point)
after = model.stats()
matched_edge, endpoint_error, length_error = _nearest_expected_edge(model, target_start, target_end)
if after.solids != before.solids:
raise SystemExit(f"{role} endpoint changed solid count: before={before.solids}, after={after.solids}")
if endpoint_error > tolerance or length_error > tolerance:
raise SystemExit(
f"{role} endpoint verification failed: matched_edge={matched_edge}, "
f"endpoint_error={endpoint_error:g}, length_error={length_error:g}"
)
print(f"mode={role}_endpoint")
print(f"source_edge={edge_id}")
print(f"matched_edge={matched_edge}")
print(f"source_length={source_length:.6f}")
print(f"target_point={target_point}")
print(f"endpoint_error={endpoint_error:.6g} length_error={length_error:.6g}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _run_center_case(center_delta: tuple[float, float, float], tolerance: float) -> None:
model = StepModel.load(DEFAULT_MODEL)
edge_id, start, end, _direction, source_length = _source_edge_frame(model, 10.0, tolerance)
current_center = _tuple3(model.edge_info(edge_id).get("length_center"), "source center")
target_center = _add(current_center, center_delta)
target_start = _add(start, center_delta)
target_end = _add(end, center_delta)
before = model.stats()
plan = model.edge_center_move_plan(edge_id, target_center)
if plan["status"] == "blocked":
raise SystemExit(f"center plan was blocked: {plan['message']}")
if plan.get("resize_strategy") != "local-edge-center-deform":
raise SystemExit(f"center move should use local-edge-center-deform, got {plan.get('resize_strategy')}")
result = model.move_edge_center(edge_id, target_center)
after = model.stats()
matched_edge, endpoint_error, length_error = _nearest_expected_edge(model, target_start, target_end)
if after.solids != before.solids:
raise SystemExit(f"center move changed solid count: before={before.solids}, after={after.solids}")
if endpoint_error > tolerance or length_error > tolerance:
raise SystemExit(
f"center move verification failed: matched_edge={matched_edge}, "
f"endpoint_error={endpoint_error:g}, length_error={length_error:g}"
)
print("mode=center")
print(f"source_edge={edge_id}")
print(f"matched_edge={matched_edge}")
print(f"source_length={source_length:.6f}")
print(f"target_center={target_center}")
print(f"endpoint_error={endpoint_error:.6g} length_error={length_error:.6g}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def main() -> int:
parser = argparse.ArgumentParser(description="Verify straight Edge start, center and end coordinate edits.")
parser.add_argument("--mode", default="all", choices=["all", "start", "center", "end"])
parser.add_argument("--endpoint-delta", type=float, default=3.0)
parser.add_argument("--center-delta", default="0,0,2")
parser.add_argument("--tolerance", type=float, default=1e-5)
args = parser.parse_args()
center_delta = _tuple3([part.strip() for part in str(args.center_delta).split(",")], "center delta")
if args.mode in {"all", "start"}:
_run_endpoint_case("start", args.endpoint_delta, args.tolerance)
if args.mode in {"all", "center"}:
_run_center_case(center_delta, args.tolerance)
if args.mode in {"all", "end"}:
_run_endpoint_case("end", args.endpoint_delta, args.tolerance)
return 0
if __name__ == "__main__":
raise SystemExit(main())