2026-07-31 16:36:05 +08:00
|
|
|
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:
|
2026-08-07 18:08:32 +08:00
|
|
|
info = model.face_info(candidate_id)
|
|
|
|
|
_assert_close("area diagnostic value", info.get("area"), 144.0)
|
|
|
|
|
if any(spec.get("key") == "area" for spec in _specs(candidate_id, info)):
|
|
|
|
|
raise SystemExit("area should stay a diagnostic result, not an editable property spec")
|
2026-07-31 16:36:05 +08:00
|
|
|
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())
|