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

195 lines
8.1 KiB
Python

from __future__ import annotations
import sys
from pathlib import Path
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
from step_editor.window_state import WindowStateMixin
DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step"
class _PropertySpecProbe(WindowStateMixin):
def __init__(self, face_id: int, info: dict[str, object]) -> None:
self.model = object()
self.operation_in_progress = False
self.scan_in_progress = False
self.load_in_progress = False
self.selected_face_id = face_id
self.selected_edge_id = None
self.selected_kind = "face"
self.selected_part_id = int(info.get("part_id", 1))
self.selected_solid_id = int(info.get("solid_id", 1))
self.manual_bottom_face_id = None
self.manual_slot_pair_face_id = None
def _specs(face_id: int, info: dict[str, object]) -> list[dict[str, object]]:
probe = _PropertySpecProbe(face_id, info)
specs, _used = probe._editable_property_specs(info)
return specs
def _spec(face_id: int, info: dict[str, object], key: str) -> dict[str, object]:
for spec in _specs(face_id, info):
if spec.get("key") == key:
return spec
raise SystemExit(f"{key} spec was not found for Face {face_id}")
def _float(value: object, label: str) -> float:
try:
return float(value)
except (TypeError, ValueError) as exc:
raise SystemExit(f"{label} should be numeric, got {value!r}") from exc
def _assert_close(label: str, value: object, expected: float, tolerance: float = 1e-5) -> None:
number = _float(value, label)
if abs(number - expected) > tolerance:
raise SystemExit(f"{label} should be {expected:g}, got {number:g}")
def _plane_metrics(model: StepModel) -> list[tuple[int, float, float, float, tuple[float, float, float]]]:
metrics: list[tuple[int, float, float, float, tuple[float, float, float]]] = []
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
center = info.get("area_center") or info.get("bbox_center")
if not isinstance(center, tuple) or len(center) != 3:
continue
metrics.append(
(
face_id,
float(info.get("area") or 0.0),
float(info.get("local_face_width") or 0.0),
float(info.get("local_face_height") or 0.0),
(float(center[0]), float(center[1]), float(center[2])),
)
)
return metrics
def _distance(a: tuple[float, float, float], b: tuple[float, float, float]) -> float:
return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2) ** 0.5
def _first_plane_face_near_size(model: StepModel, width: float, height: float, tolerance: float = 1e-5) -> int:
for face_id, _area, face_width, face_height, _center in _plane_metrics(model):
direct = abs(face_width - width) <= tolerance and abs(face_height - height) <= tolerance
swapped = abs(face_width - height) <= tolerance and abs(face_height - width) <= tolerance
if direct or swapped:
return face_id
raise SystemExit(f"no plane Face near {width:g} x {height:g}")
def _nearest_plane_center(
model: StepModel,
target: tuple[float, float, float],
) -> tuple[int, tuple[float, float, float], float]:
best: tuple[int, tuple[float, float, float], float] | None = None
for face_id, _area, _width, _height, center in _plane_metrics(model):
error = _distance(center, target)
if best is None or error < best[2]:
best = (face_id, center, error)
if best is None:
raise SystemExit("no plane Face center could be measured")
return best
def _assert_spec_readback(
model: StepModel,
face_id: int,
key: str,
expected: float,
tolerance: float = 1e-5,
) -> None:
info = model.face_info(face_id)
spec = _spec(face_id, info, key)
_assert_close(f"{key} current_raw", spec.get("current_raw"), expected, tolerance)
_assert_close(f"{key} target_text", spec.get("target_text"), expected, tolerance)
if spec.get("status_text") == "不可修改":
raise SystemExit(f"{key} should remain editable after readback: {spec}")
def main() -> int:
model = StepModel.load(DEFAULT_MODEL)
face_id = _first_plane_face_near_size(model, 10.0, 10.0)
model.resize_face_size_local(face_id, 15.0, "width")
face_id = _first_plane_face_near_size(model, 15.0, 10.0)
_assert_spec_readback(model, face_id, "local_face_width", 15.0)
model = StepModel.load(DEFAULT_MODEL)
face_id = _first_plane_face_near_size(model, 10.0, 10.0)
model.resize_face_area_local(face_id, 144.0)
for candidate_id, area, _width, _height, _center in _plane_metrics(model):
if abs(area - 144.0) <= 1e-5:
_assert_spec_readback(model, candidate_id, "area", 144.0)
break
else:
raise SystemExit("no plane Face near area 144 after local area resize")
model = StepModel.load(DEFAULT_MODEL)
face_id = _first_plane_face_near_size(model, 10.0, 10.0)
info = model.face_info(face_id)
center = info.get("area_center") or info.get("bbox_center")
if not isinstance(center, tuple) or len(center) != 3:
raise SystemExit("selected Face lacks a stable center")
target_center = (float(center[0]) + 2.0, float(center[1]), float(center[2]) + 3.0)
model.move_face_center_local(face_id, target_center)
moved_face_id, _moved_center, center_error = _nearest_plane_center(model, target_center)
if center_error > 1e-5:
raise SystemExit(f"no plane Face near moved center {target_center}")
moved_info = model.face_info(moved_face_id)
center_spec = _spec(moved_face_id, moved_info, "face_center_position")
if str(center_spec.get("target_text") or "").replace(" ", "") != "7,5,3":
raise SystemExit(f"Face center target_text did not read back the moved center: {center_spec}")
for strategy in ("local", "owning", "push_pull"):
model = StepModel.load(DEFAULT_MODEL)
face_id = _first_plane_face_near_size(model, 10.0, 10.0)
info = model.face_info(face_id)
frame = model.face_plane_offset_frame(face_id)
if frame is None:
raise SystemExit("selected Face lacks a stable plane offset frame")
_origin, direction, old_position = frame
center = info.get("area_center") or info.get("bbox_center")
if not isinstance(center, tuple) or len(center) != 3:
raise SystemExit("selected Face lacks a stable center")
target_center = (
float(center[0]) + direction[0],
float(center[1]) + direction[1],
float(center[2]) + direction[2],
)
if strategy == "local":
model.move_face_plane_offset_local(face_id, 1.0)
elif strategy == "owning":
model.translate_face_plane_offset_owning(face_id, 1.0)
else:
model.push_pull_face(face_id, 1.0)
moved_face_id, _moved_center, center_error = _nearest_plane_center(model, target_center)
if center_error > 1e-5:
raise SystemExit(f"no plane Face near offset target for strategy {strategy}")
moved_info = model.face_info(moved_face_id)
spec = _spec(moved_face_id, moved_info, "face_target_normal_position")
current_raw = _float(spec.get("current_raw"), f"{strategy} face_target_normal_position current_raw")
target_text = _float(spec.get("target_text"), f"{strategy} face_target_normal_position target_text")
if abs(current_raw) <= 1e-6:
raise SystemExit(f"{strategy} offset readback still looks unchanged: {spec}")
if abs(current_raw - target_text) > 1e-5:
raise SystemExit(f"{strategy} offset target_text should match refreshed current value: {spec}")
if abs(abs(current_raw) - abs(old_position + 1.0)) > 1e-5:
raise SystemExit(f"{strategy} offset readback has unexpected plane position: {spec}")
print("Face property readback ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())