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

171 lines
8.0 KiB
Python
Raw Normal View History

from __future__ import annotations
import sys
import tempfile
from pathlib import Path
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
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.step_io import _write_step
from step_editor.window_state import WindowStateMixin
SOURCE_WIDTH = 10.0
SOURCE_HEIGHT = 20.0
TARGET_WIDTH = 14.0
TARGET_HEIGHT = 26.0
TOLERANCE = 2e-4
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 _write_rectangular_box(path: Path) -> None:
shape = BRepPrimAPI_MakeBox(20.0, 10.0, 6.0).Shape()
_write_step(shape, path)
def _plane_infos(model: StepModel) -> list[tuple[int, float, float]]:
infos: list[tuple[int, float, float]] = []
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
width = float(info.get("local_face_width") or 0.0)
height = float(info.get("local_face_height") or 0.0)
infos.append((face_id, width, height))
return infos
def _face_near_size(model: StepModel, width: float, height: float) -> int:
for face_id, face_width, face_height in _plane_infos(model):
if abs(face_width - width) <= TOLERANCE and abs(face_height - height) <= TOLERANCE:
return face_id
raise SystemExit(f"no plane Face near width={width:g}, height={height:g}; got {_plane_infos(model)}")
def _assert_close(label: str, value: object, expected: float) -> None:
try:
number = float(value)
except (TypeError, ValueError) as exc:
raise SystemExit(f"{label} should be numeric, got {value!r}") from exc
if abs(number - expected) > TOLERANCE:
raise SystemExit(f"{label} should be {expected:g}, got {number:g}")
def _assert_single_solid(model: StepModel, label: str) -> None:
stats = model.stats()
if stats.solids != 1:
raise SystemExit(f"{label} should keep one Solid, got {stats}")
def _assert_size_specs(model: StepModel, face_id: int, width: float, height: float, label: str) -> None:
info = model.face_info(face_id)
probe = _PropertySpecProbe(face_id, info)
specs, _used = probe._editable_property_specs(info)
by_key = {str(spec.get("key")): spec for spec in specs}
width_spec = by_key.get("local_face_width")
height_spec = by_key.get("local_face_height")
if width_spec is None or height_spec is None:
raise SystemExit(f"{label} should expose width and height specs: {sorted(by_key)}")
_assert_close(f"{label} width current_raw", width_spec.get("current_raw"), width)
_assert_close(f"{label} width target_text", width_spec.get("target_text"), width)
_assert_close(f"{label} height current_raw", height_spec.get("current_raw"), height)
_assert_close(f"{label} height target_text", height_spec.get("target_text"), height)
if width_spec.get("status_text") == "不可修改" or height_spec.get("status_text") == "不可修改":
raise SystemExit(f"{label} width/height should remain editable: {width_spec} {height_spec}")
def _axis_index(direction: object) -> int:
if not isinstance(direction, tuple) or len(direction) != 3:
raise SystemExit(f"axis direction should be a vector, got {direction!r}")
values = [abs(float(direction[0])), abs(float(direction[1])), abs(float(direction[2]))]
return max(range(3), key=lambda index: values[index])
def _bbox_size(model: StepModel) -> tuple[float, float, float]:
return tuple(float(value) for value in model.geometry_stats()["bbox_size"])
def _run_local_case(path: Path, axis: str, target_size: float, expected_width: float, expected_height: float) -> None:
model = StepModel.load(path)
face_id = _face_near_size(model, SOURCE_WIDTH, SOURCE_HEIGHT)
before = model.stats()
plan = model.face_size_local_resize_plan(face_id, target_size, axis)
if plan.get("status") == "blocked":
raise SystemExit(f"local rectangular {axis} plan was blocked: {plan}")
expected_strategy = f"local-face-{axis}-only-deform"
if plan.get("resize_strategy") != expected_strategy:
raise SystemExit(f"local rectangular {axis} expected {expected_strategy}, got {plan}")
result = model.resize_face_size_local(face_id, target_size, axis)
_assert_single_solid(model, f"local rectangular {axis}")
if model.stats().faces != before.faces:
raise SystemExit(f"local rectangular {axis} should keep face count stable: before={before}, after={model.stats()}")
edited_face_id = _face_near_size(model, expected_width, expected_height)
_assert_size_specs(model, edited_face_id, expected_width, expected_height, f"local rectangular {axis}")
print(f"local {axis}: Face {face_id} -> {edited_face_id}, size={expected_width:g} x {expected_height:g}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _run_owning_case(path: Path, axis: str, target_size: float, expected_width: float, expected_height: float) -> None:
model = StepModel.load(path)
face_id = _face_near_size(model, SOURCE_WIDTH, SOURCE_HEIGHT)
before = model.stats()
before_bbox = _bbox_size(model)
plan = model.face_size_owning_scale_plan(face_id, target_size, axis)
if plan.get("status") == "blocked":
raise SystemExit(f"owning rectangular {axis} plan was blocked: {plan}")
expected_strategy = f"axis-scale-owning-shape-from-face-{axis}"
if plan.get("resize_strategy") != expected_strategy:
raise SystemExit(f"owning rectangular {axis} expected {expected_strategy}, got {plan}")
if plan.get("owning_face_size_rebuild_mode") != "planar-rebuild":
raise SystemExit(f"simple rectangular box should use planar rebuild for owning {axis}: {plan}")
resized_axis = _axis_index(plan.get("face_size_axis_direction"))
result = model.resize_face_size_owning_scale(face_id, target_size, axis)
_assert_single_solid(model, f"owning rectangular {axis}")
if model.stats().faces != before.faces:
raise SystemExit(f"owning rectangular {axis} should keep face count stable: before={before}, after={model.stats()}")
after_bbox = _bbox_size(model)
_assert_close(f"owning rectangular {axis} bbox axis {resized_axis}", after_bbox[resized_axis], target_size)
for index, before_size in enumerate(before_bbox):
if index == resized_axis:
continue
_assert_close(f"owning rectangular {axis} unchanged bbox axis {index}", after_bbox[index], before_size)
edited_face_id = _face_near_size(model, expected_width, expected_height)
_assert_size_specs(model, edited_face_id, expected_width, expected_height, f"owning rectangular {axis}")
print(f"owning {axis}: Face {face_id} -> {edited_face_id}, size={expected_width:g} x {expected_height:g}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def main() -> int:
with tempfile.TemporaryDirectory(prefix="geom_param_face_rect_axes_") as temp_dir:
path = Path(temp_dir) / "rectangular_box.step"
_write_rectangular_box(path)
_run_local_case(path, "width", TARGET_WIDTH, TARGET_WIDTH, SOURCE_HEIGHT)
_run_local_case(path, "height", TARGET_HEIGHT, SOURCE_WIDTH, TARGET_HEIGHT)
_run_owning_case(path, "width", TARGET_WIDTH, TARGET_WIDTH, SOURCE_HEIGHT)
_run_owning_case(path, "height", TARGET_HEIGHT, SOURCE_WIDTH, TARGET_HEIGHT)
print("rectangular Face width/height axis semantics ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())