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.info_panel import InfoPanelMixin from step_editor.ui_helpers import INFO_LABELS from step_editor.window_state import WindowStateMixin class _SelectionProbe(InfoPanelMixin, WindowStateMixin): pass class _LogicalFaceModel: def __init__(self, logical_id: int) -> None: self.logical_id = logical_id def face_region_logical_id(self, _face_id: int) -> int: return self.logical_id def face_logical_id(self, _face_id: int) -> int: return self.logical_id class _FeatureContextModel(_LogicalFaceModel): faces = [object()] * 800 face_part_ids = [1] * 800 face_solid_ids = [0] * 800 def quick_face_info(self, face_id: int) -> dict[str, object]: return { "kind": "face", "face_id": face_id, "topological_face_id": face_id, "logical_face_id": self.logical_id, "surface": "plane", "part_id": 1, "solid_id": 0, "area": 100.0, "area_center": (5.0, 5.0, 0.0), "local_face_width": 10.0, "local_face_height": 10.0, "feature_source_face_id": face_id, "feature_highlight_face_ids": (face_id,), } def cached_feature_info(self, _face_id: int) -> dict[str, object] | None: return None def face_first_level_topology(self, face_id: int) -> dict[str, object]: return { "topology_relation_depth": 1, "topology_relation_model": "STEP/B-Rep shared-edge first-level", "topology_relation_scope": "selected same-domain region + direct shared-edge adjacent Faces", "topology_relation_boundary": "shared-edge", "topology_ignored_relation_depths": ("second-level", "third-level", "deeper"), "topology_ignored_relation_note": "当前阶段只传播一级关系;二级、三级拓扑暂不自动递归编辑。", "same_domain_face_ids": (face_id,), "same_domain_face_count": 1, "same_domain_region_kind": "single-face", "first_level_boundary_edge_ids": (1, 2, 3, 4), "first_level_boundary_edge_count": 4, "first_level_boundary_vertex_count": 4, "first_level_adjacent_face_ids": (10, 11, 12, 13), "first_level_adjacent_face_count": 4, "first_level_face_ids": (face_id, 10, 11, 12, 13), "first_level_face_count": 5, "first_level_topology_note": "已识别当前 Face 区域 1 个 Face、边界 Edge 4 条、边界 Vertex 4 个、共享边一级相邻 Face 4 个。", } def face_first_level_facts(self, face_id: int, scope: str = "auto") -> dict[str, object]: return { "first_level_fact_model": "STEP/B-Rep first-level fact graph", "first_level_fact_status": "ready", "first_level_fact_relation_depth": 1, "first_level_fact_scope": scope, "first_level_fact_relation_boundary": "shared-edge", "first_level_fact_subject_face_ids": (face_id,), "first_level_fact_subject_face_count": 1, "first_level_fact_boundary_edge_count": 4, "first_level_fact_boundary_vertex_count": 4, "first_level_fact_adjacent_face_ids": (10, 11, 12, 13), "first_level_fact_adjacent_face_count": 4, "first_level_fact_included_face_count": 5, "first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"), "first_level_fact_summary": "一级事实=当前 Face、边界和共享边相邻 Face。", } def _recognition_summary_fields(self, _info: dict[str, object]) -> dict[str, object]: return {"recognition_summary": "识别摘要包含一级事实。"} class _FeatureRefreshProbe(WindowStateMixin): def __init__(self) -> None: self.model = _FeatureContextModel(594) self.selected_kind = "feature" self.selected_face_id = 637 self.selected_edge_id = None self.selected_part_id = 1 self.selected_solid_id = 0 self.selected_pick_position = (1.0, 2.0, 3.0) self.feature_detection_level = "current-only" self.set_info_payload: dict[str, object] | None = None self.highlighted_face_ids: tuple[int, ...] = () self.synced_id: tuple[str, int] | None = None def set_info(self, info: dict[str, object]) -> None: self.set_info_payload = dict(info) self.current_info_values = dict(info) def _highlight_faces(self, face_ids: list[int] | tuple[int, ...] | None = None, **_kwargs) -> None: self.highlighted_face_ids = tuple(int(item) for item in (face_ids or ())) def _update_selected_object_title(self) -> None: return def _sync_id_picker(self, kind: str, target_id: int) -> None: self.synced_id = (kind, int(target_id)) def _probe(kind: str, selected_face_id: int, info: dict[str, object]) -> _SelectionProbe: probe = object.__new__(_SelectionProbe) probe.model = None probe.selected_kind = kind probe.selected_part_id = None probe.selected_solid_id = None probe.selected_face_id = selected_face_id probe.selected_edge_id = None probe.current_info_values = dict(info) return probe def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def main() -> int: for key in ("selection_title", "selection_display_id", "selection_topological_face_id"): _assert(key in INFO_LABELS, f"{key} should have a readable UI label") _assert(INFO_LABELS.get("face_id") == "当前拓扑 Face ID", "raw face_id should be labelled as topological") info = { "feature_type": "可拉伸/切除平面候选", "selection_display_id": 594, "selection_topological_face_id": 637, "face_region_logical_id": 594, "logical_face_id": 594, } feature_probe = _probe("feature", 637, info) _assert( feature_probe._selected_id_text() == "face 594", f"feature copy ID should prefer logical Face ID, got {feature_probe._selected_id_text()!r}", ) feature_title = feature_probe._selected_object_title_suffix() _assert( "Face 594" in feature_title and "拓扑 637" in feature_title, f"feature title should show logical and topological IDs: {feature_title}", ) _assert("候选" not in feature_title, f"feature title should use a user-facing label, got: {feature_title}") _assert("可拉伸/切除平面" in feature_title, f"feature title should keep the editable feature name: {feature_title}") face_probe = _probe("face", 637, info) _assert( face_probe._selected_id_text() == "face 594", f"Face copy ID should prefer logical Face ID, got {face_probe._selected_id_text()!r}", ) face_title = face_probe._selected_object_title_suffix() _assert( "Face 594" in face_title and "拓扑 637" in face_title, f"Face title should show logical and topological IDs: {face_title}", ) _assert("候选" not in face_title, f"Face title should not expose candidate wording: {face_title}") fallback_probe = _probe("feature", 637, {"face_region_logical_id": 594}) _assert(fallback_probe._selected_id_text() == "face 594", "logical Face ID fallback should be copied") selection_only_probe = _probe("feature", 637, {"selection_display_id": 594}) _assert( selection_only_probe._selected_id_text() == "face 594", "selection_display_id should be enough for copied Face ID", ) selection_only_title = selection_only_probe._selected_object_title_suffix() _assert( "Face 594" in selection_only_title and "拓扑 637" in selection_only_title, f"selection_display_id should be enough for feature title: {selection_only_title}", ) face_selection_only_probe = _probe("face", 637, {"selection_display_id": 594}) face_selection_only_title = face_selection_only_probe._selected_object_title_suffix() _assert( "Face 594" in face_selection_only_title and "拓扑 637" in face_selection_only_title, f"selection_display_id should be enough for Face title: {face_selection_only_title}", ) raw_probe = _probe("feature", 637, {}) _assert(raw_probe._selected_id_text() == "face 637", "topological Face ID fallback should still work") helper_probe = _probe("feature", 637, {}) helper_probe.model = _LogicalFaceModel(594) fields = helper_probe._selection_identity_fields(637, "特征来源 Face") _assert(fields.get("selection_display_id") == 594, f"helper should expose logical Face ID: {fields}") _assert( fields.get("selection_topological_face_id") == 637, f"helper should keep current topological Face ID: {fields}", ) _assert( fields.get("selection_title") == "特征来源 Face 594(当前拓扑 Face 637)", f"helper title should explain both IDs: {fields}", ) refresh_probe = _FeatureRefreshProbe() context = refresh_probe._feature_context_info(637) _assert(context.get("selection_display_id") == 594, f"feature context should include logical ID: {context}") _assert( context.get("selection_topological_face_id") == 637, f"feature context should include topological ID: {context}", ) refresh_probe._on_feature_detection_level_changed() payload = refresh_probe.set_info_payload or {} _assert(payload.get("selection_display_id") == 594, f"detection refresh should call set_info with identity: {payload}") _assert(payload.get("pick_position") == (1.0, 2.0, 3.0), f"detection refresh should preserve pick: {payload}") _assert(refresh_probe.highlighted_face_ids == (637,), f"detection refresh should rehighlight source Face") source_probe = _FeatureRefreshProbe() source_probe.selected_face_id = 100 source_probe.selected_pick_position = (9.0, 9.0, 9.0) source_probe._activate_property_source_feature( { "source_face_id": 637, "source_feature_info": { "feature_type": "可拉伸/切除平面候选", "part_id": 1, "solid_id": 0, "feature_highlight_face_ids": (637,), }, } ) source_payload = source_probe.set_info_payload or {} _assert(source_probe.synced_id == ("Feature", 594), f"associated jump should sync logical ID: {source_probe.synced_id}") _assert(source_payload.get("selection_display_id") == 594, f"associated jump should keep logical ID: {source_payload}") _assert("pick_position" not in source_payload, f"associated jump should not reuse stale pick position: {source_payload}") print("selection identity UI ok") return 0 if __name__ == "__main__": raise SystemExit(main())