feat: 完善 Face 参数化编辑和隔离执行
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user