feat: 完善 Face 一级关系编辑和稳定性校验

This commit is contained in:
2026-08-04 18:15:29 +08:00
parent 5799d5d813
commit a76282d7dd
28 changed files with 6872 additions and 270 deletions
+112 -18
View File
@@ -9,6 +9,7 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from step_editor.window_state import WindowStateMixin
from step_editor.ui_helpers import _format_value
class _PropertySpecProbe(WindowStateMixin):
@@ -315,6 +316,10 @@ def _assert_holed_plane_local_scopes_disabled() -> None:
def main() -> int:
relation_depths_text = _format_value(("second-level", "third-level", "deeper"))
if "second-level" not in relation_depths_text or "deeper" not in relation_depths_text:
raise SystemExit(f"relation-depth tuple should render as text: {relation_depths_text!r}")
plane_info = {
"surface": "plane",
"area": 100.0,
@@ -485,6 +490,68 @@ def main() -> int:
_assert_no_generic_face_leak(cylinder_keys, "cylindrical hole feature")
_assert_contains(cylinder_keys, {"diameter", "hole_cylinder_radius"}, "cylindrical hole feature")
cylinder_topology_info = {
"surface": "cylinder",
"feature_guess": "hole/groove candidate",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 6.283185307179586,
"area": 188.0,
"area_center": (0.0, 0.0, 0.0),
"bbox_center": (0.0, 0.0, 0.0),
"axis": (0.0, 0.0, 1.0),
"axis_point": (0.0, 0.0, 0.0),
"axis_center": (0.0, 0.0, 5.0),
"topology_relation_depth": 1,
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level",
"topology_relation_status": "ready",
"topology_ignored_relation_note": "当前阶段只传播一级关系;二级、三级拓扑暂不自动递归编辑。",
"cylindrical_feature_side_face_count": 1,
"cylindrical_feature_boundary_edge_count": 2,
"cylindrical_feature_boundary_vertex_count": 4,
"cylindrical_feature_adjacent_face_count": 2,
"cylindrical_feature_end_face_count": 2,
"cylindrical_feature_opening_face_count": 2,
"first_level_topology_note": "Cylindrical side Faces=1, boundary Edges=2, boundary Vertices=4, direct adjacent Faces=2.",
}
cylinder_topology_specs = _specs(cylinder_topology_info)
cylinder_topology_spec = _spec(cylinder_topology_specs, "cylindrical_feature_first_level_topology")
cylinder_topology_text = str(cylinder_topology_spec.get("current_text") or "")
for fragment in ("侧壁 Face 1 个", "边界 Edge 2 条", "共享边相邻 Face 2 个"):
if fragment not in cylinder_topology_text:
raise SystemExit(f"cylindrical feature topology row should explain first-level counts: {cylinder_topology_spec}")
if "二级、三级" not in str(cylinder_topology_spec.get("disabled_tip") or ""):
raise SystemExit(f"cylindrical feature topology tip should document ignored deeper topology: {cylinder_topology_spec}")
cylinder_feature_probe = _PropertySpecProbe()
cylinder_feature_specs, _used = cylinder_feature_probe._editable_property_specs(cylinder_topology_info)
cylinder_feature_rows = cylinder_feature_probe._feature_property_specs(cylinder_feature_specs, cylinder_topology_info)
_spec(cylinder_feature_rows, "cylindrical_feature_first_level_topology")
generic_cylinder_specs = _specs(
{
"surface": "cylinder",
"feature_guess": "",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 6.283185307179586,
"height_estimate": 11.0,
"same_domain_height_estimate": 11.0,
"area": 207.0,
"area_center": (0.0, 0.0, 5.5),
"bbox_center": (0.0, 0.0, 5.5),
"axis": (0.0, 0.0, 1.0),
"axis_point": (0.0, 0.0, 0.0),
"axis_center": (0.0, 0.0, 5.5),
}
)
generic_height = _spec(generic_cylinder_specs, "cylinder_height")
if generic_height.get("scope_default") != "owning":
raise SystemExit(f"generic cylinder height should default to owning-axis resize: {generic_height}")
if generic_height.get("action") != "resize_cylindrical_height_owning_scale":
raise SystemExit(f"generic cylinder height should use owning-axis action by default: {generic_height}")
if generic_height.get("scope_text") != "调整整个特征":
raise SystemExit(f"generic cylinder height should show owning scope by default: {generic_height}")
slot_keys = _spec_keys(
{
"surface": "cylinder",
@@ -510,26 +577,31 @@ def main() -> int:
"slot/half-hole feature",
)
boss_keys = _spec_keys(
{
"surface": "cylinder",
"feature_guess": "boss/outer-round candidate",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 6.283185307179586,
"height_estimate": 5.0,
"same_domain_height_estimate": 5.0,
"area": 188.0,
"area_center": (0.0, 0.0, 0.0),
"bbox_center": (0.0, 0.0, 0.0),
"axis": (0.0, 0.0, 1.0),
"axis_point": (0.0, 0.0, 0.0),
"axis_center": (0.0, 0.0, 5.0),
"feature_start_end_face_ids": (1,),
}
)
boss_info = {
"surface": "cylinder",
"feature_guess": "boss/outer-round candidate",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 6.283185307179586,
"height_estimate": 5.0,
"same_domain_height_estimate": 5.0,
"area": 188.0,
"area_center": (0.0, 0.0, 0.0),
"bbox_center": (0.0, 0.0, 0.0),
"axis": (0.0, 0.0, 1.0),
"axis_point": (0.0, 0.0, 0.0),
"axis_center": (0.0, 0.0, 5.0),
"feature_start_end_face_ids": (1,),
}
boss_specs = _specs(boss_info)
boss_keys = {str(spec.get("key", "")) for spec in boss_specs}
_assert_no_generic_face_leak(boss_keys, "boss feature")
_assert_contains(boss_keys, {"boss_diameter", "boss_radius", "boss_height"}, "boss feature")
boss_height = _spec(boss_specs, "boss_height")
if boss_height.get("scope_default") != "owning":
raise SystemExit(f"boss height should default to owning-axis resize: {boss_height}")
if boss_height.get("action") != "resize_cylindrical_height_owning_scale":
raise SystemExit(f"boss height should use owning-axis action by default: {boss_height}")
fillet_keys = _spec_keys(
{
@@ -609,6 +681,28 @@ def main() -> int:
if surface_spec.get("current_text") != "圆锥面 / 拔模面":
raise SystemExit(f"cone surface should be displayed in user-facing Chinese, got {surface_spec}")
low_recognition_specs = _specs(
{
"surface": "sphere",
"radius": 5.0,
"diameter": 10.0,
"area": 100.0,
"area_center": (0.0, 0.0, 0.0),
"bbox_center": (0.0, 0.0, 0.0),
"center": (0.0, 0.0, 0.0),
"recognition_score": 24,
"recognition_confidence": "low",
"recognition_risk": "medium",
"recognition_blockers": "ambiguous analytic surface",
}
)
for key in ("sphere_radius", "sphere_diameter"):
spec = _spec(low_recognition_specs, key)
if spec.get("enabled"):
raise SystemExit(f"{key} should be read-only when analytic surface recognition is low: {spec}")
if "ambiguous analytic surface" not in str(spec.get("disabled_tip") or ""):
raise SystemExit(f"{key} disabled tip should explain the recognition blocker: {spec}")
_assert_target_change_detection()
_assert_holed_plane_local_scopes_disabled()
_assert_no_legacy_face_source_terms()