feat: 增强一级关系识别与保持关系策略
This commit is contained in:
+153
-23
@@ -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.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user