feat: 完善参数化编辑和孔修改稳定性
This commit is contained in:
+418
-199
@@ -105,7 +105,7 @@ class WindowActionMixin:
|
||||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择 Face", "请先切换到 Face 或 Feature 模式并选择一个 Face。")
|
||||
QMessageBox.information(self, "未选择 Face", "请先切换到 Face 或特征模式并选择一个 Face。")
|
||||
return
|
||||
if not self._confirm_export_quality("face", self.selected_face_id):
|
||||
return
|
||||
@@ -128,7 +128,7 @@ class WindowActionMixin:
|
||||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择特征", "请先切换到 Feature 模式并选择一个局部特征。")
|
||||
QMessageBox.information(self, "未选择特征", "请先切换到特征模式并选择一个特征。")
|
||||
return
|
||||
if not self._confirm_export_quality("feature", self.selected_face_id):
|
||||
return
|
||||
@@ -281,7 +281,7 @@ class WindowActionMixin:
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="修复选中零件",
|
||||
target=f"Part {part_id}",
|
||||
target=f"零件 {part_id}",
|
||||
parameters={
|
||||
"scope": "part",
|
||||
"part_id": part_id,
|
||||
@@ -292,7 +292,7 @@ class WindowActionMixin:
|
||||
)
|
||||
return
|
||||
|
||||
QMessageBox.information(self, "未选择对象", "请先选择 Part、Solid、Face 或 Edge。")
|
||||
QMessageBox.information(self, "未选择对象", "请先选择零件、Solid、Face 或 Edge。")
|
||||
self.statusBar().showMessage("未选择可修复对象")
|
||||
|
||||
def push_pull_face(self) -> None:
|
||||
@@ -436,6 +436,203 @@ class WindowActionMixin:
|
||||
target_id=face_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _max_quick_risk(current: str, candidate: str) -> str:
|
||||
order = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
|
||||
return current if order.get(current, 0) >= order.get(candidate, 0) else candidate
|
||||
|
||||
def _selected_face_info_snapshot(self) -> dict[str, object]:
|
||||
info = dict(getattr(self, "current_info_values", {}) or {})
|
||||
if self.model is None or self.selected_face_id is None:
|
||||
return info
|
||||
|
||||
selected_face_id = int(self.selected_face_id)
|
||||
possible_ids = (
|
||||
info.get("face_id"),
|
||||
info.get("topological_face_id"),
|
||||
info.get("feature_source_face_id"),
|
||||
info.get("face_region_source_face_id"),
|
||||
)
|
||||
has_selected_id = False
|
||||
for value in possible_ids:
|
||||
try:
|
||||
if value is not None and int(value) == selected_face_id:
|
||||
has_selected_id = True
|
||||
break
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
has_any_face_id = any(value is not None for value in possible_ids)
|
||||
if info and has_any_face_id and not has_selected_id:
|
||||
info = {}
|
||||
if info and str(info.get("kind", "")) in {"edge", "part", "solid"} and not has_selected_id:
|
||||
info = {}
|
||||
if "surface" not in info and "diameter" not in info:
|
||||
try:
|
||||
info = dict(self.model.face_info(selected_face_id))
|
||||
except Exception:
|
||||
pass
|
||||
return info
|
||||
|
||||
def _quick_cylinder_resize_plan(
|
||||
self,
|
||||
face_id: int,
|
||||
target_diameter: float,
|
||||
info: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
info = dict(info or self._selected_face_info_snapshot())
|
||||
warnings: list[str] = []
|
||||
blockers: list[str] = []
|
||||
risk = "low"
|
||||
status = "ready"
|
||||
|
||||
surface = str(info.get("surface", ""))
|
||||
current_diameter = _float_or_none(info.get("diameter"))
|
||||
height_estimate = _float_or_none(info.get("height_estimate"))
|
||||
if height_estimate is None:
|
||||
height_estimate = _float_or_none(info.get("same_domain_height_estimate"))
|
||||
if height_estimate is None:
|
||||
height_estimate = _float_or_none(info.get("hole_depth_estimate"))
|
||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||
confidence = str(info.get("confidence", "low"))
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
|
||||
if surface != "cylinder" or current_diameter is None or current_diameter <= 1e-9:
|
||||
blockers.append("当前选中对象不是可识别的圆柱面,不能调整孔/槽直径。")
|
||||
if target_diameter <= 0:
|
||||
blockers.append("目标直径必须大于 0。")
|
||||
|
||||
if guess == "round/fillet candidate":
|
||||
risk = self._max_quick_risk(risk, "high")
|
||||
warnings.append("当前圆柱面更像圆角/倒圆,调整孔径可能误切圆角。")
|
||||
elif guess == "boss/outer-round candidate":
|
||||
risk = self._max_quick_risk(risk, "high")
|
||||
warnings.append("当前圆柱面更像凸台或外圆,调整孔径可能切掉外部结构。")
|
||||
elif guess != "hole/groove candidate":
|
||||
risk = self._max_quick_risk(risk, "high")
|
||||
warnings.append("当前圆柱面还没有被稳定识别为孔/槽候选。")
|
||||
|
||||
if guess == "hole/groove candidate" and confidence == "low":
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("孔/槽判断置信度较低。")
|
||||
if (
|
||||
guess == "hole/groove candidate"
|
||||
and angular_span is not None
|
||||
and angular_span < math.tau * 0.92
|
||||
):
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("这是局部圆柱面,更像槽或半孔,不是完整圆孔。")
|
||||
|
||||
delta_diameter = None
|
||||
delta_ratio = None
|
||||
target_to_height_ratio = None
|
||||
resize_mode = "unknown"
|
||||
if current_diameter is not None and current_diameter > 1e-9:
|
||||
delta_diameter = target_diameter - current_diameter
|
||||
delta_ratio = abs(delta_diameter) / current_diameter
|
||||
resize_mode = "expand" if delta_diameter > 0 else "shrink"
|
||||
if abs(delta_diameter) <= max(current_diameter * 1e-5, 1e-6):
|
||||
blockers.append("目标直径与当前直径几乎相同,不需要修改。")
|
||||
elif target_diameter > current_diameter * 2.0:
|
||||
blockers.append(
|
||||
"目标直径超过当前直径的 2 倍;当前版本会阻止这种极端放大,避免布尔计算长时间卡死。"
|
||||
)
|
||||
elif delta_ratio > 1.0:
|
||||
risk = self._max_quick_risk(risk, "high")
|
||||
warnings.append("目标直径变化超过当前直径的 100%,很可能导致大范围误切或布尔失败。")
|
||||
elif delta_ratio > 0.35:
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("目标直径变化超过当前直径的 35%。")
|
||||
|
||||
if target_diameter < current_diameter:
|
||||
if guess != "hole/groove candidate":
|
||||
blockers.append("缩小孔径当前版本只支持明确的孔/槽候选。")
|
||||
else:
|
||||
risk = self._max_quick_risk(risk, "high")
|
||||
warnings.append("缩小孔径需要先补料再重切,属于高风险实验路径。")
|
||||
|
||||
if height_estimate is not None and height_estimate > 1e-9:
|
||||
target_to_height_ratio = target_diameter / height_estimate
|
||||
if target_diameter > height_estimate * 2.0:
|
||||
blockers.append("目标直径超过圆柱面估算高度的 2 倍,当前版本暂不放行。")
|
||||
elif target_diameter > height_estimate:
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("目标直径大于圆柱面估算高度,请确认单位和目标。")
|
||||
|
||||
if blockers:
|
||||
status = "blocked"
|
||||
risk = "blocked"
|
||||
elif risk in {"medium", "high"}:
|
||||
status = "caution"
|
||||
|
||||
message = ";".join(blockers + warnings) if blockers or warnings else "可以尝试调整孔/槽直径。"
|
||||
return {
|
||||
"status": status,
|
||||
"risk": risk,
|
||||
"message": message,
|
||||
"warnings": ";".join(warnings),
|
||||
"blockers": ";".join(blockers),
|
||||
"face_id": face_id,
|
||||
"part_id": info.get("part_id"),
|
||||
"solid_id": info.get("solid_id"),
|
||||
"current_diameter": current_diameter,
|
||||
"target_diameter": target_diameter,
|
||||
"delta_diameter": delta_diameter,
|
||||
"diameter_delta_ratio": delta_ratio,
|
||||
"target_to_height_ratio": target_to_height_ratio,
|
||||
"resize_mode": resize_mode,
|
||||
"feature_type": info.get("feature_type"),
|
||||
"feature_guess": guess,
|
||||
"confidence": confidence,
|
||||
"angular_span": angular_span,
|
||||
"height_estimate": height_estimate,
|
||||
"material_vote_summary": info.get("material_vote_summary"),
|
||||
"cylinder_end_type": info.get("cylinder_end_type"),
|
||||
"hole_depth_estimate": info.get("hole_depth_estimate"),
|
||||
"feature_bottom_face_ids": info.get("feature_bottom_face_ids"),
|
||||
"feature_opening_face_ids": info.get("feature_opening_face_ids"),
|
||||
"quick_preflight": True,
|
||||
}
|
||||
|
||||
def _confirm_quick_edit_plan(
|
||||
self,
|
||||
title: str,
|
||||
plan: dict[str, object],
|
||||
lines: list[str],
|
||||
blocked_status: str,
|
||||
cancelled_status: str,
|
||||
) -> bool:
|
||||
if plan.get("status") == "blocked":
|
||||
QMessageBox.information(self, f"不能{title}", str(plan.get("message", "")))
|
||||
self.statusBar().showMessage(blocked_status)
|
||||
return False
|
||||
if self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan):
|
||||
return False
|
||||
if plan.get("risk") == "low":
|
||||
return True
|
||||
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"\n警告: {warnings}\n" if warnings else ""
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
f"确认{title}",
|
||||
(
|
||||
"\n".join(lines)
|
||||
+ f"\n风险: {plan.get('risk')}\n"
|
||||
+ warnings_line
|
||||
+ f"\n{plan.get('message', '')}\n\n"
|
||||
"为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;"
|
||||
"详细几何方案会进入后台计算,完成后自动刷新模型。\n\n"
|
||||
"确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage(cancelled_status)
|
||||
return False
|
||||
return True
|
||||
|
||||
def resize_hole(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -449,64 +646,29 @@ class WindowActionMixin:
|
||||
except ValueError:
|
||||
QMessageBox.critical(self, "直径无效", "请输入数字形式的目标直径。")
|
||||
return
|
||||
info = self.model.face_info(self.selected_face_id)
|
||||
if "diameter" not in info:
|
||||
QMessageBox.information(self, "不是圆柱面", "当前选中的 Face 不是圆柱面,不能调整圆柱孔径。")
|
||||
return
|
||||
plan = self.model.cylindrical_resize_plan(self.selected_face_id, diameter)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整孔径", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆柱孔径调整已阻止")
|
||||
return
|
||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||
if plan["risk"] != "low":
|
||||
target_to_height_ratio = plan.get("target_to_height_ratio", "")
|
||||
target_to_height_line = (
|
||||
""
|
||||
if target_to_height_ratio is None or target_to_height_ratio == ""
|
||||
else f"目标直径/估算高度: {_format_value(target_to_height_ratio)}\n"
|
||||
)
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认圆柱孔径调整",
|
||||
(
|
||||
f"face: {plan['face_id']}\n"
|
||||
f"当前直径: {_format_value(plan['current_diameter'])}\n"
|
||||
f"目标直径: {_format_value(plan['target_diameter'])}\n"
|
||||
f"直径变化量: {_format_value(plan['delta_diameter'])}\n"
|
||||
f"直径变化比例: {_format_percent(plan['diameter_delta_ratio'])}\n"
|
||||
f"{target_to_height_line}"
|
||||
f"调整模式: {plan['resize_mode']}\n"
|
||||
f"候选判断: {plan['feature_guess']}\n"
|
||||
f"置信度: {plan['confidence']}\n"
|
||||
f"材料投票: {plan['material_vote_summary']}\n"
|
||||
f"端部类型: {plan['cylinder_end_type']}\n"
|
||||
f"深度估算: {_format_value(plan['hole_depth_estimate'])}\n"
|
||||
f"疑似底面 Face: {_format_value(plan.get('feature_bottom_face_ids', ''))}\n"
|
||||
f"开口端相邻 Face: {_format_value(plan.get('feature_opening_face_ids', ''))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"Cutter: {plan['cutter_strategy']}\n"
|
||||
f"Cutter 高度: {_format_value(plan['cutter_height'])}\n"
|
||||
f"Cutter 起点/终点余量: {_format_value(plan['cutter_start_margin'])} / "
|
||||
f"{_format_value(plan['cutter_end_margin'])}\n\n"
|
||||
f"底面保护: {_format_value(plan.get('cutter_bottom_protection', ''))}\n"
|
||||
f"{_format_value(plan.get('cutter_bottom_note', ''))}\n\n"
|
||||
f"补料策略: {plan.get('fill_strategy', '')}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"继续操作会对当前零件执行受限B-Rep布尔修改。\n\n"
|
||||
"确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage("已取消圆柱孔径调整")
|
||||
return
|
||||
face_id = self.selected_face_id
|
||||
self._show_cylinder_resize_preview(face_id, diameter)
|
||||
info = self._selected_face_info_snapshot()
|
||||
plan = self._quick_cylinder_resize_plan(face_id, diameter, info)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前直径: {_format_value(plan.get('current_diameter'))}",
|
||||
f"目标直径: {_format_value(plan.get('target_diameter'))}",
|
||||
f"直径变化量: {_format_value(plan.get('delta_diameter'))}",
|
||||
f"直径变化比例: {_format_percent(plan.get('diameter_delta_ratio'))}",
|
||||
f"候选判断: {plan.get('feature_guess')}",
|
||||
f"置信度: {plan.get('confidence')}",
|
||||
]
|
||||
if plan.get("target_to_height_ratio") not in {None, ""}:
|
||||
lines.append(f"目标直径/估算高度: {_format_value(plan.get('target_to_height_ratio'))}")
|
||||
if not self._confirm_quick_edit_plan(
|
||||
"圆柱孔径调整",
|
||||
plan,
|
||||
lines,
|
||||
"圆柱孔径调整已阻止",
|
||||
"已取消圆柱孔径调整",
|
||||
):
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.resize_cylindrical_hole(face_id, diameter)
|
||||
@@ -519,7 +681,7 @@ class WindowActionMixin:
|
||||
"part_id": plan.get("part_id"),
|
||||
"solid_id": plan.get("solid_id"),
|
||||
"new_diameter": diameter,
|
||||
"old_diameter": info.get("diameter"),
|
||||
"old_diameter": plan.get("current_diameter"),
|
||||
"delta_diameter": plan.get("delta_diameter"),
|
||||
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
|
||||
"target_to_height_ratio": plan.get("target_to_height_ratio"),
|
||||
@@ -527,8 +689,7 @@ class WindowActionMixin:
|
||||
"feature_type": plan.get("feature_type"),
|
||||
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
|
||||
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
|
||||
"feature_bottom_note": plan.get("feature_bottom_note"),
|
||||
"feature_guess": guess,
|
||||
"feature_guess": plan.get("feature_guess"),
|
||||
"confidence": plan.get("confidence"),
|
||||
"resize_status": plan.get("status"),
|
||||
"resize_risk": plan.get("risk"),
|
||||
@@ -538,23 +699,8 @@ class WindowActionMixin:
|
||||
"material_vote_summary": plan.get("material_vote_summary"),
|
||||
"cylinder_end_type": plan.get("cylinder_end_type"),
|
||||
"hole_depth_estimate": plan.get("hole_depth_estimate"),
|
||||
"start_end_state": plan.get("start_end_state"),
|
||||
"end_end_state": plan.get("end_end_state"),
|
||||
"cutter_strategy": plan.get("cutter_strategy"),
|
||||
"cutter_height": plan.get("cutter_height"),
|
||||
"cutter_margin": plan.get("cutter_margin"),
|
||||
"cutter_start_margin": plan.get("cutter_start_margin"),
|
||||
"cutter_end_margin": plan.get("cutter_end_margin"),
|
||||
"cutter_bottom_protection": plan.get("cutter_bottom_protection"),
|
||||
"cutter_protected_bottom_face_ids": plan.get("cutter_protected_bottom_face_ids"),
|
||||
"cutter_opening_face_ids": plan.get("cutter_opening_face_ids"),
|
||||
"cutter_bottom_note": plan.get("cutter_bottom_note"),
|
||||
"cutter_note": plan.get("cutter_note"),
|
||||
"fill_strategy": plan.get("fill_strategy"),
|
||||
"fill_height": plan.get("fill_height"),
|
||||
"fill_radius": plan.get("fill_radius"),
|
||||
"fill_radius_overlap": plan.get("fill_radius_overlap"),
|
||||
"fill_note": plan.get("fill_note"),
|
||||
"quick_preflight": True,
|
||||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
@@ -580,7 +726,7 @@ class WindowActionMixin:
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
info = self.model.feature_info(face_id)
|
||||
info = self._selected_face_info_snapshot()
|
||||
if info.get("surface") != "cylinder" or info.get("slot_kind") != "partial-cylindrical-groove":
|
||||
QMessageBox.information(self, "不是槽/半孔候选", "当前选中Face没有被识别为槽/半孔候选。")
|
||||
return
|
||||
@@ -597,37 +743,24 @@ class WindowActionMixin:
|
||||
|
||||
target_diameter = target_width / sin_half_span
|
||||
current_width = _float_or_none(info.get("slot_chord_width_estimate"))
|
||||
plan = self.model.cylindrical_resize_plan(face_id, target_diameter)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整槽/半孔宽度", str(plan["message"]))
|
||||
self.statusBar().showMessage("槽/半孔宽度调整已阻止")
|
||||
plan = self._quick_cylinder_resize_plan(face_id, target_diameter, info)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前槽宽估算: {_format_value(current_width)}",
|
||||
f"目标槽宽: {_format_value(target_width)}",
|
||||
f"槽圆弧角度: {_format_value(angular_span)}",
|
||||
f"换算目标圆柱直径: {_format_value(target_diameter)}",
|
||||
"当前版本会把槽宽换算成圆柱直径后执行孔/槽重建,不是完整 CAD 槽参数编辑。",
|
||||
]
|
||||
if not self._confirm_quick_edit_plan(
|
||||
"槽/半孔宽度调整",
|
||||
plan,
|
||||
lines,
|
||||
"槽/半孔宽度调整已阻止",
|
||||
"已取消槽/半孔宽度调整",
|
||||
):
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认调整槽/半孔宽度",
|
||||
(
|
||||
f"face: {face_id}\n"
|
||||
f"当前槽宽估算: {_format_value(current_width)}\n"
|
||||
f"目标槽宽: {_format_value(target_width)}\n"
|
||||
f"槽圆弧角度: {_format_value(angular_span)}\n"
|
||||
f"换算目标圆柱直径: {_format_value(target_diameter)}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"当前版本会把槽宽换算成圆柱直径后执行孔/槽重建,不是完整 CAD 槽参数编辑。确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage("已取消槽/半孔宽度调整")
|
||||
return
|
||||
|
||||
self._show_cylinder_resize_preview(face_id, target_diameter)
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.resize_cylindrical_hole(face_id, target_diameter)
|
||||
@@ -644,7 +777,7 @@ class WindowActionMixin:
|
||||
"slot_width_delta": None if current_width is None else target_width - current_width,
|
||||
"slot_angular_span": angular_span,
|
||||
"derived_new_diameter": target_diameter,
|
||||
"old_diameter": info.get("diameter"),
|
||||
"old_diameter": plan.get("current_diameter"),
|
||||
"delta_diameter": plan.get("delta_diameter"),
|
||||
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
|
||||
"resize_mode": plan.get("resize_mode"),
|
||||
@@ -664,9 +797,8 @@ class WindowActionMixin:
|
||||
"hole_depth_estimate": plan.get("hole_depth_estimate"),
|
||||
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
|
||||
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
|
||||
"cutter_strategy": plan.get("cutter_strategy"),
|
||||
"cutter_height": plan.get("cutter_height"),
|
||||
"fill_strategy": plan.get("fill_strategy"),
|
||||
"quick_preflight": True,
|
||||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
@@ -723,7 +855,7 @@ class WindowActionMixin:
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"继续操作会对当前零件执行受限B-Rep布尔修改。\n\n"
|
||||
"继续操作会对当前零件的局部几何执行受限B-Rep布尔修改。\n\n"
|
||||
"确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
@@ -877,6 +1009,111 @@ class WindowActionMixin:
|
||||
raise ValueError(f"底面 Face ID {bottom_face_id} 不存在。")
|
||||
return bottom_face_id
|
||||
|
||||
def _quick_cylinder_depth_plan(
|
||||
self,
|
||||
face_id: int,
|
||||
target_depth: float,
|
||||
bottom_face_id: int | None = None,
|
||||
info: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
info = dict(info or self._selected_face_info_snapshot())
|
||||
warnings: list[str] = []
|
||||
blockers: list[str] = []
|
||||
risk = "low"
|
||||
status = "ready"
|
||||
|
||||
surface = str(info.get("surface", ""))
|
||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||
confidence = str(info.get("confidence", "low"))
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
end_type = str(info.get("cylinder_end_type", "unknown"))
|
||||
current_depth = _float_or_none(info.get("hole_depth_estimate"))
|
||||
if current_depth is None:
|
||||
current_depth = _float_or_none(info.get("same_domain_height_estimate"))
|
||||
manual_bottom = bottom_face_id is not None
|
||||
|
||||
if surface != "cylinder":
|
||||
blockers.append("当前选中对象不是圆柱孔/槽,不能调整孔深。")
|
||||
if guess != "hole/groove candidate":
|
||||
blockers.append("孔深调整当前版本只支持明确的孔/槽候选。")
|
||||
if end_type != "blind" and not manual_bottom:
|
||||
blockers.append("孔深调整需要识别到盲孔/盲槽底面,或手动填写底面 Face ID。")
|
||||
elif end_type != "blind" and manual_bottom:
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("端部类型不是明确 blind,当前会按手动底面 Face ID 尝试推断孔深方向。")
|
||||
if current_depth is None or current_depth <= 1e-9:
|
||||
blockers.append("当前圆柱面没有可靠的深度估算。")
|
||||
if target_depth <= 0:
|
||||
blockers.append("目标深度必须大于 0。")
|
||||
|
||||
if guess == "hole/groove candidate" and confidence == "low":
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("孔/槽判断置信度较低。")
|
||||
if (
|
||||
guess == "hole/groove candidate"
|
||||
and angular_span is not None
|
||||
and angular_span < math.tau * 0.92
|
||||
):
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("这是局部圆柱面,更像槽或半孔,孔深调整会按局部槽处理。")
|
||||
|
||||
delta_depth = None
|
||||
delta_ratio = None
|
||||
depth_mode = "unknown"
|
||||
if current_depth is not None and current_depth > 1e-9:
|
||||
delta_depth = target_depth - current_depth
|
||||
delta_ratio = abs(delta_depth) / current_depth
|
||||
depth_mode = "deepen" if delta_depth > 0 else "shallow"
|
||||
if abs(delta_depth) <= max(current_depth * 1e-5, 1e-6):
|
||||
blockers.append("目标深度与当前深度几乎相同,不需要修改。")
|
||||
elif delta_ratio > 1.0:
|
||||
risk = self._max_quick_risk(risk, "high")
|
||||
warnings.append("目标深度变化超过当前深度的 100%,很可能贯穿、误切或布尔失败。")
|
||||
elif delta_ratio > 0.35:
|
||||
risk = self._max_quick_risk(risk, "medium")
|
||||
warnings.append("目标深度变化超过当前深度的 35%。")
|
||||
if target_depth < current_depth * 0.08:
|
||||
risk = self._max_quick_risk(risk, "high")
|
||||
warnings.append("目标深度非常浅,补料后可能生成很薄的局部面。")
|
||||
|
||||
if blockers:
|
||||
status = "blocked"
|
||||
risk = "blocked"
|
||||
elif risk in {"medium", "high"}:
|
||||
status = "caution"
|
||||
|
||||
message = ";".join(blockers + warnings) if blockers or warnings else "可以尝试调整盲孔/盲槽深度。"
|
||||
return {
|
||||
"status": status,
|
||||
"risk": risk,
|
||||
"message": message,
|
||||
"warnings": ";".join(warnings),
|
||||
"blockers": ";".join(blockers),
|
||||
"face_id": face_id,
|
||||
"part_id": info.get("part_id"),
|
||||
"solid_id": info.get("solid_id"),
|
||||
"current_depth": current_depth,
|
||||
"target_depth": target_depth,
|
||||
"delta_depth": delta_depth,
|
||||
"depth_delta_ratio": delta_ratio,
|
||||
"depth_mode": depth_mode,
|
||||
"diameter": info.get("diameter"),
|
||||
"radius": info.get("radius"),
|
||||
"feature_type": info.get("feature_type"),
|
||||
"feature_guess": guess,
|
||||
"confidence": confidence,
|
||||
"angular_span": angular_span,
|
||||
"cylinder_end_type": end_type,
|
||||
"feature_bottom_face_ids": info.get("feature_bottom_face_ids"),
|
||||
"manual_bottom_face_id": "" if bottom_face_id is None else bottom_face_id,
|
||||
"manual_bottom_face_used": manual_bottom,
|
||||
"feature_opening_face_ids": info.get("feature_opening_face_ids"),
|
||||
"feature_bottom_confidence": info.get("feature_bottom_confidence"),
|
||||
"feature_bottom_detection": info.get("feature_bottom_detection"),
|
||||
"feature_bottom_note": info.get("feature_bottom_note"),
|
||||
"quick_preflight": True,
|
||||
}
|
||||
|
||||
def resize_hole_depth(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -896,62 +1133,30 @@ class WindowActionMixin:
|
||||
QMessageBox.critical(self, "底面 Face ID 无效", str(exc))
|
||||
return
|
||||
|
||||
info = self.model.face_info(self.selected_face_id)
|
||||
if info.get("surface") != "cylinder":
|
||||
QMessageBox.information(self, "不是圆柱孔/槽", "当前选中的 Face 不是可调整孔深的圆柱孔/槽。")
|
||||
return
|
||||
plan = self.model.cylindrical_depth_plan(
|
||||
self.selected_face_id,
|
||||
target_depth,
|
||||
bottom_face_id=bottom_face_id,
|
||||
)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整孔深", str(plan["message"]))
|
||||
self.statusBar().showMessage("盲孔深度调整已阻止")
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认盲孔深度调整",
|
||||
(
|
||||
f"face: {plan['face_id']}\n"
|
||||
f"当前深度: {_format_value(plan['current_depth'])}\n"
|
||||
f"目标深度: {_format_value(plan['target_depth'])}\n"
|
||||
f"深度变化量: {_format_value(plan['delta_depth'])}\n"
|
||||
f"深度变化比例: {_format_percent(plan['depth_delta_ratio'])}\n"
|
||||
f"调整模式: {plan['depth_mode']}\n"
|
||||
f"候选判断: {plan['feature_guess']}\n"
|
||||
f"置信度: {plan['confidence']}\n"
|
||||
f"材料投票: {plan['material_vote_summary']}\n"
|
||||
f"端部类型: {plan['cylinder_end_type']}\n"
|
||||
f"疑似底面 Face: {_format_value(plan.get('feature_bottom_face_ids', ''))}\n"
|
||||
f"手动底面 Face: {_format_value(plan.get('manual_bottom_face_id', ''))}\n"
|
||||
f"底面识别来源: {_format_value(plan.get('feature_bottom_detection', ''))}\n"
|
||||
f"当前深度来源: {_format_value(plan.get('depth_current_depth_source', ''))}\n"
|
||||
f"开口方向来源: {_format_value(plan.get('depth_open_direction_source', ''))}\n"
|
||||
f"开口端相邻 Face: {_format_value(plan.get('feature_opening_face_ids', ''))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"工具策略: {plan['depth_tool_strategy']}\n"
|
||||
f"工具类型: {plan['depth_tool_role']}\n"
|
||||
f"工具高度: {_format_value(plan['depth_tool_height'])}\n"
|
||||
f"工具半径: {_format_value(plan['depth_tool_radius'])}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"继续操作会对当前零件执行受限B-Rep布尔修改。\n\n"
|
||||
"确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage("已取消盲孔深度调整")
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
self._show_cylinder_depth_preview(face_id, target_depth, bottom_face_id=bottom_face_id)
|
||||
info = self._selected_face_info_snapshot()
|
||||
plan = self._quick_cylinder_depth_plan(face_id, target_depth, bottom_face_id=bottom_face_id, info=info)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前深度: {_format_value(plan.get('current_depth'))}",
|
||||
f"目标深度: {_format_value(plan.get('target_depth'))}",
|
||||
f"深度变化量: {_format_value(plan.get('delta_depth'))}",
|
||||
f"深度变化比例: {_format_percent(plan.get('depth_delta_ratio'))}",
|
||||
f"候选判断: {plan.get('feature_guess')}",
|
||||
f"置信度: {plan.get('confidence')}",
|
||||
f"端部类型: {plan.get('cylinder_end_type')}",
|
||||
]
|
||||
if bottom_face_id is not None:
|
||||
lines.append(f"手动底面 Face: {bottom_face_id}")
|
||||
if not self._confirm_quick_edit_plan(
|
||||
"盲孔深度调整",
|
||||
plan,
|
||||
lines,
|
||||
"盲孔深度调整已阻止",
|
||||
"已取消盲孔深度调整",
|
||||
):
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.resize_cylindrical_depth(
|
||||
@@ -989,18 +1194,8 @@ class WindowActionMixin:
|
||||
"feature_bottom_confidence": plan.get("feature_bottom_confidence"),
|
||||
"feature_bottom_detection": plan.get("feature_bottom_detection"),
|
||||
"feature_bottom_note": plan.get("feature_bottom_note"),
|
||||
"depth_tool_strategy": plan.get("depth_tool_strategy"),
|
||||
"depth_tool_role": plan.get("depth_tool_role"),
|
||||
"depth_tool_height": plan.get("depth_tool_height"),
|
||||
"depth_tool_radius": plan.get("depth_tool_radius"),
|
||||
"depth_tool_radius_overlap": plan.get("depth_tool_radius_overlap"),
|
||||
"depth_current_depth": plan.get("depth_current_depth"),
|
||||
"depth_current_depth_source": plan.get("depth_current_depth_source"),
|
||||
"depth_bottom_parameter_source": plan.get("depth_bottom_parameter_source"),
|
||||
"depth_open_direction_source": plan.get("depth_open_direction_source"),
|
||||
"depth_open_point": plan.get("depth_open_point"),
|
||||
"depth_current_bottom_point": plan.get("depth_current_bottom_point"),
|
||||
"depth_target_bottom_point": plan.get("depth_target_bottom_point"),
|
||||
"quick_preflight": True,
|
||||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
@@ -1135,7 +1330,7 @@ class WindowActionMixin:
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"继续操作会调用OCCT倒圆并真实修改当前零件。\n\n"
|
||||
"继续操作会调用OCCT倒圆并真实修改当前零件的局部几何。\n\n"
|
||||
"确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
@@ -1212,7 +1407,7 @@ class WindowActionMixin:
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"继续操作会调用OCCT倒角并真实修改当前零件。\n\n"
|
||||
"继续操作会调用OCCT倒角并真实修改当前零件的局部几何。\n\n"
|
||||
"确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
@@ -1546,7 +1741,7 @@ class WindowActionMixin:
|
||||
if self._edit_busy("请等待当前编辑完成后再平移零件。"):
|
||||
return
|
||||
if self.selected_part_id is None:
|
||||
QMessageBox.information(self, "未选择零件", "请先选择一个Part,或选择属于某个Part的对象。")
|
||||
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
|
||||
return
|
||||
try:
|
||||
vector = self._translation_vector_from_inputs()
|
||||
@@ -1569,7 +1764,7 @@ class WindowActionMixin:
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="平移零件",
|
||||
target=f"Part {part_id}",
|
||||
target=f"零件 {part_id}",
|
||||
parameters={
|
||||
"part_id": part_id,
|
||||
"translation_vector": vector,
|
||||
@@ -1652,7 +1847,7 @@ class WindowActionMixin:
|
||||
title,
|
||||
(
|
||||
f"对象: {plan.get('target_kind', '')}\n"
|
||||
f"Part: {_format_value(plan.get('part_id', ''))}\n"
|
||||
f"零件: {_format_value(plan.get('part_id', ''))}\n"
|
||||
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
|
||||
f"平移向量: {_format_value(plan['translation_vector'])}\n"
|
||||
f"平移距离: {_format_value(plan['translation_distance'])}\n"
|
||||
@@ -1674,7 +1869,7 @@ class WindowActionMixin:
|
||||
if self._edit_busy("请等待当前编辑完成后再旋转零件。"):
|
||||
return
|
||||
if self.selected_part_id is None:
|
||||
QMessageBox.information(self, "未选择零件", "请先选择一个Part,或选择属于某个Part的对象。")
|
||||
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
|
||||
return
|
||||
try:
|
||||
axis, angle = self._rotation_values_from_inputs()
|
||||
@@ -1697,7 +1892,7 @@ class WindowActionMixin:
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="旋转零件",
|
||||
target=f"Part {part_id}",
|
||||
target=f"零件 {part_id}",
|
||||
parameters={
|
||||
"part_id": part_id,
|
||||
"rotation_axis": plan.get("rotation_axis"),
|
||||
@@ -1779,7 +1974,7 @@ class WindowActionMixin:
|
||||
title,
|
||||
(
|
||||
f"对象: {plan.get('target_kind', '')}\n"
|
||||
f"Part: {_format_value(plan.get('part_id', ''))}\n"
|
||||
f"零件: {_format_value(plan.get('part_id', ''))}\n"
|
||||
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
|
||||
f"旋转轴: {_format_value(plan['rotation_axis'])}\n"
|
||||
f"旋转角度: {_format_value(plan['rotation_angle_degrees'])}\n"
|
||||
@@ -1923,6 +2118,10 @@ class WindowActionMixin:
|
||||
scan_label: str,
|
||||
limit: int,
|
||||
) -> None:
|
||||
if not hasattr(self, "editable_table"):
|
||||
if show_info:
|
||||
self.set_plain_info("可编辑对象面板当前未启用。")
|
||||
return
|
||||
self.editable_table.setRowCount(len(candidates))
|
||||
lines = [f"可编辑对象:显示 {len(candidates)} 个({scan_label}上限 {limit})"]
|
||||
for row, item in enumerate(candidates):
|
||||
@@ -1956,7 +2155,7 @@ class WindowActionMixin:
|
||||
self.editable_table.setItem(row, column, table_item)
|
||||
lines.append(
|
||||
f"{target_kind} {target_id}: {item['operation']}, "
|
||||
f"Part={item['part_id']}, Solid={item['solid_id']}, "
|
||||
f"零件={item['part_id']}, Solid={item['solid_id']}, "
|
||||
f"object={item['feature_guess']}, "
|
||||
f"{item['current_value_label']}={_format_value(item['current_value'])}, "
|
||||
f"status={item['status']}, risk={item['risk']}, "
|
||||
@@ -2006,6 +2205,10 @@ class WindowActionMixin:
|
||||
self.statusBar().showMessage("圆柱面候选扫描失败")
|
||||
|
||||
def _filter_cached_cylinder_candidates(self, show_info: bool = False) -> None:
|
||||
if not hasattr(self, "cylinder_table"):
|
||||
if show_info:
|
||||
self.set_plain_info("圆柱面候选面板当前未启用。")
|
||||
return
|
||||
if not self.cylinder_candidates_loaded:
|
||||
self.cylinder_table.setRowCount(0)
|
||||
if show_info:
|
||||
@@ -2019,6 +2222,10 @@ class WindowActionMixin:
|
||||
self._populate_cylinder_table(candidates, show_info=show_info)
|
||||
|
||||
def _populate_cylinder_table(self, candidates: list[dict[str, object]], show_info: bool = True) -> None:
|
||||
if not hasattr(self, "cylinder_table"):
|
||||
if show_info:
|
||||
self.set_plain_info("圆柱面候选面板当前未启用。")
|
||||
return
|
||||
self.cylinder_table.setRowCount(len(candidates))
|
||||
lines = [f"圆柱面候选:显示 {len(candidates)} 个"]
|
||||
for row, item in enumerate(candidates):
|
||||
@@ -2040,7 +2247,7 @@ class WindowActionMixin:
|
||||
self.cylinder_table.setItem(row, 6, risk_item)
|
||||
self.cylinder_table.setItem(row, 7, part_item)
|
||||
lines.append(
|
||||
f"Face {item['face_id']}: Part {item['part_id']}, "
|
||||
f"Face {item['face_id']}: 零件 {item['part_id']}, "
|
||||
f"guess={item['feature_guess']}, "
|
||||
f"diameter={_format_float(float(item['diameter']))}, "
|
||||
f"height={_format_float(float(item['height_estimate']))}, "
|
||||
@@ -2053,6 +2260,8 @@ class WindowActionMixin:
|
||||
self.set_plain_info("\n".join(lines))
|
||||
|
||||
def _candidate_matches_filter(self, feature_guess: str) -> bool:
|
||||
if not hasattr(self, "candidate_filter_combo"):
|
||||
return True
|
||||
current = self.candidate_filter_combo.currentText()
|
||||
if current == "All":
|
||||
return True
|
||||
@@ -2243,13 +2452,15 @@ class WindowActionMixin:
|
||||
if "推拉" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算,半透明预览会持续渲染,3D 视图仍可旋转查看。"
|
||||
elif "孔径" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算,红色预览表示切削范围,绿色预览表示补料范围。"
|
||||
progress_text = f"{operation_name} 正在后台计算;为避免复杂模型卡顿,已跳过同步红/绿预览。"
|
||||
elif "槽/半孔宽度" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;槽宽会在后台换算并重建孔/槽几何。"
|
||||
elif "凸台" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算,绿色预览表示扩大补料范围,红色预览表示缩小切削范围。"
|
||||
elif "封堵" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算,绿色预览表示封堵补料范围。"
|
||||
elif "孔深" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算,红色预览表示加深切削范围,绿色预览表示变浅补料范围。"
|
||||
progress_text = f"{operation_name} 正在后台计算;为避免复杂模型卡顿,已跳过同步孔深预览。"
|
||||
elif "圆角" in operation_name:
|
||||
if "已有" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算,蓝色预览表示将移除并重建的已有圆角面。"
|
||||
@@ -2276,6 +2487,9 @@ class WindowActionMixin:
|
||||
|
||||
@Slot(object)
|
||||
def _finish_edit_action(self, result: object) -> None:
|
||||
if hasattr(self, "_is_ui_thread") and not self._is_ui_thread():
|
||||
self._invoke_on_ui_thread(lambda result=result: self._finish_edit_action(result))
|
||||
return
|
||||
context = self.pending_edit_context
|
||||
if context is None:
|
||||
self._end_edit_task(clear_preview=True)
|
||||
@@ -2319,9 +2533,9 @@ class WindowActionMixin:
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False))
|
||||
)
|
||||
self.clear_edit_preview(render=False)
|
||||
self._reset_selection()
|
||||
self._populate_part_tree()
|
||||
self._rebuild_scene_from_polydata(model_polydata, edge_polydata, reset_camera=False)
|
||||
locator_note = self._locate_operation_record(record)
|
||||
except Exception as exc:
|
||||
rollback_message = self._restore_failed_edit_snapshot(result.get("snapshot") if isinstance(result, dict) else None)
|
||||
self._end_edit_task(clear_preview=True)
|
||||
@@ -2340,11 +2554,16 @@ class WindowActionMixin:
|
||||
if result.get("quality_warnings"):
|
||||
self.statusBar().showMessage("编辑完成,但有质量警告,请查看操作历史详情")
|
||||
else:
|
||||
self.statusBar().showMessage(message)
|
||||
self.set_plain_info(record.detail)
|
||||
selection_note = ";已保持当前选择" if self.selected_kind is not None else ""
|
||||
self.statusBar().showMessage(f"{message}{selection_note}")
|
||||
if self.selected_kind is None:
|
||||
self.set_plain_info(f"{record.detail}\n\n{locator_note}")
|
||||
|
||||
@Slot(str)
|
||||
def _fail_edit_action(self, message: str) -> None:
|
||||
if hasattr(self, "_is_ui_thread") and not self._is_ui_thread():
|
||||
self._invoke_on_ui_thread(lambda message=message: self._fail_edit_action(message))
|
||||
return
|
||||
self._end_edit_task(clear_preview=True)
|
||||
QMessageBox.critical(self, "操作失败", message)
|
||||
self._clear_editable_candidates()
|
||||
|
||||
Reference in New Issue
Block a user