feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
@@ -64,6 +64,185 @@ from .geometry_utils import * # noqa: F403
|
||||
|
||||
|
||||
class FeatureMixin:
|
||||
def _cylindrical_feature_first_level_plan_fields(self, face_id: int) -> dict[str, object]:
|
||||
try:
|
||||
topology = self.cylindrical_feature_first_level_topology(face_id)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"topology_relation_depth": 1,
|
||||
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level",
|
||||
"topology_relation_status": "unavailable",
|
||||
"topology_relation_message": str(exc),
|
||||
"first_level_boundary_edge_ids": (),
|
||||
"first_level_boundary_edge_count": 0,
|
||||
"first_level_boundary_vertex_count": 0,
|
||||
"first_level_adjacent_face_ids": (),
|
||||
"first_level_adjacent_face_count": 0,
|
||||
"cylindrical_feature_side_face_ids": (face_id,),
|
||||
"cylindrical_feature_side_face_count": 1,
|
||||
}
|
||||
|
||||
fields = {
|
||||
"topology_relation_depth": topology.get("topology_relation_depth", 1),
|
||||
"topology_relation_model": topology.get("topology_relation_model"),
|
||||
"topology_relation_scope": topology.get("topology_relation_scope"),
|
||||
"topology_relation_boundary": topology.get("topology_relation_boundary"),
|
||||
"topology_relation_status": "ready",
|
||||
"topology_ignored_relation_depths": topology.get("topology_ignored_relation_depths", ()),
|
||||
"topology_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
|
||||
"first_level_boundary_edge_ids": topology.get("cylindrical_feature_boundary_edge_ids", ()),
|
||||
"first_level_boundary_edge_count": topology.get("cylindrical_feature_boundary_edge_count", 0),
|
||||
"first_level_boundary_vertex_count": topology.get("cylindrical_feature_boundary_vertex_count", 0),
|
||||
"first_level_adjacent_face_ids": topology.get("cylindrical_feature_adjacent_face_ids", ()),
|
||||
"first_level_adjacent_face_count": topology.get("cylindrical_feature_adjacent_face_count", 0),
|
||||
"first_level_adjacent_surface_types": topology.get("cylindrical_feature_adjacent_surface_types", ()),
|
||||
"first_level_shared_edges_by_face": topology.get("cylindrical_feature_shared_edges_by_face", ()),
|
||||
"first_level_face_ids": topology.get("cylindrical_feature_first_level_face_ids", ()),
|
||||
"first_level_face_count": topology.get("cylindrical_feature_first_level_face_count", 0),
|
||||
"first_level_topology_note": topology.get("first_level_topology_note", ""),
|
||||
"cylindrical_feature_side_face_ids": topology.get("cylindrical_feature_side_face_ids", (face_id,)),
|
||||
"cylindrical_feature_side_face_count": topology.get("cylindrical_feature_side_face_count", 1),
|
||||
"cylindrical_feature_boundary_edge_ids": topology.get("cylindrical_feature_boundary_edge_ids", ()),
|
||||
"cylindrical_feature_boundary_edge_count": topology.get("cylindrical_feature_boundary_edge_count", 0),
|
||||
"cylindrical_feature_adjacent_face_ids": topology.get("cylindrical_feature_adjacent_face_ids", ()),
|
||||
"cylindrical_feature_adjacent_face_count": topology.get("cylindrical_feature_adjacent_face_count", 0),
|
||||
"cylindrical_feature_end_face_ids": topology.get("cylindrical_feature_end_face_ids", ()),
|
||||
"cylindrical_feature_end_face_count": topology.get("cylindrical_feature_end_face_count", 0),
|
||||
"cylindrical_feature_bottom_face_ids": topology.get("cylindrical_feature_bottom_face_ids", ()),
|
||||
"cylindrical_feature_bottom_face_count": topology.get("cylindrical_feature_bottom_face_count", 0),
|
||||
"cylindrical_feature_opening_face_ids": topology.get("cylindrical_feature_opening_face_ids", ()),
|
||||
"cylindrical_feature_opening_face_count": topology.get("cylindrical_feature_opening_face_count", 0),
|
||||
"cylindrical_feature_slot_boundary_face_ids": topology.get(
|
||||
"cylindrical_feature_slot_boundary_face_ids",
|
||||
(),
|
||||
),
|
||||
"cylindrical_feature_slot_boundary_face_count": topology.get(
|
||||
"cylindrical_feature_slot_boundary_face_count",
|
||||
0,
|
||||
),
|
||||
}
|
||||
fields["first_level_edit_semantics"] = (
|
||||
"Cylindrical feature edits currently use only first-level shared-edge topology: "
|
||||
"the selected cylinder/slot wall is rebuilt together with its direct boundary neighbors; "
|
||||
"second-level and deeper propagation is not automatic yet."
|
||||
)
|
||||
return fields
|
||||
|
||||
def _cylindrical_first_level_guard_fields(
|
||||
self,
|
||||
topology_fields: dict[str, object],
|
||||
*,
|
||||
require_slot_boundary: bool = False,
|
||||
require_bottom: bool = False,
|
||||
) -> dict[str, object]:
|
||||
blockers: list[str] = []
|
||||
warnings: list[str] = []
|
||||
risk = "low"
|
||||
|
||||
status = str(topology_fields.get("topology_relation_status") or "")
|
||||
if status != "ready":
|
||||
blockers.append(
|
||||
"当前孔/槽的一级关系拓扑无法确认,已阻止局部重建;请换一个更明确的孔壁/槽壁 Face。"
|
||||
)
|
||||
|
||||
side_count = int(topology_fields.get("cylindrical_feature_side_face_count", 0) or 0)
|
||||
edge_count = int(topology_fields.get("cylindrical_feature_boundary_edge_count", 0) or 0)
|
||||
vertex_count = int(topology_fields.get("first_level_boundary_vertex_count", 0) or 0)
|
||||
adjacent_count = int(topology_fields.get("cylindrical_feature_adjacent_face_count", 0) or 0)
|
||||
slot_boundary_count = int(topology_fields.get("cylindrical_feature_slot_boundary_face_count", 0) or 0)
|
||||
bottom_count = int(topology_fields.get("cylindrical_feature_bottom_face_count", 0) or 0)
|
||||
|
||||
if side_count <= 0:
|
||||
blockers.append("没有识别到当前孔/槽的圆柱侧壁区域,不能稳定局部修改。")
|
||||
if edge_count <= 0:
|
||||
blockers.append("没有识别到当前孔/槽侧壁的边界 Edge,不能确定一级联动范围。")
|
||||
if adjacent_count <= 0:
|
||||
blockers.append("没有识别到与当前孔/槽直接共边的相邻 Face,不能保证修改后拓扑闭合。")
|
||||
if require_slot_boundary and slot_boundary_count <= 0:
|
||||
blockers.append("当前槽/半孔缺少直接槽边界 Face,不能稳定执行槽的局部重建。")
|
||||
if require_bottom and bottom_count <= 0:
|
||||
blockers.append("当前盲孔/盲槽缺少可靠底面 Face,不能稳定执行局部深度修改。")
|
||||
|
||||
if not blockers:
|
||||
if side_count > 4 or edge_count > 20 or adjacent_count > 14:
|
||||
risk = _max_risk(risk, "high")
|
||||
warnings.append(
|
||||
"当前孔/槽的一级关系邻域较复杂,局部布尔重建可能影响多个直接相邻面。"
|
||||
)
|
||||
elif side_count > 1 or edge_count > 10 or adjacent_count > 8:
|
||||
risk = _max_risk(risk, "medium")
|
||||
warnings.append("当前孔/槽由多个侧壁或较多相邻面组成,修改后请重点检查一级邻域。")
|
||||
|
||||
note = (
|
||||
f"一级关系拓扑检查:侧壁 Face {side_count} 个,边界 Edge {edge_count} 条,"
|
||||
f"边界 Vertex {vertex_count} 个,直接相邻 Face {adjacent_count} 个。"
|
||||
)
|
||||
return {
|
||||
"first_level_topology_status": "blocked" if blockers else "ready",
|
||||
"first_level_topology_risk": "blocked" if blockers else risk,
|
||||
"first_level_topology_blockers": ";".join(blockers),
|
||||
"first_level_topology_warnings": ";".join(warnings),
|
||||
"first_level_topology_guard_note": note,
|
||||
}
|
||||
|
||||
def _apply_cylindrical_first_level_guard(
|
||||
self,
|
||||
topology_fields: dict[str, object],
|
||||
blockers: list[str],
|
||||
warnings: list[str],
|
||||
risk: str,
|
||||
*,
|
||||
require_slot_boundary: bool = False,
|
||||
require_bottom: bool = False,
|
||||
) -> str:
|
||||
guard = self._cylindrical_first_level_guard_fields(
|
||||
topology_fields,
|
||||
require_slot_boundary=require_slot_boundary,
|
||||
require_bottom=require_bottom,
|
||||
)
|
||||
topology_fields.update(guard)
|
||||
blocker_text = str(guard.get("first_level_topology_blockers") or "").strip()
|
||||
warning_text = str(guard.get("first_level_topology_warnings") or "").strip()
|
||||
if blocker_text:
|
||||
blockers.append(blocker_text)
|
||||
if warning_text:
|
||||
warnings.append(warning_text)
|
||||
return _max_risk(risk, str(guard.get("first_level_topology_risk") or "low"))
|
||||
|
||||
def _apply_cylindrical_first_level_guard_to_readiness(
|
||||
self,
|
||||
readiness: dict[str, object],
|
||||
topology_fields: dict[str, object],
|
||||
*,
|
||||
status_key: str,
|
||||
risk_key: str,
|
||||
note_key: str,
|
||||
warnings_key: str,
|
||||
blockers_key: str,
|
||||
require_slot_boundary: bool = False,
|
||||
require_bottom: bool = False,
|
||||
) -> dict[str, object]:
|
||||
guard = self._cylindrical_first_level_guard_fields(
|
||||
topology_fields,
|
||||
require_slot_boundary=require_slot_boundary,
|
||||
require_bottom=require_bottom,
|
||||
)
|
||||
topology_fields.update(guard)
|
||||
result = dict(readiness)
|
||||
blocker_text = str(guard.get("first_level_topology_blockers") or "").strip()
|
||||
warning_text = str(guard.get("first_level_topology_warnings") or "").strip()
|
||||
if blocker_text:
|
||||
result[status_key] = "blocked"
|
||||
result[risk_key] = "blocked"
|
||||
result[blockers_key] = _join_nonempty(result.get(blockers_key), blocker_text)
|
||||
result[note_key] = _join_nonempty(result.get(note_key), blocker_text)
|
||||
else:
|
||||
result[risk_key] = _max_risk(str(result.get(risk_key) or "low"), str(guard["first_level_topology_risk"]))
|
||||
if warning_text:
|
||||
result[warnings_key] = _join_nonempty(result.get(warnings_key), warning_text)
|
||||
result[note_key] = _join_nonempty(result.get(note_key), warning_text)
|
||||
return result
|
||||
|
||||
def editable_feature_candidates(
|
||||
self,
|
||||
limit: int = 160,
|
||||
@@ -727,6 +906,7 @@ class FeatureMixin:
|
||||
}
|
||||
current_diameter = float(info["diameter"])
|
||||
feature = self.feature_info(face_id)
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
axis_range = self._cylindrical_axis_range(
|
||||
face_id,
|
||||
BRepAdaptor_Surface(self.faces[face_id]),
|
||||
@@ -737,6 +917,15 @@ class FeatureMixin:
|
||||
scoped_info["v_range"] = (axis_range["v_min"], axis_range["v_max"])
|
||||
scoped_info.update(self._cylinder_end_opening_info(face_id, BRepAdaptor_Surface(self.faces[face_id]), axis_range))
|
||||
readiness = _cylinder_resize_readiness(scoped_info, new_diameter)
|
||||
readiness = self._apply_cylindrical_first_level_guard_to_readiness(
|
||||
readiness,
|
||||
topology_fields,
|
||||
status_key="resize_status",
|
||||
risk_key="resize_risk",
|
||||
note_key="resize_note",
|
||||
warnings_key="resize_warnings",
|
||||
blockers_key="resize_blockers",
|
||||
)
|
||||
resize_mode = _resize_mode(current_diameter, new_diameter)
|
||||
delta_diameter = new_diameter - current_diameter
|
||||
diameter_delta_ratio = abs(delta_diameter) / max(current_diameter, 1e-9)
|
||||
@@ -781,6 +970,7 @@ class FeatureMixin:
|
||||
"resize_strategy": "same-axis-bounded-cylinder-recut",
|
||||
"edit_strategy_label": "同轴圆柱重切",
|
||||
"edit_semantics": edit_semantics,
|
||||
**topology_fields,
|
||||
**cutter_plan,
|
||||
**fill_plan,
|
||||
}
|
||||
@@ -799,12 +989,14 @@ class FeatureMixin:
|
||||
feature = self.feature_info(face_id)
|
||||
except Exception:
|
||||
feature = {}
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
|
||||
blockers: list[str] = []
|
||||
warnings: list[str] = [
|
||||
"Cylinder axis move fills the current cylindrical hole, then cuts a same-diameter hole on the target axis."
|
||||
]
|
||||
risk = "medium"
|
||||
risk = self._apply_cylindrical_first_level_guard(topology_fields, blockers, warnings, risk)
|
||||
|
||||
try:
|
||||
target_center = (float(target_center[0]), float(target_center[1]), float(target_center[2]))
|
||||
@@ -897,6 +1089,7 @@ class FeatureMixin:
|
||||
return {
|
||||
**cutter_plan,
|
||||
**fill_plan,
|
||||
**topology_fields,
|
||||
"status": status,
|
||||
"risk": risk,
|
||||
"message": message,
|
||||
@@ -965,6 +1158,7 @@ class FeatureMixin:
|
||||
feature = self.feature_info(face_id)
|
||||
except Exception:
|
||||
feature = {}
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
|
||||
current_diameter = _float_or_none(info.get("diameter"))
|
||||
if current_diameter is None:
|
||||
@@ -982,6 +1176,13 @@ class FeatureMixin:
|
||||
"Slot parameter edit keeps the current partial-cylinder angular span, converts the target value to a cylinder diameter, and rebuilds only the local sector volume."
|
||||
]
|
||||
risk = "medium"
|
||||
risk = self._apply_cylindrical_first_level_guard(
|
||||
topology_fields,
|
||||
blockers,
|
||||
warnings,
|
||||
risk,
|
||||
require_slot_boundary=True,
|
||||
)
|
||||
|
||||
try:
|
||||
target_value = float(target_value)
|
||||
@@ -1067,6 +1268,7 @@ class FeatureMixin:
|
||||
"feature_slot_face_ids": feature.get("feature_slot_face_ids"),
|
||||
"feature_slot_boundary_face_ids": feature.get("feature_slot_boundary_face_ids"),
|
||||
"slot_note": feature.get("slot_note"),
|
||||
**topology_fields,
|
||||
}
|
||||
if blockers:
|
||||
return {
|
||||
@@ -1141,6 +1343,7 @@ class FeatureMixin:
|
||||
feature = self.feature_info(face_id)
|
||||
except Exception:
|
||||
feature = {}
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
|
||||
current_diameter = _float_or_none(info.get("diameter"))
|
||||
if current_diameter is None:
|
||||
@@ -1153,6 +1356,13 @@ class FeatureMixin:
|
||||
"Slot angular-span edit keeps the current cylinder radius, fills the old local sector, then cuts a new local sector around the same angular center."
|
||||
]
|
||||
risk = "medium"
|
||||
risk = self._apply_cylindrical_first_level_guard(
|
||||
topology_fields,
|
||||
blockers,
|
||||
warnings,
|
||||
risk,
|
||||
require_slot_boundary=True,
|
||||
)
|
||||
|
||||
try:
|
||||
target_angular_span = float(target_angular_span)
|
||||
@@ -1241,6 +1451,7 @@ class FeatureMixin:
|
||||
return {
|
||||
**cutter_plan,
|
||||
**fill_plan,
|
||||
**topology_fields,
|
||||
"status": status,
|
||||
"risk": "blocked" if blockers else risk,
|
||||
"message": message,
|
||||
@@ -1303,12 +1514,20 @@ class FeatureMixin:
|
||||
feature = self.feature_info(face_id)
|
||||
except Exception:
|
||||
feature = {}
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
|
||||
blockers: list[str] = []
|
||||
warnings: list[str] = [
|
||||
"Slot/half-hole axis move fills the old local sector, then cuts the same sector tool on the target axis."
|
||||
]
|
||||
risk = "medium"
|
||||
risk = self._apply_cylindrical_first_level_guard(
|
||||
topology_fields,
|
||||
blockers,
|
||||
warnings,
|
||||
risk,
|
||||
require_slot_boundary=True,
|
||||
)
|
||||
|
||||
try:
|
||||
target_center = (float(target_center[0]), float(target_center[1]), float(target_center[2]))
|
||||
@@ -1473,6 +1692,7 @@ class FeatureMixin:
|
||||
**cutter_plan,
|
||||
**fill_plan,
|
||||
**pair_plan,
|
||||
**topology_fields,
|
||||
"status": status,
|
||||
"risk": "blocked" if blockers else risk,
|
||||
"message": message,
|
||||
@@ -1723,6 +1943,7 @@ class FeatureMixin:
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
info = self.face_info(face_id)
|
||||
feature = self.feature_info(face_id)
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
current_diameter = _float_or_none(info.get("diameter"))
|
||||
if current_diameter is None or current_diameter <= 1e-9:
|
||||
return {
|
||||
@@ -1749,6 +1970,13 @@ class FeatureMixin:
|
||||
blockers: list[str] = []
|
||||
warnings: list[str] = []
|
||||
risk = "medium"
|
||||
risk = self._apply_cylindrical_first_level_guard(
|
||||
topology_fields,
|
||||
blockers,
|
||||
warnings,
|
||||
risk,
|
||||
require_slot_boundary=True,
|
||||
)
|
||||
|
||||
if not paired_plan:
|
||||
if pair_face_id is None:
|
||||
@@ -1815,6 +2043,7 @@ class FeatureMixin:
|
||||
**cutter_plan,
|
||||
**fill_plan,
|
||||
**paired_plan,
|
||||
**topology_fields,
|
||||
"status": status,
|
||||
"risk": risk,
|
||||
"message": message,
|
||||
@@ -1918,6 +2147,7 @@ class FeatureMixin:
|
||||
|
||||
current_diameter = float(info["diameter"])
|
||||
feature = self.feature_info(face_id)
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
axis_range = self._cylindrical_axis_range(
|
||||
face_id,
|
||||
BRepAdaptor_Surface(self.faces[face_id]),
|
||||
@@ -2130,6 +2360,7 @@ class FeatureMixin:
|
||||
"message": "当前选中的 Face 不是圆柱面,不能封堵圆柱孔。",
|
||||
}
|
||||
feature = self.feature_info(face_id)
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
axis_range = self._cylindrical_axis_range(
|
||||
face_id,
|
||||
BRepAdaptor_Surface(self.faces[face_id]),
|
||||
@@ -2140,6 +2371,15 @@ class FeatureMixin:
|
||||
scoped_info["v_range"] = (axis_range["v_min"], axis_range["v_max"])
|
||||
scoped_info.update(self._cylinder_end_opening_info(face_id, BRepAdaptor_Surface(self.faces[face_id]), axis_range))
|
||||
readiness = _cylinder_suppress_readiness(scoped_info)
|
||||
readiness = self._apply_cylindrical_first_level_guard_to_readiness(
|
||||
readiness,
|
||||
topology_fields,
|
||||
status_key="suppress_status",
|
||||
risk_key="suppress_risk",
|
||||
note_key="suppress_note",
|
||||
warnings_key="suppress_warnings",
|
||||
blockers_key="suppress_blockers",
|
||||
)
|
||||
fill_plan = self._bounded_cylinder_fill_plan(face_id)
|
||||
fill_plan["fill_strategy"] = "bounded-hole-suppress-fill"
|
||||
fill_plan["fill_note"] = "按当前圆柱孔范围生成略带重叠的补料圆柱体,用于封堵完整通孔或盲孔。"
|
||||
@@ -2169,6 +2409,7 @@ class FeatureMixin:
|
||||
"feature_opening_face_ids": feature.get("feature_opening_face_ids"),
|
||||
"feature_bottom_note": feature.get("feature_bottom_note"),
|
||||
"resize_strategy": "fill-cylindrical-hole-volume",
|
||||
**topology_fields,
|
||||
"edit_strategy_label": "圆柱补料封堵",
|
||||
"edit_semantics": "按当前孔轴线和估算高度生成补料圆柱体,局部 Fuse 后封堵当前完整圆柱孔。",
|
||||
**fill_plan,
|
||||
@@ -2191,6 +2432,7 @@ class FeatureMixin:
|
||||
}
|
||||
|
||||
feature = self.feature_info(face_id)
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
context = self._blind_cylindrical_depth_context(
|
||||
face_id,
|
||||
info,
|
||||
@@ -2212,6 +2454,15 @@ class FeatureMixin:
|
||||
context.get("context_message"),
|
||||
)
|
||||
readiness["depth_note"] = readiness["depth_blockers"]
|
||||
readiness = self._apply_cylindrical_first_level_guard_to_readiness(
|
||||
readiness,
|
||||
topology_fields,
|
||||
status_key="depth_status",
|
||||
risk_key="depth_risk",
|
||||
note_key="depth_note",
|
||||
warnings_key="depth_warnings",
|
||||
blockers_key="depth_blockers",
|
||||
)
|
||||
|
||||
current_depth = float(depth_info.get("hole_depth_estimate", 0.0))
|
||||
delta_depth = target_depth - current_depth
|
||||
@@ -2249,6 +2500,7 @@ class FeatureMixin:
|
||||
"feature_bottom_detection": feature.get("feature_bottom_detection"),
|
||||
"feature_bottom_note": feature.get("feature_bottom_note"),
|
||||
"resize_strategy": "bounded-blind-depth-cut-or-fill",
|
||||
**topology_fields,
|
||||
"edit_strategy_label": "盲孔/盲槽深度切削或补料",
|
||||
"edit_semantics": "沿识别到的开口到底面方向调整深度:加深时切削,变浅时从新底面到旧底面补料。",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user