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, "inspect_existing_chamfer": 51, "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, "只读/诊断", "暂未稳定归类为可修改特征。"), ) 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 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 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) 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) if _text(info.get("existing_chamfer_status")) == "candidate" or feature_guess == "chamfer candidate": return 50 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 ) or _text(info.get("slot_kind")) == "partial-cylindrical-groove" or "槽/半孔候选" in feature_type: 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 def feature_recognition_sort_key(info: Mapping[str, object]) -> tuple[int, int, int, int, int, int]: 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), _confidence_sort_rank(info.get("confidence") or info.get("recognition_confidence")), -external_relation_score_bonus(info), numeric_target, )