2026-08-05 15:08:16 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from step_editor.window_state import WindowStateMixin, _feature_dimension_keys
|
2026-08-05 18:06:15 +08:00
|
|
|
from scripts.verify_large_stepped_cap_push_pull import _large_multi_boundary_cap_face, _large_stepped_cap_face
|
2026-08-05 15:08:16 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
|
|
|
|
|
FACE_ID = 594
|
|
|
|
|
BASE_FACE_KEYS = (
|
|
|
|
|
"area",
|
|
|
|
|
"local_face_width",
|
|
|
|
|
"local_face_height",
|
|
|
|
|
"face_center_position",
|
|
|
|
|
"face_target_normal_position",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Probe(WindowStateMixin):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 18:06:15 +08:00
|
|
|
def _feature_context(model: StepModel, face_id: int) -> dict[str, object]:
|
2026-08-05 15:08:16 +08:00
|
|
|
info = model.quick_face_info(face_id)
|
|
|
|
|
probe = object.__new__(_Probe)
|
|
|
|
|
probe.model = model
|
|
|
|
|
probe.operation_in_progress = False
|
|
|
|
|
probe.scan_in_progress = False
|
|
|
|
|
probe.load_in_progress = False
|
|
|
|
|
probe.selected_face_id = face_id
|
|
|
|
|
probe.selected_edge_id = None
|
|
|
|
|
probe.selected_kind = "feature"
|
|
|
|
|
probe.selected_part_id = int(info["part_id"])
|
|
|
|
|
probe.selected_solid_id = int(info["solid_id"])
|
|
|
|
|
probe.feature_detection_level = "current-only"
|
|
|
|
|
probe.manual_bottom_face_id = None
|
|
|
|
|
probe.manual_slot_pair_face_id = None
|
2026-08-05 18:06:15 +08:00
|
|
|
return probe._feature_context_info(face_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _feature_rows(model: StepModel, face_id: int) -> tuple[str, ...]:
|
|
|
|
|
context = _feature_context(model, face_id)
|
|
|
|
|
probe = object.__new__(_Probe)
|
|
|
|
|
probe.model = model
|
|
|
|
|
probe.operation_in_progress = False
|
|
|
|
|
probe.scan_in_progress = False
|
|
|
|
|
probe.load_in_progress = False
|
|
|
|
|
probe.selected_face_id = face_id
|
|
|
|
|
probe.selected_edge_id = None
|
|
|
|
|
probe.selected_kind = "feature"
|
|
|
|
|
probe.selected_part_id = int(context["part_id"])
|
|
|
|
|
probe.selected_solid_id = int(context["solid_id"])
|
|
|
|
|
probe.feature_detection_level = "current-only"
|
|
|
|
|
probe.manual_bottom_face_id = None
|
|
|
|
|
probe.manual_slot_pair_face_id = None
|
2026-08-05 15:08:16 +08:00
|
|
|
specs, _used = probe._editable_property_specs(context)
|
|
|
|
|
rows = probe._feature_context_property_specs(specs, context)
|
|
|
|
|
return tuple(str(row.get("key")) for row in rows if row.get("parameter_role") == "dimension")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assert_contains(keys: tuple[str, ...], expected: tuple[str, ...], label: str) -> None:
|
|
|
|
|
missing = [key for key in expected if key not in keys]
|
|
|
|
|
if missing:
|
|
|
|
|
raise AssertionError(f"{label} missing {missing}, got {keys}")
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 18:06:15 +08:00
|
|
|
def _assert_face_594_stays_stable_after_remote_cap_edit() -> None:
|
|
|
|
|
model = StepModel.load(MODEL_PATH)
|
|
|
|
|
source_face_id = _large_stepped_cap_face(model)
|
|
|
|
|
target_face_id = _large_multi_boundary_cap_face(model)
|
|
|
|
|
if target_face_id != FACE_ID:
|
|
|
|
|
raise AssertionError(f"expected large multi-boundary cap to be Face {FACE_ID}, got {target_face_id}")
|
|
|
|
|
|
|
|
|
|
target_logical_id = model.face_region_logical_id(target_face_id)
|
|
|
|
|
before_rows = _feature_rows(model, target_face_id)
|
|
|
|
|
before_info = model.quick_face_info(target_face_id)
|
|
|
|
|
model.push_pull_face(source_face_id, 89.0)
|
|
|
|
|
|
|
|
|
|
resolved_face_id = model.resolve_face_selection_id(target_logical_id)
|
|
|
|
|
if resolved_face_id is None:
|
|
|
|
|
raise AssertionError(f"remote cap edit lost logical Face {target_logical_id}")
|
|
|
|
|
after_info = model.quick_face_info(resolved_face_id)
|
|
|
|
|
if after_info.get("surface") != "plane":
|
|
|
|
|
raise AssertionError(
|
|
|
|
|
f"logical Face {target_logical_id} should still point to the multi-boundary plane, "
|
|
|
|
|
f"got Face {resolved_face_id}: {after_info}"
|
|
|
|
|
)
|
|
|
|
|
if int(after_info.get("inner_boundary_wires", 0) or 0) < int(before_info.get("inner_boundary_wires", 0) or 0):
|
|
|
|
|
raise AssertionError(
|
|
|
|
|
f"logical Face {target_logical_id} lost inner boundary wires after remote edit: "
|
|
|
|
|
f"before={before_info}, after={after_info}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
after_rows = _feature_rows(model, resolved_face_id)
|
|
|
|
|
if after_rows != before_rows:
|
|
|
|
|
raise AssertionError(
|
|
|
|
|
"Face 594 current-only feature rows changed after editing a remote cylindrical cap: "
|
|
|
|
|
f"before={before_rows}, after={after_rows}, resolved={resolved_face_id}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
context = _feature_context(model, resolved_face_id)
|
|
|
|
|
if context.get("selection_display_id") != target_logical_id:
|
|
|
|
|
raise AssertionError(f"feature context should keep logical Face ID {target_logical_id}: {context}")
|
|
|
|
|
if context.get("selection_topological_face_id") != resolved_face_id:
|
|
|
|
|
raise AssertionError(f"feature context should expose current topological Face {resolved_face_id}: {context}")
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 15:08:16 +08:00
|
|
|
def main() -> int:
|
|
|
|
|
model = StepModel.load(MODEL_PATH)
|
|
|
|
|
if FACE_ID >= len(model.faces):
|
|
|
|
|
raise AssertionError(f"test model has no Face {FACE_ID}")
|
|
|
|
|
|
|
|
|
|
quick_info = model.quick_face_info(FACE_ID)
|
|
|
|
|
if quick_info.get("surface") != "plane":
|
|
|
|
|
raise AssertionError(f"Face {FACE_ID} should be planar in the baseline model: {quick_info}")
|
|
|
|
|
|
|
|
|
|
before_cached_rows = _feature_rows(model, FACE_ID)
|
|
|
|
|
_assert_contains(before_cached_rows, BASE_FACE_KEYS, "Face 594 before full feature cache")
|
|
|
|
|
|
|
|
|
|
full_info = model.feature_info(FACE_ID)
|
|
|
|
|
if full_info.get("shell_region_status") == "candidate":
|
|
|
|
|
dimension_keys = _feature_dimension_keys(full_info)
|
|
|
|
|
_assert_contains(dimension_keys, BASE_FACE_KEYS, "shell candidate feature dimensions")
|
|
|
|
|
if "shell_thickness_estimate" not in dimension_keys:
|
|
|
|
|
raise AssertionError(f"shell candidate should keep shell thickness as an extra dimension: {dimension_keys}")
|
|
|
|
|
|
|
|
|
|
after_cached_rows = _feature_rows(model, FACE_ID)
|
|
|
|
|
_assert_contains(after_cached_rows, BASE_FACE_KEYS, "Face 594 after full feature cache")
|
|
|
|
|
if before_cached_rows != after_cached_rows:
|
|
|
|
|
raise AssertionError(
|
|
|
|
|
"Face 594 current-only feature rows changed after full recognition cache: "
|
|
|
|
|
f"before={before_cached_rows}, after={after_cached_rows}"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-05 18:06:15 +08:00
|
|
|
_assert_face_594_stays_stable_after_remote_cap_edit()
|
|
|
|
|
|
2026-08-05 15:08:16 +08:00
|
|
|
print("Face feature parameter consistency ok")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|