2026-08-07 18:08:32 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import math
|
|
|
|
|
|
from typing import Mapping
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
USER_OPERATION_PRIORITY: dict[str, int] = {
|
|
|
|
|
|
"push_pull_plane": 10,
|
|
|
|
|
|
"resize_cylinder": 20,
|
|
|
|
|
|
"resize_depth": 24,
|
|
|
|
|
|
"suppress_cylinder": 28,
|
|
|
|
|
|
"resize_slot_width": 30,
|
|
|
|
|
|
"resize_slot_depth": 31,
|
|
|
|
|
|
"resize_slot_arc_length": 34,
|
|
|
|
|
|
"resize_slot_angular_span": 35,
|
|
|
|
|
|
"resize_boss": 40,
|
|
|
|
|
|
"resize_boss_height": 41,
|
|
|
|
|
|
"move_boss_axis": 44,
|
|
|
|
|
|
"inspect_existing_fillet": 50,
|
2026-08-10 14:56:50 +08:00
|
|
|
|
"inspect_existing_chamfer": 51,
|
2026-08-07 18:08:32 +08:00
|
|
|
|
"fillet_edge": 52,
|
|
|
|
|
|
"chamfer_edge": 53,
|
|
|
|
|
|
"resize_shell_thickness": 60,
|
|
|
|
|
|
"resize_edge_length": 70,
|
|
|
|
|
|
"resize_ellipse_edge_major_radius": 72,
|
|
|
|
|
|
"resize_ellipse_edge_minor_radius": 73,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
USER_PRIORITY_BUCKETS: tuple[tuple[int, str, str], ...] = (
|
|
|
|
|
|
(10, "Face 面编辑", "最常用:平面拉伸/切除、面尺寸、中心和偏移。"),
|
|
|
|
|
|
(20, "孔", "常用:孔径、孔深、孔轴心和封堵。"),
|
|
|
|
|
|
(30, "槽/半孔", "常用:槽宽、槽深、弧长、弧角和槽轴心。"),
|
|
|
|
|
|
(40, "凸台/外圆", "常用:凸台直径、高度和轴心。"),
|
|
|
|
|
|
(50, "圆角/倒角", "常用但风险更高:已有圆角半径、新增圆角或倒角。"),
|
|
|
|
|
|
(60, "壳体厚度", "专项:相对面可识别时修改局部厚度。"),
|
|
|
|
|
|
(70, "Edge 边编辑", "受限:边长、端点、圆边半径等一级关系编辑。"),
|
|
|
|
|
|
(80, "解析曲面", "较少直接修改:圆锥、球面、环面等解析曲面参数。"),
|
|
|
|
|
|
(90, "只读/诊断", "暂未稳定归类为可修改特征。"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-14 18:42:39 +08:00
|
|
|
|
EXTERNAL_RELATION_SCORE_WEIGHTS: dict[str, int] = {
|
|
|
|
|
|
"coaxial": 8,
|
|
|
|
|
|
"tangent": 5,
|
|
|
|
|
|
"coplanar": 4,
|
|
|
|
|
|
"parallel": 3,
|
|
|
|
|
|
"perpendicular": 3,
|
|
|
|
|
|
"parallel_axis": 3,
|
|
|
|
|
|
}
|
|
|
|
|
|
EXTERNAL_RELATION_SCORE_LIMIT = 18
|
|
|
|
|
|
EXTERNAL_FEATURE_HINT_SCORE_LIMIT = 12
|
|
|
|
|
|
|
2026-08-07 18:08:32 +08:00
|
|
|
|
|
|
|
|
|
|
def _text(value: object) -> str:
|
|
|
|
|
|
return str(value or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _float_or_none(value: object) -> float | None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return float(value)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:42:39 +08:00
|
|
|
|
def _int_or_zero(value: object) -> int:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(value)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _text_values(value: object) -> tuple[str, ...]:
|
|
|
|
|
|
if value is None or value == "":
|
|
|
|
|
|
return ()
|
|
|
|
|
|
if isinstance(value, str):
|
|
|
|
|
|
return (value.strip(),) if value.strip() else ()
|
|
|
|
|
|
if isinstance(value, (list, tuple, set)):
|
|
|
|
|
|
return tuple(str(item).strip() for item in value if str(item).strip())
|
|
|
|
|
|
return ()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _relation_types_from_summary(value: object) -> tuple[str, ...]:
|
|
|
|
|
|
text = _text(value)
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return ()
|
|
|
|
|
|
result: list[str] = []
|
|
|
|
|
|
for chunk in text.replace(";", ",").split(","):
|
|
|
|
|
|
relation_type = chunk.split(":", 1)[0].strip()
|
|
|
|
|
|
if relation_type:
|
|
|
|
|
|
result.append(relation_type)
|
|
|
|
|
|
return tuple(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def external_relation_score_bonus(info: Mapping[str, object]) -> int:
|
|
|
|
|
|
relation_types = _text_values(info.get("external_recognition_relation_types")) or _text_values(
|
|
|
|
|
|
info.get("asitus_geometric_relation_types")
|
|
|
|
|
|
)
|
|
|
|
|
|
if not relation_types:
|
|
|
|
|
|
relation_types = _relation_types_from_summary(
|
|
|
|
|
|
info.get("external_recognition_relation_summary")
|
|
|
|
|
|
or info.get("asitus_geometric_relation_summary")
|
|
|
|
|
|
)
|
|
|
|
|
|
relation_count = _int_or_zero(
|
|
|
|
|
|
info.get("external_recognition_relation_count")
|
|
|
|
|
|
or info.get("asitus_geometric_relation_count")
|
|
|
|
|
|
)
|
|
|
|
|
|
score = 0
|
|
|
|
|
|
for relation_type in relation_types:
|
|
|
|
|
|
score += EXTERNAL_RELATION_SCORE_WEIGHTS.get(relation_type, 1)
|
|
|
|
|
|
if relation_count and not relation_types:
|
|
|
|
|
|
score = min(relation_count * 2, EXTERNAL_RELATION_SCORE_LIMIT)
|
|
|
|
|
|
score = max(0, min(score, EXTERNAL_RELATION_SCORE_LIMIT))
|
|
|
|
|
|
hint_score = min(_int_or_zero(info.get("analysis_situs_feature_hint_score")), EXTERNAL_FEATURE_HINT_SCORE_LIMIT)
|
|
|
|
|
|
return max(0, min(score + hint_score, EXTERNAL_RELATION_SCORE_LIMIT + EXTERNAL_FEATURE_HINT_SCORE_LIMIT))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _confidence_sort_rank(value: object) -> int:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"high": 0,
|
|
|
|
|
|
"medium": 1,
|
|
|
|
|
|
"low": 2,
|
|
|
|
|
|
"pending": 3,
|
|
|
|
|
|
"unchecked": 3,
|
|
|
|
|
|
"none": 4,
|
|
|
|
|
|
}.get(_text(value), 5)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 18:08:32 +08:00
|
|
|
|
def _is_effectively_full_cylinder(info: Mapping[str, object]) -> bool:
|
|
|
|
|
|
if bool(info.get("is_full_cylinder")):
|
|
|
|
|
|
return True
|
|
|
|
|
|
for key in ("same_domain_angular_span", "angular_span"):
|
|
|
|
|
|
angular_span = _float_or_none(info.get(key))
|
|
|
|
|
|
if angular_span is not None and angular_span >= math.tau * 0.92:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def feature_recognition_priority(info: Mapping[str, object]) -> int:
|
|
|
|
|
|
"""Rank feature candidates by likely user editing frequency.
|
|
|
|
|
|
|
|
|
|
|
|
The rank is intentionally product-facing, not a geometry confidence score:
|
|
|
|
|
|
lower numbers should appear earlier in editable-feature lists.
|
|
|
|
|
|
"""
|
|
|
|
|
|
operation_key = _text(info.get("operation_key"))
|
|
|
|
|
|
if operation_key in USER_OPERATION_PRIORITY:
|
|
|
|
|
|
return USER_OPERATION_PRIORITY[operation_key]
|
|
|
|
|
|
|
|
|
|
|
|
surface = _text(info.get("surface"))
|
|
|
|
|
|
feature_guess = _text(info.get("feature_guess"))
|
|
|
|
|
|
feature_type = _text(info.get("feature_type"))
|
|
|
|
|
|
feature_actions = _text(info.get("feature_edit_actions"))
|
|
|
|
|
|
ready_actions = _text(info.get("recognition_ready_actions"))
|
|
|
|
|
|
|
|
|
|
|
|
combined_text = ";".join(item for item in (feature_type, feature_actions, ready_actions) if item)
|
|
|
|
|
|
|
2026-08-10 14:56:50 +08:00
|
|
|
|
if _text(info.get("existing_chamfer_status")) == "candidate" or feature_guess == "chamfer candidate":
|
|
|
|
|
|
return 50
|
|
|
|
|
|
|
2026-08-07 18:08:32 +08:00
|
|
|
|
if surface == "plane":
|
|
|
|
|
|
return 10
|
|
|
|
|
|
|
|
|
|
|
|
if surface == "cylinder":
|
|
|
|
|
|
if feature_guess == "hole/groove candidate":
|
|
|
|
|
|
angular_span = _float_or_none(info.get("angular_span"))
|
|
|
|
|
|
if (
|
|
|
|
|
|
not _is_effectively_full_cylinder(info)
|
|
|
|
|
|
and angular_span is not None
|
|
|
|
|
|
and angular_span < math.tau * 0.92
|
2026-08-20 17:12:01 +08:00
|
|
|
|
) or _text(info.get("slot_kind")) == "partial-cylindrical-groove" or any(
|
|
|
|
|
|
token in feature_type for token in ("槽/半孔候选", "盲槽", "凹槽")
|
|
|
|
|
|
):
|
2026-08-07 18:08:32 +08:00
|
|
|
|
return 30
|
|
|
|
|
|
return 20
|
|
|
|
|
|
if feature_guess == "boss/outer-round candidate":
|
|
|
|
|
|
return 40
|
|
|
|
|
|
if feature_guess == "round/fillet candidate":
|
|
|
|
|
|
return 50
|
|
|
|
|
|
if "孔" in combined_text:
|
|
|
|
|
|
return 20
|
|
|
|
|
|
if "槽" in combined_text or "半孔" in combined_text:
|
|
|
|
|
|
return 30
|
|
|
|
|
|
if "凸台" in combined_text or "外圆" in combined_text:
|
|
|
|
|
|
return 40
|
|
|
|
|
|
if "圆角" in combined_text or "倒圆" in combined_text:
|
|
|
|
|
|
return 50
|
|
|
|
|
|
return 90
|
|
|
|
|
|
|
|
|
|
|
|
if surface == "edge" or _text(info.get("curve")):
|
|
|
|
|
|
if "圆角" in combined_text:
|
|
|
|
|
|
return 52
|
|
|
|
|
|
if "倒角" in combined_text:
|
|
|
|
|
|
return 53
|
|
|
|
|
|
return 70
|
|
|
|
|
|
|
|
|
|
|
|
if surface in {"cone", "sphere", "torus"}:
|
|
|
|
|
|
return 80
|
|
|
|
|
|
|
|
|
|
|
|
if "壳体" in combined_text or _text(info.get("shell_region_status")) == "candidate":
|
|
|
|
|
|
return 60
|
|
|
|
|
|
|
|
|
|
|
|
return 90
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def feature_recognition_priority_bucket(priority: int) -> tuple[int, str, str]:
|
|
|
|
|
|
selected = USER_PRIORITY_BUCKETS[-1]
|
|
|
|
|
|
for bucket in USER_PRIORITY_BUCKETS:
|
|
|
|
|
|
if priority >= bucket[0]:
|
|
|
|
|
|
selected = bucket
|
|
|
|
|
|
else:
|
|
|
|
|
|
break
|
|
|
|
|
|
return selected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def feature_recognition_priority_label(info: Mapping[str, object]) -> str:
|
|
|
|
|
|
priority = feature_recognition_priority(info)
|
|
|
|
|
|
_start, label, _reason = feature_recognition_priority_bucket(priority)
|
|
|
|
|
|
return f"{priority:02d} · {label}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def feature_recognition_priority_reason(info: Mapping[str, object]) -> str:
|
|
|
|
|
|
priority = feature_recognition_priority(info)
|
|
|
|
|
|
_start, _label, reason = feature_recognition_priority_bucket(priority)
|
|
|
|
|
|
return reason
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:42:39 +08:00
|
|
|
|
def feature_recognition_sort_key(info: Mapping[str, object]) -> tuple[int, int, int, int, int, int]:
|
2026-08-07 18:08:32 +08:00
|
|
|
|
status_order = {"ready": 0, "candidate": 0, "caution": 1, "blocked": 2}
|
|
|
|
|
|
risk_order = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
|
|
|
|
|
|
target_id = info.get("target_id", info.get("face_id", info.get("edge_id", -1)))
|
|
|
|
|
|
try:
|
|
|
|
|
|
numeric_target = int(target_id)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
numeric_target = -1
|
|
|
|
|
|
return (
|
|
|
|
|
|
feature_recognition_priority(info),
|
|
|
|
|
|
status_order.get(_text(info.get("status")), 9),
|
|
|
|
|
|
risk_order.get(_text(info.get("risk")), 9),
|
2026-08-14 18:42:39 +08:00
|
|
|
|
_confidence_sort_rank(info.get("confidence") or info.get("recognition_confidence")),
|
|
|
|
|
|
-external_relation_score_bonus(info),
|
2026-08-07 18:08:32 +08:00
|
|
|
|
numeric_target,
|
|
|
|
|
|
)
|