feat: 完善 Face 一级编辑与隔离计算
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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}",
|
||||
)
|
||||
|
||||
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}",
|
||||
)
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user