feat: 完善 Edge 一级编辑与 CAD 建模语义

This commit is contained in:
2026-08-05 18:06:15 +08:00
parent ed1faf51d8
commit 57d75541c3
19 changed files with 2365 additions and 367 deletions
+181 -51
View File
@@ -37,6 +37,15 @@ def _specs(info: dict[str, object]) -> list[dict[str, object]]:
return specs
def _edge_specs(info: dict[str, object]) -> list[dict[str, object]]:
probe = _PropertySpecProbe()
probe.selected_kind = "edge"
probe.selected_face_id = None
probe.selected_edge_id = 7
specs, _used = probe._editable_property_specs(info)
return specs
def _display_specs(info: dict[str, object]) -> list[dict[str, object]]:
probe = _PropertySpecProbe()
probe.selected_kind = "face"
@@ -80,6 +89,19 @@ def _assert_label(specs: list[dict[str, object]], key: str, label: str) -> None:
raise SystemExit(f"{key} label should be {label!r}, got {actual!r}")
def _assert_current_text_contains(
specs: list[dict[str, object]],
key: str,
fragments: tuple[str, ...],
label: str,
) -> None:
spec = _spec(specs, key)
text = str(spec.get("current_text") or "")
missing = [fragment for fragment in fragments if fragment not in text]
if missing:
raise SystemExit(f"{label} {key} text missing {missing}: {text!r}")
def _assert_hard_range(specs: list[dict[str, object]], key: str, low: float, high: float) -> None:
spec = _spec(specs, key)
actual_low = spec.get("min_value")
@@ -167,6 +189,8 @@ def _assert_actionable_rows_first(info: dict[str, object], expected_keys: tuple[
seen_non_actionable = False
front_keys: list[str] = []
for spec in display_specs:
if bool(spec.get("pin_top")):
continue
key = str(spec.get("key", ""))
if _is_actionable_edit_spec(spec):
if seen_non_actionable:
@@ -368,6 +392,25 @@ def main() -> int:
}
plane_specs = _specs(plane_info)
plane_keys = {str(spec.get("key", "")) for spec in plane_specs}
_assert_label(plane_specs, "cad_modeling_form", "建模形式")
_assert_current_text_contains(
plane_specs,
"cad_modeling_form",
("柔性建模", "拉伸切除", "偏移变换"),
"plane Face",
)
_assert_label(plane_specs, "cad_recommended_operation", "推荐操作")
_assert_current_text_contains(
plane_specs,
"cad_recommended_operation",
("优先改偏移变换", "拉伸/切除"),
"plane Face",
)
plane_display_specs = _display_specs(plane_info)
if str(plane_display_specs[0].get("key", "")) != "cad_modeling_form":
raise SystemExit(f"plane Face should show CAD modeling form first: {plane_display_specs[0]}")
if str(plane_display_specs[1].get("key", "")) != "cad_recommended_operation":
raise SystemExit(f"plane Face should show recommended operation second: {plane_display_specs[1]}")
_assert_contains(
plane_keys,
{
@@ -381,7 +424,7 @@ def main() -> int:
)
_assert_label(plane_specs, "local_face_width", "U向尺寸")
_assert_label(plane_specs, "local_face_height", "V向尺寸")
_assert_label(plane_specs, "face_target_normal_position", "偏移曲面")
_assert_label(plane_specs, "face_target_normal_position", "偏移变换")
_assert_actionable_rows_first(
plane_info,
(
@@ -404,6 +447,13 @@ def main() -> int:
for fragment in ("Face 区域 1 个", "边界 Edge 4 条", "共享边相邻 Face 4 个"):
if fragment not in topology_text:
raise SystemExit(f"plane feature topology row should explain first-level counts, got {topology_spec}")
_assert_label(plane_feature_rows, "face_edit_semantics", "建模意图")
_assert_label(plane_feature_rows, "cad_modeling_form", "建模形式")
_assert_label(plane_feature_rows, "cad_recommended_operation", "推荐操作")
if str(plane_feature_rows[0].get("key", "")) != "cad_modeling_form":
raise SystemExit(f"feature mode should keep CAD modeling form first: {plane_feature_rows[0]}")
if str(plane_feature_rows[1].get("key", "")) != "cad_recommended_operation":
raise SystemExit(f"feature mode should keep recommended operation second: {plane_feature_rows[1]}")
expected_plane_feature_keys = {
"area",
"local_face_width",
@@ -512,24 +562,36 @@ def main() -> int:
if shell_owning.get("label") != "缩放特征":
raise SystemExit(f"shell thickness owning scope should expose an edit semantic, got {shell_owning}")
cylinder_keys = _spec_keys(
{
"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),
"feature_bottom_face_ids": (),
}
)
cylinder_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),
"feature_bottom_face_ids": (),
}
cylinder_specs = _specs(cylinder_info)
cylinder_keys = {str(spec.get("key", "")) for spec in cylinder_specs}
_assert_no_generic_face_leak(cylinder_keys, "cylindrical hole feature")
_assert_contains(cylinder_keys, {"diameter", "hole_cylinder_radius"}, "cylindrical hole feature")
_assert_current_text_contains(
cylinder_specs,
"cad_modeling_form",
("工程特征", "", "重切"),
"cylindrical hole feature",
)
_assert_current_text_contains(
cylinder_specs,
"cad_recommended_operation",
("孔径", "盲孔", "轴心"),
"cylindrical hole feature",
)
cylinder_topology_info = {
"surface": "cylinder",
@@ -646,30 +708,42 @@ def main() -> int:
if "slot_chord_width_estimate" in split_full_hole_keys:
raise SystemExit(f"split full cylindrical hole should not be shown as a slot: {split_full_hole_keys}")
slot_keys = _spec_keys(
{
"surface": "cylinder",
"feature_guess": "hole/groove candidate",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 3.141592653589793,
"slot_chord_width_estimate": 6.0,
"slot_sagitta_depth_estimate": 3.0,
"slot_arc_length_estimate": 9.42477796076938,
"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),
}
)
slot_info = {
"surface": "cylinder",
"feature_guess": "hole/groove candidate",
"diameter": 6.0,
"radius": 3.0,
"angular_span": 3.141592653589793,
"slot_chord_width_estimate": 6.0,
"slot_sagitta_depth_estimate": 3.0,
"slot_arc_length_estimate": 9.42477796076938,
"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),
}
slot_specs = _specs(slot_info)
slot_keys = {str(spec.get("key", "")) for spec in slot_specs}
_assert_no_generic_face_leak(slot_keys, "slot/half-hole feature")
_assert_contains(
slot_keys,
{"slot_chord_width_estimate", "slot_sagitta_depth_estimate", "slot_arc_length_estimate"},
"slot/half-hole feature",
)
_assert_current_text_contains(
slot_specs,
"cad_modeling_form",
("工程特征", "", "局部重建"),
"slot/half-hole feature",
)
_assert_current_text_contains(
slot_specs,
"cad_recommended_operation",
("槽宽", "槽深", "轴心"),
"slot/half-hole feature",
)
boss_info = {
"surface": "cylinder",
@@ -691,30 +765,42 @@ def main() -> int:
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")
_assert_current_text_contains(
boss_specs,
"cad_modeling_form",
("工程特征", "凸台", "局部重建"),
"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(
{
"surface": "cylinder",
"feature_guess": "round/fillet candidate",
"diameter": 2.0,
"radius": 1.0,
"angular_span": 1.5707963267948966,
"existing_fillet_radius": 1.0,
"feature_existing_fillet_support_face_ids": (1, 2),
"area": 3.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),
}
)
fillet_info = {
"surface": "cylinder",
"feature_guess": "round/fillet candidate",
"diameter": 2.0,
"radius": 1.0,
"angular_span": 1.5707963267948966,
"existing_fillet_radius": 1.0,
"feature_existing_fillet_support_face_ids": (1, 2),
"area": 3.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),
}
fillet_specs = _specs(fillet_info)
fillet_keys = {str(spec.get("key", "")) for spec in fillet_specs}
_assert_no_generic_face_leak(fillet_keys, "existing fillet feature")
_assert_contains(fillet_keys, {"existing_fillet_radius_estimate"}, "existing fillet feature")
_assert_current_text_contains(
fillet_specs,
"cad_modeling_form",
("工程特征", "倒圆角", "重新倒圆"),
"existing fillet feature",
)
analytic_cases = (
(
@@ -766,6 +852,27 @@ def main() -> int:
keys = {str(spec.get("key", "")) for spec in specs}
_assert_no_generic_face_leak(keys, label)
_assert_contains(keys, required, label)
if label == "cone feature":
_assert_current_text_contains(
specs,
"cad_modeling_form",
("工程特征", "拔模", "锥孔"),
label,
)
elif label == "sphere feature":
_assert_current_text_contains(
specs,
"cad_modeling_form",
("解析曲面", "缩放特征", "球面"),
label,
)
elif label == "torus feature":
_assert_current_text_contains(
specs,
"cad_modeling_form",
("解析曲面", "缩放特征", "环面"),
label,
)
if label == "cone feature":
_assert_label(specs, "cone_reference_radius", "参考半径")
_assert_label(specs, "cone_reference_diameter", "参考直径")
@@ -775,6 +882,29 @@ def main() -> int:
if surface_spec.get("current_text") != "圆锥面 / 拔模面":
raise SystemExit(f"cone surface should be displayed in user-facing Chinese, got {surface_spec}")
edge_specs = _edge_specs(
{
"curve": "line",
"length": 10.0,
"start_point": (0.0, 0.0, 0.0),
"end_point": (10.0, 0.0, 0.0),
"length_center": (5.0, 0.0, 0.0),
}
)
_assert_current_text_contains(
edge_specs,
"cad_modeling_form",
("柔性建模", "移动几何", "直线 Edge"),
"line Edge",
)
_assert_current_text_contains(
edge_specs,
"cad_recommended_operation",
("优先改长度", "移动端面", "只改当前Edge"),
"line Edge",
)
_assert_contains({str(spec.get("key", "")) for spec in edge_specs}, {"length"}, "line Edge")
low_recognition_specs = _specs(
{
"surface": "sphere",