feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user