feat: 完善 Face 一级编辑与隔离计算

This commit is contained in:
2026-08-05 15:08:16 +08:00
parent a76282d7dd
commit ed1faf51d8
28 changed files with 3740 additions and 876 deletions
+252
View File
@@ -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())