feat: 增强一级关系识别与保持关系策略

This commit is contained in:
2026-08-10 14:56:50 +08:00
parent 12250603dd
commit 83c44265ba
24 changed files with 3460 additions and 241 deletions
+153 -23
View File
@@ -302,9 +302,11 @@ class FeatureMixin:
depth_count = 0
suppress_count = 0
existing_fillet_count = 0
existing_chamfer_count = 0
depth_limit = max(2, min(per_type_limit, limit // 12))
suppress_limit = max(2, min(per_type_limit, limit // 12))
existing_fillet_limit = max(2, min(per_type_limit, limit // 12))
existing_chamfer_limit = max(2, min(per_type_limit, limit // 12))
slot_width_limit = max(2, min(per_type_limit, limit // 10))
slot_depth_limit = max(2, min(per_type_limit, limit // 10))
slot_arc_length_limit = max(2, min(per_type_limit, limit // 10))
@@ -323,15 +325,27 @@ class FeatureMixin:
if existing_fillet_status == "blocked":
continue
support_face_ids = tuple(feature.get("feature_existing_fillet_support_face_ids", ()))
chain_face_ids = tuple(feature.get("feature_existing_fillet_chain_face_ids", ()))
chain_status = str(feature.get("existing_fillet_chain_status") or "")
is_same_radius_chain = chain_status == "same-radius-chain-candidate" and len(chain_face_ids) > 1
support_note = (
f"支撑 Face: {support_face_ids}"
if support_face_ids
else "暂未识别出稳定支撑 Face。"
)
fillet_note = (
f"这是简单等半径圆角链候选,链上 Face: {chain_face_ids};点击后会选中并预填目标半径/弧长,"
"在特征参数表修改后点击“参数化建模”,会尝试一起 defeature 后重新倒圆。"
if is_same_radius_chain
else (
"这是已有圆角/倒圆候选;点击后会选中并预填目标半径/弧长,"
"在特征参数表修改后点击“参数化建模”,会尝试 defeature 后重新倒圆。"
)
)
candidates.append(
{
"operation_key": "inspect_existing_fillet",
"operation": "修改已有圆角半径",
"operation": "修改已有圆角半径/弧长" if is_same_radius_chain else "修改已有圆角半径/弧长",
"target_kind": "face",
"target_id": item["face_id"],
"face_id": item["face_id"],
@@ -342,13 +356,9 @@ class FeatureMixin:
"current_value": feature.get("existing_fillet_radius_estimate", item["radius"]),
"current_value_label": "radius",
"status": "caution",
"risk": "medium" if len(support_face_ids) >= 2 else "high",
"risk": "high" if is_same_radius_chain or len(support_face_ids) < 2 else "medium",
"confidence": item["confidence"],
"note": (
"这是已有圆角/倒圆候选;点击后会选中并预填目标半径,"
"再点击“修改已有圆角半径”会尝试 defeature 后重新倒圆。"
f" {support_note}"
),
"note": f"{fillet_note} {support_note}",
}
)
existing_fillet_count += 1
@@ -356,7 +366,7 @@ class FeatureMixin:
if diameter_count < per_type_limit and feature_guess not in {
"round/fillet candidate",
"boss/outer-round candidate",
}:
} and str(item.get("resize_status") or "") != "blocked":
candidates.append(
{
"operation_key": "resize_cylinder",
@@ -388,6 +398,8 @@ class FeatureMixin:
and float(item.get("angular_span", 0.0)) < math.tau * 0.92
):
feature = self.feature_info(int(item["face_id"]))
if str(feature.get("slot_status") or "") != "candidate":
continue
slot_width = feature.get("slot_chord_width_estimate")
if slot_width_count < slot_width_limit and isinstance(slot_width, (int, float)) and float(slot_width) > 0:
candidates.append(
@@ -666,17 +678,70 @@ class FeatureMixin:
plane_count = 0
shell_thickness_count = 0
shell_thickness_limit = max(2, min(per_type_limit, limit // 10))
shell_candidate_rows: list[dict[str, object]] = []
shell_scan_pool_limit = max(shell_thickness_limit * 4, 12)
face_scan_limit = len(self.faces) if max_scan_faces is None else min(len(self.faces), max(0, int(max_scan_faces)))
for face_id, face in enumerate(self.faces[:face_scan_limit]):
if progress_callback is not None and face_id % 30 == 0:
progress_callback()
if plane_count >= per_type_limit and shell_thickness_count >= shell_thickness_limit:
if (
plane_count >= per_type_limit
and len(shell_candidate_rows) >= shell_scan_pool_limit
and existing_chamfer_count >= existing_chamfer_limit
):
break
surf = BRepAdaptor_Surface(face)
if surf.GetType() != GeomAbs_Plane:
continue
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
feature: dict[str, object] | None = None
quick_info: dict[str, object] | None = None
if existing_chamfer_count < existing_chamfer_limit:
quick_info = self.quick_face_info(face_id)
width = _float_or_none(quick_info.get("local_face_width"))
height = _float_or_none(quick_info.get("local_face_height"))
aspect = max(width or 0.0, height or 0.0) / max(min(width or 0.0, height or 0.0), 1e-9)
if (
int(quick_info.get("boundary_edges", 0) or 0) == 4
and not bool(quick_info.get("has_inner_boundaries"))
and aspect >= 1.8
):
feature = self.feature_info(face_id)
if str(feature.get("existing_chamfer_status") or "") == "candidate":
support_face_ids = tuple(feature.get("feature_existing_chamfer_support_face_ids", ()))
long_edge_ids = tuple(feature.get("feature_existing_chamfer_long_edge_ids", ()))
candidates.append(
{
"operation_key": "inspect_existing_chamfer",
"operation": "修改已有倒角距离",
"target_kind": "face",
"target_id": face_id,
"face_id": face_id,
"part_id": self.face_part_ids[face_id],
"solid_id": self.face_solid_ids[face_id],
"surface": "plane",
"feature_guess": "chamfer candidate",
"current_value": feature.get("existing_chamfer_distance_estimate"),
"current_value_label": "chamfer_distance",
"status": "caution",
"risk": str(feature.get("existing_chamfer_risk") or "medium"),
"confidence": str(feature.get("recognition_confidence") or "medium"),
"note": (
"这是简单已有倒角斜面;点击后会选中该 Face,并把倒角距离填入参数表。"
f" 支撑 Face: {support_face_ids}; 长边 Edge: {long_edge_ids}"
),
}
)
existing_chamfer_count += 1
continue
if plane_count < per_type_limit or len(shell_candidate_rows) < shell_scan_pool_limit:
quick_info = quick_info or self.quick_face_info(face_id)
boundary_edges = int(quick_info.get("boundary_edges", 0) or 0)
if bool(quick_info.get("has_inner_boundaries")) or boundary_edges > 4:
feature = feature or self.feature_info(face_id)
if feature.get("multistep_prismatic_status") == "blocked":
continue
if plane_count < per_type_limit:
if detailed:
direction_info = self._plane_push_pull_direction(face_id, surf)
@@ -709,12 +774,21 @@ class FeatureMixin:
}
)
plane_count += 1
if shell_thickness_count < shell_thickness_limit:
feature = self.feature_info(face_id)
if len(shell_candidate_rows) < shell_scan_pool_limit:
feature = feature or self.feature_info(face_id)
if feature.get("shell_region_status") == "candidate":
shell_confidence = str(feature.get("shell_confidence", "low"))
shell_risk = "low" if shell_confidence == "high" else "medium" if shell_confidence == "medium" else "high"
candidates.append(
shell_note = (
"快速扫描:已找到投影重叠的相对平面;点击后会填入参考目标厚度,"
"执行时会移动当前平面区域来改变局部壳体厚度。"
)
if feature.get("open_shell_context_status") == "limited":
shell_note = (
f"{shell_note} 已识别为开口薄壁壳体上下文;"
"完整抽壳/开口面编辑暂未实现,当前只开放壳体厚度、平面推拉或整体缩放。"
)
shell_candidate_rows.append(
{
"operation_key": "resize_shell_thickness",
"operation": "调整壳体厚度",
@@ -730,13 +804,26 @@ class FeatureMixin:
"status": "ready" if shell_confidence == "high" else "caution",
"risk": shell_risk,
"confidence": shell_confidence,
"note": (
"快速扫描:已找到投影重叠的相对平面;点击后会填入参考目标厚度,"
"执行时会移动当前平面区域来改变局部壳体厚度。"
),
"note": shell_note,
"open_shell_context_status": feature.get("open_shell_context_status", ""),
}
)
shell_thickness_count += 1
def _shell_candidate_sort_key(item: dict[str, object]) -> tuple[int, int, int, float, int]:
confidence_rank = {"high": 0, "medium": 1, "low": 2}.get(str(item.get("confidence") or ""), 3)
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}.get(str(item.get("risk") or ""), 3)
thickness = _float_or_none(item.get("current_value"))
return (
0 if item.get("open_shell_context_status") == "limited" else 1,
confidence_rank,
risk_rank,
thickness if thickness is not None else float("inf"),
int(item.get("face_id", 10**9) or 10**9),
)
for shell_candidate in sorted(shell_candidate_rows, key=_shell_candidate_sort_key)[:shell_thickness_limit]:
candidates.append(shell_candidate)
shell_thickness_count += 1
fillet_edge_count = 0
chamfer_edge_count = 0
@@ -959,6 +1046,32 @@ class FeatureMixin:
candidate.update(self._cylinder_end_opening_info(face_id, surf))
candidate.update(_cylinder_resize_readiness(candidate))
candidate.update(_cylinder_boss_resize_readiness(candidate))
try:
feature = self.feature_info(face_id)
for key in (
"feature_guess",
"feature_type",
"feature_edit_actions",
"confidence",
"slot_kind",
"slot_status",
"slot_risk",
"slot_blockers",
"resize_status",
"resize_risk",
"resize_blockers",
"resize_note",
"boss_resize_status",
"boss_resize_risk",
"boss_resize_blockers",
"boss_resize_note",
"recognition_risk",
"recognition_blockers",
):
if feature.get(key) not in {None, ""}:
candidate[key] = feature.get(key)
except Exception:
pass
candidates.append(candidate)
if len(candidates) >= limit:
break
@@ -976,6 +1089,24 @@ class FeatureMixin:
}
current_diameter = float(info["diameter"])
feature = self.feature_info(face_id)
if str(feature.get("slot_status") or "") == "blocked":
blocker = str(feature.get("slot_blockers") or "当前槽/半孔属于复杂槽或多槽组,暂不开放稳定修改。")
return {
"status": "blocked",
"risk": "blocked",
"message": blocker,
"warnings": "",
"blockers": blocker,
"face_id": face_id,
"part_id": info.get("part_id"),
"solid_id": info.get("solid_id"),
"current_diameter": current_diameter,
"target_diameter": new_diameter,
"feature_type": feature.get("feature_type"),
"feature_guess": feature.get("feature_guess"),
"slot_status": feature.get("slot_status"),
"slot_blockers": blocker,
}
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
axis_range = self._cylindrical_axis_range(
face_id,
@@ -1271,8 +1402,7 @@ class FeatureMixin:
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
blockers.append("Selected slot does not have a stable partial-cylinder angular span.")
if slot_status and slot_status != "candidate":
risk = _max_risk(risk, "high")
warnings.append(f"Slot candidate status is {slot_status}.")
blockers.append(str(feature.get("slot_blockers") or info.get("slot_blockers") or f"Slot candidate status is {slot_status}."))
span = min(max(float(angular_span or 0.0), 1e-6), math.tau - 1e-6)
sin_half_span = math.sin(span / 2.0)
@@ -1464,8 +1594,7 @@ class FeatureMixin:
if target_angular_span >= math.tau * 0.92:
blockers.append("Target slot angular span must remain below a near-full cylinder.")
if slot_status and slot_status != "candidate":
risk = _max_risk(risk, "high")
warnings.append(f"Slot candidate status is {slot_status}.")
blockers.append(str(feature.get("slot_blockers") or info.get("slot_blockers") or f"Slot candidate status is {slot_status}."))
delta_span = None if current_span <= 0 else target_angular_span - current_span
delta_ratio = (
@@ -1676,8 +1805,7 @@ class FeatureMixin:
risk = _max_risk(risk, "medium")
warnings.append("Slot recognition confidence is low.")
if slot_status and slot_status != "candidate":
risk = _max_risk(risk, "high")
warnings.append(f"Slot candidate status is {slot_status}.")
blockers.append(str(feature.get("slot_blockers") or info.get("slot_blockers") or f"Slot candidate status is {slot_status}."))
cutter_plan: dict[str, object] = {}
fill_plan: dict[str, object] = {}
@@ -2053,6 +2181,8 @@ class FeatureMixin:
blockers.append("Current slot face is not recognized as one end of a paired obround slot.")
else:
blockers.append("Manual paired Face ID could not be used as a compatible obround slot end.")
if str(feature.get("slot_status") or "") not in {"", "candidate"}:
blockers.append(str(feature.get("slot_blockers") or f"Slot candidate status is {feature.get('slot_status')}."))
if target_total_length <= 0:
blockers.append("Target slot total length must be greater than 0.")