2716 lines
128 KiB
Python
2716 lines
128 KiB
Python
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
import math
|
||
from pathlib import Path
|
||
|
||
import vtk
|
||
from PySide6.QtCore import Qt, QThread, QTimer, Slot
|
||
from PySide6.QtWidgets import (
|
||
QApplication,
|
||
QFileDialog,
|
||
QMessageBox,
|
||
QTableWidgetItem,
|
||
QTreeWidgetItem,
|
||
)
|
||
|
||
from .model import StepModel
|
||
from .records import OperationRecord
|
||
from .ui_helpers import * # noqa: F403
|
||
from .workers import EditWorker, LoadWorker, ScanWorker
|
||
|
||
|
||
def _message_field(message: str, key: str) -> str | None:
|
||
marker = f"{key}="
|
||
start = message.find(marker)
|
||
if start < 0:
|
||
return None
|
||
start += len(marker)
|
||
end = message.find(",", start)
|
||
if end < 0:
|
||
end = len(message)
|
||
value = message[start:end].strip().rstrip(".")
|
||
return value or None
|
||
|
||
|
||
class WindowActionMixin:
|
||
def export_all(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||
return
|
||
if not self._confirm_export_quality("all"):
|
||
return
|
||
target, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"导出当前完整 STEP",
|
||
str(self.step_path.parent / f"{self.step_path.stem}_edited.step"),
|
||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||
)
|
||
if not target:
|
||
return
|
||
self._run_action(lambda: self.model.export_all(target), f"已导出 {Path(target).name}")
|
||
|
||
def export_selected_part(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||
return
|
||
if self.selected_part_id is None:
|
||
QMessageBox.information(self, "未选择零件", "请先选择一个零件。")
|
||
return
|
||
if not self._confirm_export_quality("part", self.selected_part_id):
|
||
return
|
||
part = self.model.part_by_id(self.selected_part_id)
|
||
default_name = f"{part.name if part else 'part'}_export.step".replace(" ", "_")
|
||
target, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"导出选中零件",
|
||
str(self.step_path.parent / default_name),
|
||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||
)
|
||
if not target:
|
||
return
|
||
self._run_action(
|
||
lambda: self.model.export_part(self.selected_part_id, target),
|
||
f"已导出选中零件到 {Path(target).name}",
|
||
)
|
||
|
||
def export_selected_solid(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||
return
|
||
if self.selected_solid_id is None:
|
||
QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择一个属于 Solid 的 Face。")
|
||
return
|
||
if not self._confirm_export_quality("solid", self.selected_solid_id):
|
||
return
|
||
target, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"导出选中 Solid",
|
||
str(self.step_path.parent / f"solid_{self.selected_solid_id}_export.step"),
|
||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||
)
|
||
if not target:
|
||
return
|
||
self._run_action(
|
||
lambda: self.model.export_solid(self.selected_solid_id, target),
|
||
f"已导出选中 Solid 到 {Path(target).name}",
|
||
)
|
||
|
||
def export_selected_face(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择 Face", "请先切换到 Face 或特征模式并选择一个 Face。")
|
||
return
|
||
if not self._confirm_export_quality("face", self.selected_face_id):
|
||
return
|
||
target, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"导出选中面区域",
|
||
str(self.step_path.parent / f"face_region_{self.selected_face_id}_export.step"),
|
||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||
)
|
||
if not target:
|
||
return
|
||
self._run_action(
|
||
lambda: self.model.export_face(self.selected_face_id, target),
|
||
f"已导出选中面区域到 {Path(target).name}",
|
||
)
|
||
|
||
def export_selected_feature(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择特征", "请先切换到特征模式并选择一个特征。")
|
||
return
|
||
if not self._confirm_export_quality("feature", self.selected_face_id):
|
||
return
|
||
target, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"导出选中特征区域",
|
||
str(self.step_path.parent / f"feature_face_{self.selected_face_id}_export.step"),
|
||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||
)
|
||
if not target:
|
||
return
|
||
self._run_action(
|
||
lambda: self.model.export_feature(self.selected_face_id, target),
|
||
f"已导出选中特征区域到 {Path(target).name}",
|
||
)
|
||
|
||
def export_selected_edge(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再导出。"):
|
||
return
|
||
if self.selected_edge_id is None:
|
||
QMessageBox.information(self, "未选择Edge", "请先切换到Edge模式并选择一个Edge。")
|
||
return
|
||
if not self._confirm_export_quality("edge", self.selected_edge_id):
|
||
return
|
||
target, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"导出选中 Edge",
|
||
str(self.step_path.parent / f"edge_{self.selected_edge_id}_export.step"),
|
||
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
|
||
)
|
||
if not target:
|
||
return
|
||
self._run_action(
|
||
lambda: self.model.export_edge(self.selected_edge_id, target),
|
||
f"已导出选中 Edge 到 {Path(target).name}",
|
||
)
|
||
|
||
def check_export_quality(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("编辑计算中,暂时不能检查导出质量。"):
|
||
return
|
||
scope, target_id = self._current_export_quality_target()
|
||
try:
|
||
info = self.model.export_quality_info(scope, target_id)
|
||
except Exception as exc:
|
||
QMessageBox.critical(self, "质量检查失败", str(exc))
|
||
self.statusBar().showMessage("导出质量检查失败")
|
||
return
|
||
report = _export_quality_text(info)
|
||
self.set_plain_info(report)
|
||
if info.get("quality_status") == "ok":
|
||
self.statusBar().showMessage("导出质量检查通过")
|
||
else:
|
||
self.statusBar().showMessage("导出质量检查发现警告")
|
||
|
||
def _current_export_quality_target(self) -> tuple[str, int | None]:
|
||
if self.selected_kind == "feature" and self.selected_face_id is not None:
|
||
return "feature", self.selected_face_id
|
||
if self.selected_kind == "face" and self.selected_face_id is not None:
|
||
return "face", self.selected_face_id
|
||
if self.selected_kind == "edge" and self.selected_edge_id is not None:
|
||
return "edge", self.selected_edge_id
|
||
if self.selected_kind == "edge":
|
||
if self.selected_solid_id is not None:
|
||
return "solid", self.selected_solid_id
|
||
if self.selected_part_id is not None:
|
||
return "part", self.selected_part_id
|
||
if self.selected_kind == "solid" and self.selected_solid_id is not None:
|
||
return "solid", self.selected_solid_id
|
||
if self.selected_kind == "part" and self.selected_part_id is not None:
|
||
return "part", self.selected_part_id
|
||
return "all", None
|
||
|
||
def _confirm_export_quality(self, scope: str, target_id: int | None = None) -> bool:
|
||
if self.model is None:
|
||
return False
|
||
try:
|
||
info = self.model.export_quality_info(scope, target_id)
|
||
except Exception as exc:
|
||
QMessageBox.critical(self, "导出质量检查失败", str(exc))
|
||
self.statusBar().showMessage("导出质量检查失败")
|
||
return False
|
||
report = _export_quality_text(info)
|
||
self.set_plain_info(report)
|
||
if info.get("quality_status") == "ok":
|
||
return True
|
||
result = QMessageBox.question(
|
||
self,
|
||
"导出质量警告",
|
||
f"{report}\n\n仍然继续导出吗?",
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
return result == QMessageBox.StandardButton.Yes
|
||
|
||
def repair_model(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再修复模型。"):
|
||
return
|
||
|
||
def action():
|
||
return self.model.repair_model()
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修复当前模型",
|
||
target="当前完整模型",
|
||
parameters={"scope": "all", "repair_strategy": "ShapeFix + UnifySameDomain"},
|
||
target_kind=None,
|
||
target_id=None,
|
||
)
|
||
|
||
def repair_selected_shape(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再修复选中对象。"):
|
||
return
|
||
|
||
if self.selected_solid_id is not None:
|
||
solid_id = self.selected_solid_id
|
||
|
||
def action():
|
||
return self.model.repair_solid(solid_id)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修复选中Solid",
|
||
target=f"Solid {solid_id}",
|
||
parameters={
|
||
"scope": "solid",
|
||
"solid_id": solid_id,
|
||
"part_id": self.selected_part_id,
|
||
"repair_strategy": "ShapeFix + UnifySameDomain",
|
||
},
|
||
target_kind="solid",
|
||
target_id=solid_id,
|
||
)
|
||
return
|
||
|
||
if self.selected_part_id is not None:
|
||
part_id = self.selected_part_id
|
||
|
||
def action():
|
||
return self.model.repair_part(part_id)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修复选中零件",
|
||
target=f"零件 {part_id}",
|
||
parameters={
|
||
"scope": "part",
|
||
"part_id": part_id,
|
||
"repair_strategy": "ShapeFix + UnifySameDomain",
|
||
},
|
||
target_kind="part",
|
||
target_id=part_id,
|
||
)
|
||
return
|
||
|
||
QMessageBox.information(self, "未选择对象", "请先选择零件、Solid、Face 或 Edge。")
|
||
self.statusBar().showMessage("未选择可修复对象")
|
||
|
||
def push_pull_face(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再执行新的推拉。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择 Face", "请先选择一个平面 Face。")
|
||
return
|
||
try:
|
||
distance = float(self.offset_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "距离无效", "请输入数字形式的Face偏移距离。")
|
||
return
|
||
face_id = self.selected_face_id
|
||
plan = self.model.push_pull_plan(face_id, distance)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能推拉平面", str(plan["message"]))
|
||
self.statusBar().showMessage("推拉平面已阻止")
|
||
return
|
||
self._show_push_pull_preview(face_id, distance)
|
||
|
||
def action():
|
||
return self.model.push_pull_face(face_id, distance)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="推拉平面",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"semantic_distance": distance,
|
||
"distance_rule": "positive=outward fuse, negative=inward cut",
|
||
"surface": plan.get("surface"),
|
||
"outward_direction": plan.get("outward_direction"),
|
||
"direction_confidence": plan.get("direction_confidence"),
|
||
"direction_note": plan.get("direction_note"),
|
||
"push_pull_risk": plan.get("risk"),
|
||
"push_pull_status": plan.get("status"),
|
||
"push_pull_message": plan.get("message"),
|
||
"push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"),
|
||
"push_pull_scope_face_count": plan.get("push_pull_scope_face_count"),
|
||
"push_pull_scope_note": plan.get("push_pull_scope_note"),
|
||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def resize_shell_thickness(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再调整薄壁厚度。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择平面", "请先选择一个已识别到相对面的薄壁/壳体平面候选。")
|
||
return
|
||
if not hasattr(self, "shell_thickness_input"):
|
||
return
|
||
try:
|
||
target_thickness = float(self.shell_thickness_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "目标厚度无效", "请输入数字形式的目标薄壁厚度。")
|
||
return
|
||
|
||
face_id = self.selected_face_id
|
||
plan = self.model.shell_thickness_plan(face_id, target_thickness)
|
||
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['shell_current_thickness'])}\n"
|
||
f"目标厚度: {_format_value(plan['shell_target_thickness'])}\n"
|
||
f"厚度变化量: {_format_value(plan['shell_delta_thickness'])}\n"
|
||
f"厚度变化比例: {_format_percent(plan['shell_delta_ratio'])}\n"
|
||
f"相对平面 Face: {_format_value(plan.get('shell_opposite_face_id', ''))}\n"
|
||
f"相对平面重叠率: {_format_percent(plan.get('shell_overlap_ratio_estimate'))}\n"
|
||
f"置信度: {_format_value(plan.get('shell_confidence', ''))}\n"
|
||
f"当前面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\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_shell_thickness_preview(face_id, target_thickness)
|
||
|
||
def action():
|
||
return self.model.resize_shell_thickness(face_id, target_thickness)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="调整薄壁厚度",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"shell_region_kind": plan.get("shell_region_kind"),
|
||
"shell_confidence": plan.get("shell_confidence"),
|
||
"shell_source_face_ids": plan.get("shell_source_face_ids"),
|
||
"shell_opposite_face_id": plan.get("shell_opposite_face_id"),
|
||
"shell_current_thickness": plan.get("shell_current_thickness"),
|
||
"shell_target_thickness": plan.get("shell_target_thickness"),
|
||
"shell_delta_thickness": plan.get("shell_delta_thickness"),
|
||
"shell_delta_ratio": plan.get("shell_delta_ratio"),
|
||
"shell_signed_thickness": plan.get("shell_signed_thickness"),
|
||
"shell_overlap_ratio_estimate": plan.get("shell_overlap_ratio_estimate"),
|
||
"shell_opposite_normal_dot": plan.get("shell_opposite_normal_dot"),
|
||
"shell_desired_movement_vector": plan.get("shell_desired_movement_vector"),
|
||
"shell_movement_alignment": plan.get("shell_movement_alignment"),
|
||
"push_pull_distance": plan.get("push_pull_distance"),
|
||
"outward_direction": plan.get("outward_direction"),
|
||
"push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"),
|
||
"push_pull_scope_face_count": plan.get("push_pull_scope_face_count"),
|
||
"push_pull_scope_note": plan.get("push_pull_scope_note"),
|
||
"resize_strategy": plan.get("resize_strategy"),
|
||
"resize_status": plan.get("status"),
|
||
"resize_risk": plan.get("risk"),
|
||
"resize_message": plan.get("message"),
|
||
"resize_warnings": plan.get("warnings"),
|
||
"resize_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="feature" if self.selected_kind == "feature" else "face",
|
||
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
|
||
if self._edit_busy("请等待当前编辑完成后再调整孔径。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择圆柱面", "请先选择一个圆柱面。")
|
||
return
|
||
try:
|
||
diameter = float(self.hole_diameter_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "直径无效", "请输入数字形式的目标直径。")
|
||
return
|
||
face_id = self.selected_face_id
|
||
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)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="调整圆柱孔径",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"new_diameter": 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"),
|
||
"resize_mode": plan.get("resize_mode"),
|
||
"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_guess": plan.get("feature_guess"),
|
||
"confidence": plan.get("confidence"),
|
||
"resize_status": plan.get("status"),
|
||
"resize_risk": plan.get("risk"),
|
||
"resize_message": plan.get("message"),
|
||
"resize_warnings": plan.get("warnings"),
|
||
"resize_blockers": plan.get("blockers"),
|
||
"material_vote_summary": plan.get("material_vote_summary"),
|
||
"cylinder_end_type": plan.get("cylinder_end_type"),
|
||
"hole_depth_estimate": plan.get("hole_depth_estimate"),
|
||
"quick_preflight": True,
|
||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def resize_slot_width(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再调整槽/半孔宽度。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择槽/半孔", "请先选择一个槽/半孔候选 Face。")
|
||
return
|
||
if not hasattr(self, "slot_width_input"):
|
||
return
|
||
try:
|
||
target_width = float(self.slot_width_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "目标槽宽无效", "请输入数字形式的目标槽/半孔宽度。")
|
||
return
|
||
if target_width <= 0:
|
||
QMessageBox.critical(self, "目标槽宽无效", "目标槽/半孔宽度必须大于 0。")
|
||
return
|
||
|
||
face_id = self.selected_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
|
||
angular_span = _float_or_none(info.get("slot_angular_span"))
|
||
if angular_span is None:
|
||
angular_span = _float_or_none(info.get("angular_span"))
|
||
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
|
||
QMessageBox.information(self, "不能调整槽宽", "当前候选没有稳定的部分圆柱角度,不能换算槽宽。")
|
||
return
|
||
sin_half_span = math.sin(min(max(angular_span, 1e-6), math.tau - 1e-6) / 2.0)
|
||
if abs(sin_half_span) <= 1e-6:
|
||
QMessageBox.information(self, "不能调整槽宽", "当前槽圆弧角度过小,不能可靠换算为圆柱直径。")
|
||
return
|
||
|
||
target_diameter = target_width / sin_half_span
|
||
current_width = _float_or_none(info.get("slot_chord_width_estimate"))
|
||
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
|
||
self.clear_edit_preview(render=False)
|
||
|
||
def action():
|
||
return self.model.resize_cylindrical_hole(face_id, target_diameter)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="调整槽/半孔宽度",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"new_slot_width": target_width,
|
||
"old_slot_width": current_width,
|
||
"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": plan.get("current_diameter"),
|
||
"delta_diameter": plan.get("delta_diameter"),
|
||
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
|
||
"resize_mode": plan.get("resize_mode"),
|
||
"feature_type": plan.get("feature_type"),
|
||
"feature_guess": plan.get("feature_guess"),
|
||
"confidence": plan.get("confidence"),
|
||
"slot_status": info.get("slot_status"),
|
||
"slot_kind": info.get("slot_kind"),
|
||
"feature_slot_boundary_face_ids": info.get("feature_slot_boundary_face_ids"),
|
||
"resize_status": plan.get("status"),
|
||
"resize_risk": plan.get("risk"),
|
||
"resize_message": plan.get("message"),
|
||
"resize_warnings": plan.get("warnings"),
|
||
"resize_blockers": plan.get("blockers"),
|
||
"material_vote_summary": plan.get("material_vote_summary"),
|
||
"cylinder_end_type": plan.get("cylinder_end_type"),
|
||
"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"),
|
||
"quick_preflight": True,
|
||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def resize_boss(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再调整凸台直径。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择圆柱凸台", "请先选择一个圆柱凸台候选 Face。")
|
||
return
|
||
try:
|
||
diameter = float(self.boss_diameter_input.text())
|
||
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_boss_resize_plan(self.selected_face_id, diameter)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能调整凸台直径", str(plan["message"]))
|
||
self.statusBar().showMessage("圆柱凸台直径调整已阻止")
|
||
return
|
||
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['boss_tool_strategy']}\n"
|
||
f"工具高度: {_format_value(plan['boss_tool_height'])}\n"
|
||
f"轴向重叠余量: {_format_value(plan['boss_tool_axial_margin'])}\n"
|
||
f"风险: {plan['risk']}\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_boss_resize_preview(face_id, diameter)
|
||
|
||
def action():
|
||
return self.model.resize_cylindrical_boss(face_id, diameter)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="调整圆柱凸台直径",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"new_diameter": diameter,
|
||
"old_diameter": info.get("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"),
|
||
"resize_mode": plan.get("resize_mode"),
|
||
"feature_type": plan.get("feature_type"),
|
||
"feature_guess": plan.get("feature_guess"),
|
||
"confidence": plan.get("confidence"),
|
||
"boss_resize_status": plan.get("status"),
|
||
"boss_resize_risk": plan.get("risk"),
|
||
"boss_resize_message": plan.get("message"),
|
||
"boss_resize_warnings": plan.get("warnings"),
|
||
"boss_resize_blockers": plan.get("blockers"),
|
||
"material_vote_summary": plan.get("material_vote_summary"),
|
||
"boss_tool_strategy": plan.get("boss_tool_strategy"),
|
||
"boss_tool_height": plan.get("boss_tool_height"),
|
||
"boss_tool_axial_margin": plan.get("boss_tool_axial_margin"),
|
||
"boss_tool_radius": plan.get("boss_tool_radius"),
|
||
"boss_tool_outer_radius": plan.get("boss_tool_outer_radius"),
|
||
"boss_tool_inner_radius": plan.get("boss_tool_inner_radius"),
|
||
"boss_tool_radial_overlap": plan.get("boss_tool_radial_overlap"),
|
||
"boss_tool_note": plan.get("boss_tool_note"),
|
||
"feature_adjacent_face_ids": plan.get("feature_adjacent_face_ids"),
|
||
"feature_boundary_edge_ids": plan.get("feature_boundary_edge_ids"),
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def suppress_hole(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再封堵圆柱孔。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择圆柱面", "请先选择一个圆柱孔 Face。")
|
||
return
|
||
|
||
info = self.model.face_info(self.selected_face_id)
|
||
if info.get("surface") != "cylinder" or "diameter" not in info:
|
||
QMessageBox.information(self, "不是圆柱孔", "当前选中的 Face 不是可封堵的圆柱孔。")
|
||
return
|
||
plan = self.model.cylindrical_suppress_plan(self.selected_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['diameter'])}\n"
|
||
f"估算高度: {_format_value(plan['height_estimate'])}\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"风险: {plan['risk']}\n\n"
|
||
f"补料策略: {plan['fill_strategy']}\n"
|
||
f"补料半径: {_format_value(plan['fill_radius'])}\n"
|
||
f"补料高度: {_format_value(plan['fill_height'])}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会用补料体封堵当前孔,并对当前零件执行受限OCCT Fuse。\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_suppress_preview(face_id)
|
||
|
||
def action():
|
||
return self.model.suppress_cylindrical_hole(face_id)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="封堵圆柱孔",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"diameter": plan.get("diameter"),
|
||
"height_estimate": plan.get("height_estimate"),
|
||
"feature_type": plan.get("feature_type"),
|
||
"feature_guess": plan.get("feature_guess"),
|
||
"confidence": plan.get("confidence"),
|
||
"suppress_status": plan.get("status"),
|
||
"suppress_risk": plan.get("risk"),
|
||
"suppress_message": plan.get("message"),
|
||
"suppress_warnings": plan.get("warnings"),
|
||
"suppress_blockers": plan.get("blockers"),
|
||
"cylinder_end_type": plan.get("cylinder_end_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"),
|
||
"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"),
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def _hole_bottom_face_override(self) -> int | None:
|
||
if not hasattr(self, "hole_bottom_face_input"):
|
||
return None
|
||
text = self.hole_bottom_face_input.text().strip()
|
||
if not text:
|
||
return None
|
||
try:
|
||
bottom_face_id = int(text)
|
||
except ValueError as exc:
|
||
raise ValueError("底面 Face ID 必须是整数。") from exc
|
||
if self.model is not None and (bottom_face_id < 0 or bottom_face_id >= len(self.model.faces)):
|
||
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
|
||
if self._edit_busy("请等待当前编辑完成后再调整孔深。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择圆柱面", "请先选择一个盲孔或盲槽的圆柱面。")
|
||
return
|
||
try:
|
||
target_depth = float(self.hole_depth_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "深度无效", "请输入数字形式的目标孔深。")
|
||
return
|
||
try:
|
||
bottom_face_id = self._hole_bottom_face_override()
|
||
except ValueError as exc:
|
||
QMessageBox.critical(self, "底面 Face ID 无效", str(exc))
|
||
return
|
||
|
||
face_id = self.selected_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(
|
||
face_id,
|
||
target_depth,
|
||
bottom_face_id=bottom_face_id,
|
||
)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="调整盲孔深度",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"old_depth": plan.get("current_depth"),
|
||
"new_depth": target_depth,
|
||
"delta_depth": plan.get("delta_depth"),
|
||
"depth_delta_ratio": plan.get("depth_delta_ratio"),
|
||
"depth_mode": plan.get("depth_mode"),
|
||
"feature_type": plan.get("feature_type"),
|
||
"feature_guess": plan.get("feature_guess"),
|
||
"confidence": plan.get("confidence"),
|
||
"depth_status": plan.get("status"),
|
||
"depth_risk": plan.get("risk"),
|
||
"depth_message": plan.get("message"),
|
||
"depth_warnings": plan.get("warnings"),
|
||
"depth_blockers": plan.get("blockers"),
|
||
"cylinder_end_type": plan.get("cylinder_end_type"),
|
||
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
|
||
"manual_bottom_face_id": plan.get("manual_bottom_face_id"),
|
||
"manual_bottom_face_used": plan.get("manual_bottom_face_used"),
|
||
"manual_bottom_face_note": plan.get("manual_bottom_face_note"),
|
||
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
|
||
"feature_bottom_confidence": plan.get("feature_bottom_confidence"),
|
||
"feature_bottom_detection": plan.get("feature_bottom_detection"),
|
||
"feature_bottom_note": plan.get("feature_bottom_note"),
|
||
"quick_preflight": True,
|
||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def resize_existing_fillet(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再修改已有圆角。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择已有圆角", "请先选择一个已有圆角/倒圆候选 Face。")
|
||
return
|
||
try:
|
||
target_radius = float(self.edge_fillet_radius_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "半径无效", "请输入数字形式的目标圆角半径。")
|
||
return
|
||
|
||
face_id = self.selected_face_id
|
||
plan = self.model.existing_fillet_resize_plan(face_id, target_radius)
|
||
if self._block_unisolated_high_risk_operation("已阻止高风险已有圆角修改", plan):
|
||
return
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能修改已有圆角", str(plan["message"]))
|
||
self.statusBar().showMessage("已有圆角半径修改已阻止")
|
||
return
|
||
|
||
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_radius'])}\n"
|
||
f"目标半径: {_format_value(plan['target_radius'])}\n"
|
||
f"半径变化量: {_format_value(plan['delta_radius'])}\n"
|
||
f"半径变化比例: {_format_percent(plan['radius_delta_ratio'])}\n"
|
||
f"圆弧跨度: {_format_value(plan.get('angular_span', ''))}\n"
|
||
f"长度估算: {_format_value(plan.get('height_estimate', ''))}\n"
|
||
f"支撑 Face: {_format_value(plan.get('feature_existing_fillet_support_face_ids', ''))}\n"
|
||
f"边界 Edge: {_format_value(plan.get('feature_boundary_edge_ids', ''))}\n"
|
||
f"策略: {plan['resize_strategy']}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会先尝试移除当前圆角面,再对恢复出的锐边重新倒圆。\n"
|
||
"这不是 CAD 历史特征参数编辑,复杂圆角可能失败。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消修改已有圆角半径")
|
||
return
|
||
|
||
self._show_existing_fillet_resize_preview(face_id, target_radius)
|
||
|
||
def action():
|
||
return self.model.resize_existing_fillet(face_id, target_radius)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修改已有圆角半径",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"old_radius": plan.get("current_radius"),
|
||
"new_radius": target_radius,
|
||
"delta_radius": plan.get("delta_radius"),
|
||
"radius_delta_ratio": plan.get("radius_delta_ratio"),
|
||
"feature_type": plan.get("feature_type"),
|
||
"feature_guess": plan.get("feature_guess"),
|
||
"confidence": plan.get("confidence"),
|
||
"existing_fillet_status": plan.get("status"),
|
||
"existing_fillet_risk": plan.get("risk"),
|
||
"existing_fillet_message": plan.get("message"),
|
||
"existing_fillet_warnings": plan.get("warnings"),
|
||
"existing_fillet_blockers": plan.get("blockers"),
|
||
"feature_existing_fillet_support_face_ids": plan.get("feature_existing_fillet_support_face_ids"),
|
||
"feature_boundary_edge_ids": plan.get("feature_boundary_edge_ids"),
|
||
"resize_strategy": plan.get("resize_strategy"),
|
||
"resize_note": plan.get("resize_note"),
|
||
"axis_point": plan.get("axis_point"),
|
||
"axis": plan.get("axis"),
|
||
"height_estimate": plan.get("height_estimate"),
|
||
"angular_span": plan.get("angular_span"),
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def fillet_edge(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再添加圆角。"):
|
||
return
|
||
if self.selected_edge_id is None:
|
||
QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。")
|
||
return
|
||
try:
|
||
radius = float(self.edge_fillet_radius_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "半径无效", "请输入数字形式的圆角半径。")
|
||
return
|
||
|
||
edge_id = self.selected_edge_id
|
||
plan = self.model.edge_fillet_plan(edge_id, radius)
|
||
if self._block_unisolated_high_risk_operation("已阻止高风险边圆角", plan):
|
||
return
|
||
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,
|
||
"确认给Edge添加圆角",
|
||
(
|
||
f"Edge: {plan['edge_id']}\n"
|
||
f"Edge长度: {_format_value(plan['edge_length'])}\n"
|
||
f"目标圆角半径: {_format_value(plan['target_radius'])}\n"
|
||
f"半径/Edge长度比例: {_format_percent(plan['radius_to_length_ratio'])}\n"
|
||
f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n"
|
||
f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会调用OCCT倒圆并真实修改当前零件的局部几何。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消添加圆角")
|
||
return
|
||
|
||
self._show_edge_fillet_preview(edge_id, radius)
|
||
|
||
def action():
|
||
return self.model.fillet_edge(edge_id, radius)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="给Edge添加圆角",
|
||
target=f"Edge {edge_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"edge_length": plan.get("edge_length"),
|
||
"fillet_radius": radius,
|
||
"radius_to_length_ratio": plan.get("radius_to_length_ratio"),
|
||
"curve": plan.get("curve"),
|
||
"adjacent_face_ids": plan.get("adjacent_face_ids"),
|
||
"adjacent_face_count": plan.get("adjacent_face_count"),
|
||
"fillet_status": plan.get("status"),
|
||
"fillet_risk": plan.get("risk"),
|
||
"fillet_message": plan.get("message"),
|
||
"fillet_warnings": plan.get("warnings"),
|
||
"fillet_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
)
|
||
|
||
def chamfer_edge(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再添加倒角。"):
|
||
return
|
||
if self.selected_edge_id is None:
|
||
QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。")
|
||
return
|
||
try:
|
||
distance = float(self.edge_chamfer_distance_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "距离无效", "请输入数字形式的倒角距离。")
|
||
return
|
||
|
||
edge_id = self.selected_edge_id
|
||
plan = self.model.edge_chamfer_plan(edge_id, distance)
|
||
if self._block_unisolated_high_risk_operation("已阻止高风险边倒角", plan):
|
||
return
|
||
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,
|
||
"确认给Edge添加倒角",
|
||
(
|
||
f"Edge: {plan['edge_id']}\n"
|
||
f"Edge长度: {_format_value(plan['edge_length'])}\n"
|
||
f"目标倒角距离: {_format_value(plan['target_distance'])}\n"
|
||
f"倒角距离/Edge长度比例: {_format_percent(plan['distance_to_length_ratio'])}\n"
|
||
f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n"
|
||
f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会调用OCCT倒角并真实修改当前零件的局部几何。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消添加倒角")
|
||
return
|
||
|
||
self._show_edge_chamfer_preview(edge_id, distance)
|
||
|
||
def action():
|
||
return self.model.chamfer_edge(edge_id, distance)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="给Edge添加倒角",
|
||
target=f"Edge {edge_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"edge_length": plan.get("edge_length"),
|
||
"chamfer_distance": distance,
|
||
"distance_to_length_ratio": plan.get("distance_to_length_ratio"),
|
||
"curve": plan.get("curve"),
|
||
"adjacent_face_ids": plan.get("adjacent_face_ids"),
|
||
"adjacent_face_count": plan.get("adjacent_face_count"),
|
||
"chamfer_status": plan.get("status"),
|
||
"chamfer_risk": plan.get("risk"),
|
||
"chamfer_message": plan.get("message"),
|
||
"chamfer_warnings": plan.get("warnings"),
|
||
"chamfer_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
)
|
||
|
||
def resize_edge_length(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再修改Edge长度。"):
|
||
return
|
||
if self.selected_edge_id is None:
|
||
QMessageBox.information(self, "未选择Edge", "请先选择一条Edge。")
|
||
return
|
||
try:
|
||
target_length = float(self.edge_target_length_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "目标长度无效", "请输入数字形式的Edge目标长度。")
|
||
return
|
||
|
||
edge_id = self.selected_edge_id
|
||
anchor_mode = self._edge_length_anchor_mode_from_ui()
|
||
plan = self.model.general_edge_length_plan(edge_id, target_length, anchor_mode=anchor_mode)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
|
||
self.statusBar().showMessage("Edge长度修改已阻止")
|
||
return
|
||
|
||
if plan["risk"] != "low":
|
||
warnings = str(plan.get("warnings", ""))
|
||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认修改Edge长度",
|
||
(
|
||
f"Edge: {plan['edge_id']}\n"
|
||
f"当前Edge长度: {_format_value(plan['current_length'])}\n"
|
||
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
|
||
f"变化量: {_format_value(plan['delta_length'])}\n"
|
||
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
||
f"策略: {_format_value(plan.get('resize_strategy', ''))}\n"
|
||
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
|
||
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
|
||
f"重建面数: {_format_value(plan.get('local_edge_deform_face_count', ''))}\n"
|
||
f"端面Face: {_format_value(plan.get('end_face_id', ''))}\n"
|
||
f"端面位置: {_format_value(plan.get('end_face_label', ''))}\n"
|
||
f"端面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n"
|
||
f"圆柱Face: {_format_value(plan.get('cylinder_resize_face_id', ''))}\n"
|
||
f"圆形Edge模式: {_format_value(plan.get('circular_edge_cylinder_mode_label', ''))}\n"
|
||
f"换算目标直径: {_format_value(plan.get('cylinder_resize_target_diameter', ''))}\n"
|
||
f"缩放类型: {_format_value(plan.get('affine_transform_label') or plan.get('affine_transform_kind', ''))}\n"
|
||
f"缩放说明: {_format_value(plan.get('affine_transform_note', ''))}\n"
|
||
f"缩放目标: {_format_value(plan.get('affine_target_kind', ''))}\n"
|
||
f"预估Edge长度: {_format_value(plan.get('affine_predicted_edge_length', ''))}\n"
|
||
f"缩放校正: {_format_value(plan.get('affine_scale_correction', ''))}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"当前会优先只移动被选直线Edge的端点并重建周边平面;没有明确局部路径时会尝试端面推拉、相邻圆柱直径编辑或几何缩放 fallback。"
|
||
"这不是通用参数化Edge长度编辑。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消Edge长度修改")
|
||
return
|
||
|
||
self._show_edge_length_preview(edge_id, target_length, anchor_mode=anchor_mode)
|
||
|
||
def action():
|
||
return self.model.resize_general_edge_length(edge_id, target_length, anchor_mode=anchor_mode)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修改Edge长度",
|
||
target=f"Edge {edge_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"current_length": plan.get("current_length"),
|
||
"target_length": plan.get("target_length"),
|
||
"delta_length": plan.get("delta_length"),
|
||
"length_change_ratio": plan.get("length_change_ratio"),
|
||
"edge_length_anchor_mode": plan.get("edge_length_anchor_mode"),
|
||
"edge_length_anchor_label": plan.get("edge_length_anchor_label"),
|
||
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
|
||
"local_edge_deform_anchor": plan.get("local_edge_deform_anchor"),
|
||
"local_edge_deform_face_count": plan.get("local_edge_deform_face_count"),
|
||
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
|
||
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
|
||
"local_edge_deform_moved_endpoint_count": plan.get("local_edge_deform_moved_endpoint_count"),
|
||
"local_edge_deform_risk": plan.get("local_edge_deform_risk"),
|
||
"local_edge_deform_note": plan.get("local_edge_deform_note"),
|
||
"end_face_id": plan.get("end_face_id"),
|
||
"end_face_endpoint_role": plan.get("end_face_endpoint_role"),
|
||
"end_face_label": plan.get("end_face_label"),
|
||
"push_pull_distance": plan.get("push_pull_distance"),
|
||
"resize_strategy": plan.get("resize_strategy"),
|
||
"affine_scale": plan.get("affine_scale"),
|
||
"affine_initial_scale": plan.get("affine_initial_scale"),
|
||
"affine_scale_correction": plan.get("affine_scale_correction"),
|
||
"affine_predicted_edge_length": plan.get("affine_predicted_edge_length"),
|
||
"affine_scale_refine_iterations": plan.get("affine_scale_refine_iterations"),
|
||
"affine_scale_refine_note": plan.get("affine_scale_refine_note"),
|
||
"affine_transform_kind": plan.get("affine_transform_kind"),
|
||
"affine_transform_label": plan.get("affine_transform_label"),
|
||
"affine_transform_note": plan.get("affine_transform_note"),
|
||
"affine_transform_warning": plan.get("affine_transform_warning"),
|
||
"affine_transform_risk": plan.get("affine_transform_risk"),
|
||
"affine_axis_point": plan.get("affine_axis_point"),
|
||
"affine_axis_direction": plan.get("affine_axis_direction"),
|
||
"affine_axis_source": plan.get("affine_axis_source"),
|
||
"affine_anchor_source": plan.get("affine_anchor_source"),
|
||
"affine_target_kind": plan.get("affine_target_kind"),
|
||
"planar_edge_scale_sample_count": plan.get("planar_edge_scale_sample_count"),
|
||
"planar_edge_scale_plane_deviation": plan.get("planar_edge_scale_plane_deviation"),
|
||
"circular_edge_current_radius": plan.get("circular_edge_current_radius"),
|
||
"circular_edge_target_radius": plan.get("circular_edge_target_radius"),
|
||
"circular_edge_length_scale": plan.get("circular_edge_length_scale"),
|
||
"circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"),
|
||
"circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"),
|
||
"circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"),
|
||
"cylinder_resize_face_id": plan.get("cylinder_resize_face_id"),
|
||
"cylinder_resize_operation": plan.get("cylinder_resize_operation"),
|
||
"cylinder_resize_current_diameter": plan.get("cylinder_resize_current_diameter"),
|
||
"cylinder_resize_target_diameter": plan.get("cylinder_resize_target_diameter"),
|
||
"cylinder_resize_delta_diameter": plan.get("cylinder_resize_delta_diameter"),
|
||
"cylinder_resize_delta_ratio": plan.get("cylinder_resize_delta_ratio"),
|
||
"cylinder_resize_status": plan.get("cylinder_resize_status"),
|
||
"cylinder_resize_risk": plan.get("cylinder_resize_risk"),
|
||
"cylinder_resize_feature_guess": plan.get("cylinder_resize_feature_guess"),
|
||
"cylinder_resize_confidence": plan.get("cylinder_resize_confidence"),
|
||
"cylinder_resize_same_domain_face_ids": plan.get("cylinder_resize_same_domain_face_ids"),
|
||
"cylinder_resize_same_domain_face_count": plan.get("cylinder_resize_same_domain_face_count"),
|
||
"resize_status": plan.get("status"),
|
||
"resize_risk": plan.get("risk"),
|
||
"resize_message": plan.get("message"),
|
||
"resize_warnings": plan.get("warnings"),
|
||
"resize_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
)
|
||
|
||
def resize_any_edge_length(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再修改Edge长度。"):
|
||
return
|
||
if self.selected_edge_id is None:
|
||
QMessageBox.information(self, "未选择Edge", "请先选择一条Edge。")
|
||
return
|
||
try:
|
||
target_length = float(self.edge_target_length_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "目标长度无效", "请输入数字形式的Edge目标长度。")
|
||
return
|
||
|
||
edge_id = self.selected_edge_id
|
||
anchor_mode = self._edge_length_anchor_mode_from_ui()
|
||
plan = self.model.general_edge_length_plan(edge_id, target_length, anchor_mode=anchor_mode)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
|
||
self.statusBar().showMessage("Edge长度修改已阻止")
|
||
return
|
||
|
||
warnings = str(plan.get("warnings", ""))
|
||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||
strategy = str(plan.get("resize_strategy", ""))
|
||
if plan["risk"] != "low":
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认修改Edge长度",
|
||
(
|
||
f"Edge: {plan['edge_id']}\n"
|
||
f"曲线类型: {_format_value(plan.get('curve', ''))}\n"
|
||
f"当前Edge长度: {_format_value(plan['current_length'])}\n"
|
||
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
|
||
f"变化量: {_format_value(plan['delta_length'])}\n"
|
||
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
||
f"策略: {_format_value(strategy)}\n"
|
||
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
|
||
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
|
||
f"重建面数: {_format_value(plan.get('local_edge_deform_face_count', ''))}\n"
|
||
f"端面Face: {_format_value(plan.get('end_face_id', ''))}\n"
|
||
f"端面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n"
|
||
f"圆柱Face: {_format_value(plan.get('cylinder_resize_face_id', ''))}\n"
|
||
f"圆形Edge模式: {_format_value(plan.get('circular_edge_cylinder_mode_label', ''))}\n"
|
||
f"换算目标直径: {_format_value(plan.get('cylinder_resize_target_diameter', ''))}\n"
|
||
f"缩放类型: {_format_value(plan.get('affine_transform_label') or plan.get('affine_transform_kind', ''))}\n"
|
||
f"缩放说明: {_format_value(plan.get('affine_transform_note', ''))}\n"
|
||
f"缩放目标: {_format_value(plan.get('affine_target_kind', ''))}\n"
|
||
f"缩放比例: {_format_value(plan.get('affine_scale', ''))}\n"
|
||
f"预估Edge长度: {_format_value(plan.get('affine_predicted_edge_length', ''))}\n"
|
||
f"缩放校正: {_format_value(plan.get('affine_scale_correction', ''))}\n"
|
||
f"轴线来源: {_format_value(plan.get('affine_axis_source', ''))}\n"
|
||
f"平面采样偏差: {_format_value(plan.get('planar_edge_scale_plane_deviation', ''))}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消Edge长度修改")
|
||
return
|
||
|
||
self._show_edge_length_preview(edge_id, target_length, anchor_mode=anchor_mode)
|
||
|
||
def action():
|
||
return self.model.resize_general_edge_length(edge_id, target_length, anchor_mode=anchor_mode)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修改Edge长度",
|
||
target=f"Edge {edge_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"curve": plan.get("curve"),
|
||
"current_length": plan.get("current_length"),
|
||
"target_length": plan.get("target_length"),
|
||
"delta_length": plan.get("delta_length"),
|
||
"length_change_ratio": plan.get("length_change_ratio"),
|
||
"edge_length_anchor_mode": plan.get("edge_length_anchor_mode"),
|
||
"edge_length_anchor_label": plan.get("edge_length_anchor_label"),
|
||
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
|
||
"local_edge_deform_anchor": plan.get("local_edge_deform_anchor"),
|
||
"local_edge_deform_face_count": plan.get("local_edge_deform_face_count"),
|
||
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
|
||
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
|
||
"local_edge_deform_moved_endpoint_count": plan.get("local_edge_deform_moved_endpoint_count"),
|
||
"local_edge_deform_risk": plan.get("local_edge_deform_risk"),
|
||
"local_edge_deform_note": plan.get("local_edge_deform_note"),
|
||
"end_face_id": plan.get("end_face_id"),
|
||
"end_face_endpoint_role": plan.get("end_face_endpoint_role"),
|
||
"end_face_label": plan.get("end_face_label"),
|
||
"push_pull_distance": plan.get("push_pull_distance"),
|
||
"resize_strategy": plan.get("resize_strategy"),
|
||
"affine_scale": plan.get("affine_scale"),
|
||
"affine_initial_scale": plan.get("affine_initial_scale"),
|
||
"affine_scale_correction": plan.get("affine_scale_correction"),
|
||
"affine_predicted_edge_length": plan.get("affine_predicted_edge_length"),
|
||
"affine_scale_refine_iterations": plan.get("affine_scale_refine_iterations"),
|
||
"affine_scale_refine_note": plan.get("affine_scale_refine_note"),
|
||
"affine_transform_kind": plan.get("affine_transform_kind"),
|
||
"affine_transform_label": plan.get("affine_transform_label"),
|
||
"affine_transform_note": plan.get("affine_transform_note"),
|
||
"affine_transform_warning": plan.get("affine_transform_warning"),
|
||
"affine_transform_risk": plan.get("affine_transform_risk"),
|
||
"affine_axis_point": plan.get("affine_axis_point"),
|
||
"affine_axis_direction": plan.get("affine_axis_direction"),
|
||
"affine_axis_source": plan.get("affine_axis_source"),
|
||
"affine_anchor_source": plan.get("affine_anchor_source"),
|
||
"affine_target_kind": plan.get("affine_target_kind"),
|
||
"planar_edge_scale_sample_count": plan.get("planar_edge_scale_sample_count"),
|
||
"planar_edge_scale_plane_deviation": plan.get("planar_edge_scale_plane_deviation"),
|
||
"circular_edge_current_radius": plan.get("circular_edge_current_radius"),
|
||
"circular_edge_target_radius": plan.get("circular_edge_target_radius"),
|
||
"circular_edge_length_scale": plan.get("circular_edge_length_scale"),
|
||
"circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"),
|
||
"circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"),
|
||
"circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"),
|
||
"cylinder_resize_face_id": plan.get("cylinder_resize_face_id"),
|
||
"cylinder_resize_operation": plan.get("cylinder_resize_operation"),
|
||
"cylinder_resize_current_diameter": plan.get("cylinder_resize_current_diameter"),
|
||
"cylinder_resize_target_diameter": plan.get("cylinder_resize_target_diameter"),
|
||
"cylinder_resize_delta_diameter": plan.get("cylinder_resize_delta_diameter"),
|
||
"cylinder_resize_delta_ratio": plan.get("cylinder_resize_delta_ratio"),
|
||
"cylinder_resize_status": plan.get("cylinder_resize_status"),
|
||
"cylinder_resize_risk": plan.get("cylinder_resize_risk"),
|
||
"cylinder_resize_feature_guess": plan.get("cylinder_resize_feature_guess"),
|
||
"cylinder_resize_confidence": plan.get("cylinder_resize_confidence"),
|
||
"cylinder_resize_same_domain_face_ids": plan.get("cylinder_resize_same_domain_face_ids"),
|
||
"cylinder_resize_same_domain_face_count": plan.get("cylinder_resize_same_domain_face_count"),
|
||
"resize_status": plan.get("status"),
|
||
"resize_risk": plan.get("risk"),
|
||
"resize_message": plan.get("message"),
|
||
"resize_warnings": plan.get("warnings"),
|
||
"resize_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
)
|
||
|
||
def _edge_length_anchor_mode_from_ui(self) -> str:
|
||
combo = getattr(self, "edge_length_anchor_combo", None)
|
||
if combo is None:
|
||
return "auto"
|
||
data = combo.currentData()
|
||
return str(data or "auto")
|
||
|
||
def translate_selected_part(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再平移零件。"):
|
||
return
|
||
if self.selected_part_id is None:
|
||
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
|
||
return
|
||
try:
|
||
vector = self._translation_vector_from_inputs()
|
||
except ValueError as exc:
|
||
QMessageBox.critical(self, "平移向量无效", str(exc))
|
||
return
|
||
part_id = self.selected_part_id
|
||
plan = self.model.translate_part_plan(part_id, vector)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能平移零件", str(plan["message"]))
|
||
self.statusBar().showMessage("平移零件已阻止")
|
||
return
|
||
if not self._confirm_translation_plan("确认平移零件", plan):
|
||
self.statusBar().showMessage("已取消平移零件")
|
||
return
|
||
|
||
def action():
|
||
return self.model.translate_part(part_id, vector)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="平移零件",
|
||
target=f"零件 {part_id}",
|
||
parameters={
|
||
"part_id": part_id,
|
||
"translation_vector": vector,
|
||
"translation_distance": plan.get("translation_distance"),
|
||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||
"translate_status": plan.get("status"),
|
||
"translate_risk": plan.get("risk"),
|
||
"translate_message": plan.get("message"),
|
||
"translate_warnings": plan.get("warnings"),
|
||
"translate_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="part",
|
||
target_id=part_id,
|
||
)
|
||
|
||
def translate_selected_solid(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再平移Solid。"):
|
||
return
|
||
if self.selected_solid_id is None:
|
||
QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择属于 Solid 的 Face/Edge。")
|
||
return
|
||
try:
|
||
vector = self._translation_vector_from_inputs()
|
||
except ValueError as exc:
|
||
QMessageBox.critical(self, "平移向量无效", str(exc))
|
||
return
|
||
solid_id = self.selected_solid_id
|
||
plan = self.model.translate_solid_plan(solid_id, vector)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能平移Solid", str(plan["message"]))
|
||
self.statusBar().showMessage("平移Solid已阻止")
|
||
return
|
||
if not self._confirm_translation_plan("确认平移Solid", plan):
|
||
self.statusBar().showMessage("已取消平移Solid")
|
||
return
|
||
|
||
def action():
|
||
return self.model.translate_solid(solid_id, vector)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="平移Solid",
|
||
target=f"Solid {solid_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": solid_id,
|
||
"part_solid_count": plan.get("part_solid_count"),
|
||
"translation_vector": vector,
|
||
"translation_distance": plan.get("translation_distance"),
|
||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||
"translate_status": plan.get("status"),
|
||
"translate_risk": plan.get("risk"),
|
||
"translate_message": plan.get("message"),
|
||
"translate_warnings": plan.get("warnings"),
|
||
"translate_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="solid",
|
||
target_id=solid_id,
|
||
)
|
||
|
||
def _translation_vector_from_inputs(self) -> tuple[float, float, float]:
|
||
try:
|
||
return (
|
||
float(self.translate_x_input.text()),
|
||
float(self.translate_y_input.text()),
|
||
float(self.translate_z_input.text()),
|
||
)
|
||
except ValueError as exc:
|
||
raise ValueError("请输入数字形式的 X/Y/Z 平移量。") from exc
|
||
|
||
def _confirm_translation_plan(self, title: str, plan: dict[str, object]) -> bool:
|
||
if plan["risk"] == "low":
|
||
return True
|
||
warnings = str(plan.get("warnings", ""))
|
||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||
result = QMessageBox.question(
|
||
self,
|
||
title,
|
||
(
|
||
f"对象: {plan.get('target_kind', '')}\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"
|
||
f"对象 bbox_diagonal: {_format_value(plan.get('bbox_diagonal', ''))}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会移动当前对象的 B-Rep 形状,并刷新模型拓扑。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
return result == QMessageBox.StandardButton.Yes
|
||
|
||
def rotate_selected_part(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再旋转零件。"):
|
||
return
|
||
if self.selected_part_id is None:
|
||
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
|
||
return
|
||
try:
|
||
axis, angle = self._rotation_values_from_inputs()
|
||
except ValueError as exc:
|
||
QMessageBox.critical(self, "旋转参数无效", str(exc))
|
||
return
|
||
part_id = self.selected_part_id
|
||
plan = self.model.rotate_part_plan(part_id, axis, angle)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能旋转零件", str(plan["message"]))
|
||
self.statusBar().showMessage("旋转零件已阻止")
|
||
return
|
||
if not self._confirm_rotation_plan("确认旋转零件", plan):
|
||
self.statusBar().showMessage("已取消旋转零件")
|
||
return
|
||
|
||
def action():
|
||
return self.model.rotate_part(part_id, axis, angle)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="旋转零件",
|
||
target=f"零件 {part_id}",
|
||
parameters={
|
||
"part_id": part_id,
|
||
"rotation_axis": plan.get("rotation_axis"),
|
||
"rotation_angle_degrees": angle,
|
||
"rotation_center": plan.get("rotation_center"),
|
||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||
"rotate_status": plan.get("status"),
|
||
"rotate_risk": plan.get("risk"),
|
||
"rotate_message": plan.get("message"),
|
||
"rotate_warnings": plan.get("warnings"),
|
||
"rotate_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="part",
|
||
target_id=part_id,
|
||
)
|
||
|
||
def rotate_selected_solid(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再旋转Solid。"):
|
||
return
|
||
if self.selected_solid_id is None:
|
||
QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择属于 Solid 的 Face/Edge。")
|
||
return
|
||
try:
|
||
axis, angle = self._rotation_values_from_inputs()
|
||
except ValueError as exc:
|
||
QMessageBox.critical(self, "旋转参数无效", str(exc))
|
||
return
|
||
solid_id = self.selected_solid_id
|
||
plan = self.model.rotate_solid_plan(solid_id, axis, angle)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能旋转Solid", str(plan["message"]))
|
||
self.statusBar().showMessage("旋转Solid已阻止")
|
||
return
|
||
if not self._confirm_rotation_plan("确认旋转Solid", plan):
|
||
self.statusBar().showMessage("已取消旋转Solid")
|
||
return
|
||
|
||
def action():
|
||
return self.model.rotate_solid(solid_id, axis, angle)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="旋转Solid",
|
||
target=f"Solid {solid_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": solid_id,
|
||
"part_solid_count": plan.get("part_solid_count"),
|
||
"rotation_axis": plan.get("rotation_axis"),
|
||
"rotation_angle_degrees": angle,
|
||
"rotation_center": plan.get("rotation_center"),
|
||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||
"rotate_status": plan.get("status"),
|
||
"rotate_risk": plan.get("risk"),
|
||
"rotate_message": plan.get("message"),
|
||
"rotate_warnings": plan.get("warnings"),
|
||
"rotate_blockers": plan.get("blockers"),
|
||
},
|
||
target_kind="solid",
|
||
target_id=solid_id,
|
||
)
|
||
|
||
def _rotation_values_from_inputs(self) -> tuple[str, float]:
|
||
try:
|
||
angle = float(self.rotate_angle_input.text())
|
||
except ValueError as exc:
|
||
raise ValueError("请输入数字形式的旋转角度。") from exc
|
||
return self.rotate_axis_combo.currentText(), angle
|
||
|
||
def _confirm_rotation_plan(self, title: str, plan: dict[str, object]) -> bool:
|
||
if plan["risk"] == "low":
|
||
return True
|
||
warnings = str(plan.get("warnings", ""))
|
||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||
result = QMessageBox.question(
|
||
self,
|
||
title,
|
||
(
|
||
f"对象: {plan.get('target_kind', '')}\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"
|
||
f"旋转中心: {_format_value(plan['rotation_center'])}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会旋转当前对象的 B-Rep 形状,并刷新模型拓扑。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
return result == QMessageBox.StandardButton.Yes
|
||
|
||
def _run_scan_task(self, scan_kind: str, action, context: dict[str, object] | None = None) -> None:
|
||
if self.scan_in_progress:
|
||
self.statusBar().showMessage("扫描中,请等待当前扫描完成。")
|
||
return
|
||
if self.operation_in_progress:
|
||
self.statusBar().showMessage("编辑计算中,请等待当前操作完成后再扫描。")
|
||
return
|
||
self.scan_in_progress = True
|
||
self.pending_scan_kind = scan_kind
|
||
self.pending_scan_context = dict(context or {})
|
||
self._clear_hover(render=True)
|
||
self._update_action_states()
|
||
self.scan_thread = None
|
||
self.scan_worker = None
|
||
QTimer.singleShot(0, lambda: self._run_scan_task_sync(action))
|
||
|
||
def _run_scan_task_sync(self, action) -> None:
|
||
if not self.scan_in_progress:
|
||
return
|
||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||
QApplication.processEvents()
|
||
try:
|
||
result = action()
|
||
except Exception as exc:
|
||
self._fail_scan_task_result(str(exc))
|
||
else:
|
||
self._finish_scan_task_result(result)
|
||
finally:
|
||
QApplication.restoreOverrideCursor()
|
||
|
||
@Slot(object)
|
||
def _finish_scan_task_result(self, result: object) -> None:
|
||
scan_kind = self.pending_scan_kind
|
||
context = dict(self.pending_scan_context or {})
|
||
if scan_kind == "editable":
|
||
self._finish_editable_scan(
|
||
result,
|
||
bool(context.get("show_info", True)),
|
||
str(context.get("scan_label", "扫描")),
|
||
int(context.get("limit", 0)),
|
||
)
|
||
return
|
||
if scan_kind == "cylinder":
|
||
self._finish_cylinder_scan(result, bool(context.get("show_info", True)))
|
||
return
|
||
self._end_scan_task()
|
||
self.statusBar().showMessage("扫描已完成,但扫描类型已丢失。")
|
||
|
||
@Slot(str)
|
||
def _fail_scan_task_result(self, message: str) -> None:
|
||
scan_kind = self.pending_scan_kind
|
||
if scan_kind == "editable":
|
||
self._fail_editable_scan(message)
|
||
return
|
||
if scan_kind == "cylinder":
|
||
self._fail_cylinder_scan(message)
|
||
return
|
||
self._end_scan_task()
|
||
QMessageBox.critical(self, "扫描失败", message)
|
||
self.statusBar().showMessage("扫描失败")
|
||
|
||
def _end_scan_task(self) -> None:
|
||
self.scan_in_progress = False
|
||
self.pending_scan_kind = None
|
||
self.pending_scan_context = None
|
||
self._update_action_states()
|
||
self._request_thread_quit(self.scan_thread)
|
||
|
||
def _forget_scan_thread(self) -> None:
|
||
self.scan_thread = None
|
||
self.scan_worker = None
|
||
|
||
def list_cylinders(self) -> None:
|
||
self.refresh_cylinder_candidates(show_info=True)
|
||
|
||
def _clear_editable_candidates(self) -> None:
|
||
if hasattr(self, "editable_table"):
|
||
self.editable_table.setRowCount(0)
|
||
|
||
def refresh_editable_candidates(self, show_info: bool = True, deep_scan: bool = False) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("编辑计算中,暂时不能扫描可编辑对象。"):
|
||
return
|
||
limit = 48 if deep_scan else 24
|
||
max_scan_faces = 1600 if deep_scan else 700
|
||
max_scan_edges = 2500 if deep_scan else 1200
|
||
scan_label = "深度扫描" if deep_scan else "扫描"
|
||
self.statusBar().showMessage(f"正在{scan_label}可编辑对象...")
|
||
|
||
def action():
|
||
if self.model is None:
|
||
return []
|
||
return self.model.editable_feature_candidates(
|
||
limit=limit,
|
||
max_scan_faces=max_scan_faces,
|
||
max_scan_edges=max_scan_edges,
|
||
)
|
||
|
||
self._run_scan_task(
|
||
"editable",
|
||
action,
|
||
{
|
||
"show_info": show_info,
|
||
"scan_label": scan_label,
|
||
"limit": limit,
|
||
},
|
||
)
|
||
|
||
def _finish_editable_scan(self, result: object, show_info: bool, scan_label: str, limit: int) -> None:
|
||
try:
|
||
candidates = list(result) if isinstance(result, (list, tuple)) else []
|
||
self._populate_editable_candidates(candidates, show_info, scan_label, limit)
|
||
finally:
|
||
self._end_scan_task()
|
||
|
||
def _fail_editable_scan(self, message: str) -> None:
|
||
self._end_scan_task()
|
||
QMessageBox.critical(self, "扫描失败", message)
|
||
self.statusBar().showMessage("可编辑对象扫描失败")
|
||
|
||
def _populate_editable_candidates(
|
||
self,
|
||
candidates: list[dict[str, object]],
|
||
show_info: bool,
|
||
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):
|
||
target_kind = str(item.get("target_kind", "face"))
|
||
target_id = int(item.get("target_id", item.get("face_id", item.get("edge_id", -1))))
|
||
operation_item = QTableWidgetItem(str(item["operation"]))
|
||
operation_item.setData(EDITABLE_TARGET_ID_ROLE, target_id)
|
||
operation_item.setData(EDITABLE_TARGET_KIND_ROLE, target_kind)
|
||
operation_item.setData(EDITABLE_ACTION_ROLE, str(item["operation_key"]))
|
||
id_item = QTableWidgetItem(f"{target_kind} {target_id}")
|
||
object_item = QTableWidgetItem(str(item["feature_guess"]))
|
||
current_item = QTableWidgetItem(
|
||
f"{item['current_value_label']}={_format_value(item['current_value'])}"
|
||
)
|
||
status_item = QTableWidgetItem(str(item["status"]))
|
||
risk_item = QTableWidgetItem(str(item["risk"]))
|
||
confidence_item = QTableWidgetItem(str(item["confidence"]))
|
||
note_item = QTableWidgetItem(str(item["note"]))
|
||
row_items = [
|
||
operation_item,
|
||
id_item,
|
||
object_item,
|
||
current_item,
|
||
status_item,
|
||
risk_item,
|
||
confidence_item,
|
||
note_item,
|
||
]
|
||
for column, table_item in enumerate(row_items):
|
||
table_item.setToolTip(table_item.text())
|
||
self.editable_table.setItem(row, column, table_item)
|
||
lines.append(
|
||
f"{target_kind} {target_id}: {item['operation']}, "
|
||
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']}, "
|
||
f"confidence={item['confidence']}, note={item['note']}"
|
||
)
|
||
self.editable_table.resizeColumnsToContents()
|
||
self.statusBar().showMessage(f"已{scan_label}可编辑对象:{len(candidates)} 个")
|
||
if show_info:
|
||
self.set_plain_info("\n".join(lines))
|
||
|
||
def _clear_cylinder_candidates(self) -> None:
|
||
self.cylinder_candidate_cache = []
|
||
self.cylinder_candidates_loaded = False
|
||
if hasattr(self, "cylinder_table"):
|
||
self.cylinder_table.setRowCount(0)
|
||
|
||
def refresh_cylinder_candidates(self, show_info: bool = True) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("编辑计算中,暂时不能扫描圆柱面候选。"):
|
||
return
|
||
self.statusBar().showMessage("正在扫描圆柱面候选...")
|
||
|
||
def action():
|
||
if self.model is None:
|
||
return []
|
||
return self.model.cylindrical_feature_candidates(limit=60, max_scan_faces=1200)
|
||
|
||
self._run_scan_task(
|
||
"cylinder",
|
||
action,
|
||
{"show_info": show_info},
|
||
)
|
||
|
||
def _finish_cylinder_scan(self, result: object, show_info: bool) -> None:
|
||
try:
|
||
self.cylinder_candidate_cache = list(result) if isinstance(result, (list, tuple)) else []
|
||
self.cylinder_candidates_loaded = True
|
||
self._filter_cached_cylinder_candidates(show_info=show_info)
|
||
finally:
|
||
self._end_scan_task()
|
||
|
||
def _fail_cylinder_scan(self, message: str) -> None:
|
||
self._end_scan_task()
|
||
self._clear_cylinder_candidates()
|
||
QMessageBox.critical(self, "扫描失败", message)
|
||
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:
|
||
self.set_plain_info("圆柱面候选尚未扫描。点击 `扫描圆柱面` 后再切换筛选类型。")
|
||
return
|
||
candidates = [
|
||
item
|
||
for item in self.cylinder_candidate_cache
|
||
if self._candidate_matches_filter(str(item["feature_guess"]))
|
||
]
|
||
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):
|
||
face_item = QTableWidgetItem(str(item["face_id"]))
|
||
face_item.setData(Qt.UserRole, int(item["face_id"]))
|
||
guess_item = QTableWidgetItem(str(item["feature_guess"]))
|
||
diameter_item = QTableWidgetItem(_format_float(float(item["diameter"])))
|
||
span_item = QTableWidgetItem(_format_float(float(item["angular_span"])))
|
||
height_item = QTableWidgetItem(_format_float(float(item["height_estimate"])))
|
||
confidence_item = QTableWidgetItem(str(item["confidence"]))
|
||
risk_item = QTableWidgetItem(str(item["resize_risk"]))
|
||
part_item = QTableWidgetItem(str(item["part_id"]))
|
||
self.cylinder_table.setItem(row, 0, face_item)
|
||
self.cylinder_table.setItem(row, 1, guess_item)
|
||
self.cylinder_table.setItem(row, 2, diameter_item)
|
||
self.cylinder_table.setItem(row, 3, span_item)
|
||
self.cylinder_table.setItem(row, 4, height_item)
|
||
self.cylinder_table.setItem(row, 5, confidence_item)
|
||
self.cylinder_table.setItem(row, 6, risk_item)
|
||
self.cylinder_table.setItem(row, 7, part_item)
|
||
lines.append(
|
||
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']))}, "
|
||
f"confidence={item['confidence']}, "
|
||
f"risk={item['resize_risk']}"
|
||
)
|
||
self.cylinder_table.resizeColumnsToContents()
|
||
self.statusBar().showMessage(f"已显示圆柱面候选:{len(candidates)} 个")
|
||
if show_info:
|
||
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
|
||
if current == "Hole/Groove":
|
||
return feature_guess == "hole/groove candidate"
|
||
if current == "Round/Fillet":
|
||
return feature_guess == "round/fillet candidate"
|
||
if current == "Boss/Outer":
|
||
return feature_guess == "boss/outer-round candidate"
|
||
if current == "Unclear":
|
||
return feature_guess == "cylindrical face"
|
||
return True
|
||
|
||
def _run_action(self, action, success_message: str | None) -> None:
|
||
try:
|
||
result = action()
|
||
except Exception as exc:
|
||
QMessageBox.critical(self, "操作失败", str(exc))
|
||
self.statusBar().showMessage("操作失败")
|
||
return
|
||
message = success_message or str(result)
|
||
self.statusBar().showMessage(message)
|
||
self.set_plain_info(message)
|
||
|
||
def _block_unisolated_high_risk_operation(self, title: str, plan: dict[str, object]) -> bool:
|
||
if str(plan.get("risk", "")) != "high":
|
||
return False
|
||
QMessageBox.information(
|
||
self,
|
||
title,
|
||
(
|
||
"该操作被稳定性保护阻止。\n\n"
|
||
"当前计划被判定为 high risk;这类 OCCT 布尔、倒圆或局部重建在复杂 STEP 上"
|
||
"可能不是普通失败,而是让进程卡死或直接退出。请先尝试更小的参数、修复模型、"
|
||
"选择更明确的面/边,或等后续子进程隔离执行通道实现后再开放。"
|
||
),
|
||
)
|
||
self.statusBar().showMessage("高风险操作已被稳定性保护阻止")
|
||
return True
|
||
|
||
def _run_edit_action(
|
||
self,
|
||
action,
|
||
operation_name: str,
|
||
target: str,
|
||
parameters: dict[str, object],
|
||
target_kind: str | None = None,
|
||
target_id: int | None = None,
|
||
) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self.operation_in_progress:
|
||
self.statusBar().showMessage("后台编辑正在计算,请等待当前操作完成。")
|
||
return
|
||
target_logical_id = self._edit_target_logical_id(target_kind, target_id)
|
||
context = {
|
||
"operation_name": operation_name,
|
||
"target": target,
|
||
"parameters": parameters,
|
||
"target_kind": target_kind,
|
||
"target_id": target_id,
|
||
"target_logical_id": target_logical_id,
|
||
"pick_position": self.selected_pick_position,
|
||
"show_same_domain_internal_edges": self._show_same_domain_internal_edges(),
|
||
}
|
||
|
||
self._begin_edit_task(operation_name)
|
||
self.pending_edit_context = context
|
||
thread = QThread(self)
|
||
worker = EditWorker(self._make_edit_job(action, context))
|
||
worker.moveToThread(thread)
|
||
thread.started.connect(worker.run)
|
||
worker.finished.connect(self._finish_edit_action, Qt.ConnectionType.QueuedConnection)
|
||
worker.failed.connect(self._fail_edit_action, Qt.ConnectionType.QueuedConnection)
|
||
thread.finished.connect(worker.deleteLater)
|
||
thread.finished.connect(thread.deleteLater)
|
||
thread.finished.connect(self._forget_edit_thread)
|
||
self.edit_thread = thread
|
||
self.edit_worker = worker
|
||
thread.start()
|
||
|
||
def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None:
|
||
if self.model is None or target_id is None or target_kind not in {"face", "feature"}:
|
||
return None
|
||
try:
|
||
numeric_id = int(target_id)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
if 0 <= numeric_id < len(self.model.faces):
|
||
try:
|
||
return self.model.face_region_logical_id(numeric_id)
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
def _make_edit_job(self, action, context: dict[str, object]):
|
||
def job():
|
||
if self.model is None:
|
||
raise RuntimeError("Model is not loaded.")
|
||
snapshot = self.model.snapshot()
|
||
target_part_id = self._edit_context_part_id(context)
|
||
before_stats = self.model.stats()
|
||
before_part_stats = self._part_stats_or_none(target_part_id)
|
||
before_geometry = self.model.geometry_stats()
|
||
try:
|
||
result = action()
|
||
after_snapshot = self.model.snapshot()
|
||
after_stats = self.model.stats()
|
||
after_part_stats = self._part_stats_or_none(target_part_id)
|
||
after_geometry = self.model.geometry_stats()
|
||
except Exception as exc:
|
||
try:
|
||
self.model.restore_snapshot(snapshot)
|
||
except Exception as rollback_exc:
|
||
raise RuntimeError(
|
||
f"编辑失败,且回滚到操作前状态也失败:{rollback_exc}\n原始错误:{exc}"
|
||
) from exc
|
||
raise RuntimeError(f"编辑失败,模型已恢复到操作前状态:{exc}") from exc
|
||
model_polydata = None
|
||
edge_polydata = None
|
||
try:
|
||
model_polydata = self.model.build_face_polydata()
|
||
edge_polydata = self.model.build_edge_polydata(
|
||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False))
|
||
)
|
||
except Exception:
|
||
model_polydata = None
|
||
edge_polydata = None
|
||
return {
|
||
"message": str(result),
|
||
"snapshot": snapshot,
|
||
"before_stats": before_stats,
|
||
"before_part_stats": before_part_stats,
|
||
"before_geometry": before_geometry,
|
||
"after_snapshot": after_snapshot,
|
||
"after_stats": after_stats,
|
||
"after_part_stats": after_part_stats,
|
||
"quality_warnings": _edit_quality_warnings(before_part_stats, after_part_stats),
|
||
"after_geometry": after_geometry,
|
||
"model_polydata": model_polydata,
|
||
"edge_polydata": edge_polydata,
|
||
}
|
||
|
||
return job
|
||
|
||
def _edit_context_part_id(self, context: dict[str, object]) -> int | None:
|
||
if self.model is None:
|
||
return None
|
||
parameters = context.get("parameters")
|
||
if isinstance(parameters, dict):
|
||
part_id = parameters.get("part_id")
|
||
if part_id not in {"", None}:
|
||
try:
|
||
return int(part_id)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
|
||
target_kind = context.get("target_kind")
|
||
target_id = context.get("target_id")
|
||
if target_id is None:
|
||
return None
|
||
try:
|
||
numeric_id = int(target_id)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
if target_kind == "part":
|
||
return numeric_id
|
||
if target_kind in {"face", "feature"} and 0 <= numeric_id < len(self.model.face_part_ids):
|
||
return int(self.model.face_part_ids[numeric_id])
|
||
if target_kind == "edge" and 0 <= numeric_id < len(self.model.edge_part_ids):
|
||
return int(self.model.edge_part_ids[numeric_id])
|
||
if target_kind == "solid" and 0 <= numeric_id < len(self.model.solids):
|
||
return int(self.model.solids[numeric_id][0])
|
||
return None
|
||
|
||
def _part_stats_or_none(self, part_id: int | None):
|
||
if self.model is None or part_id is None:
|
||
return None
|
||
try:
|
||
return self.model.part_topology_stats(part_id)
|
||
except Exception:
|
||
return None
|
||
|
||
def _begin_edit_task(self, operation_name: str) -> None:
|
||
self.operation_in_progress = True
|
||
self._update_action_states()
|
||
if "推拉" in operation_name:
|
||
progress_text = f"{operation_name} 正在后台计算,半透明预览会持续渲染,3D 视图仍可旋转查看。"
|
||
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} 正在后台计算,绿色预览表示封堵补料范围。"
|
||
elif "孔深" in operation_name:
|
||
progress_text = f"{operation_name} 正在后台计算;为避免复杂模型卡顿,已跳过同步孔深预览。"
|
||
elif "圆角" in operation_name:
|
||
if "已有" in operation_name:
|
||
progress_text = f"{operation_name} 正在后台计算,蓝色预览表示将移除并重建的已有圆角面。"
|
||
else:
|
||
progress_text = f"{operation_name} 正在后台计算,蓝色预览表示目标边和半径范围。"
|
||
elif "倒角" in operation_name:
|
||
progress_text = f"{operation_name} 正在后台计算,橙色预览表示目标边和倒角距离范围。"
|
||
elif "边长" in operation_name or "Edge长度" 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} 正在后台计算,完成后会刷新模型姿态和拓扑索引。"
|
||
else:
|
||
progress_text = f"{operation_name} 正在后台计算。"
|
||
self.statusBar().showMessage(progress_text)
|
||
if self.edit_preview_timer is not None:
|
||
self.edit_preview_timer.stop()
|
||
QApplication.processEvents()
|
||
|
||
def _set_edit_status_text(self, text: str) -> None:
|
||
self.statusBar().showMessage(text)
|
||
QApplication.processEvents()
|
||
|
||
@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)
|
||
self.statusBar().showMessage("编辑已完成,但上下文丢失")
|
||
return
|
||
if self.model is None:
|
||
self._end_edit_task(clear_preview=True)
|
||
return
|
||
self._set_edit_status_text("布尔计算已完成,正在刷新模型显示和历史记录...")
|
||
if not isinstance(result, dict):
|
||
self._end_edit_task(clear_preview=True)
|
||
QMessageBox.critical(self, "操作失败", "后台编辑返回了无法识别的结果。")
|
||
self.statusBar().showMessage("编辑结果无法识别")
|
||
return
|
||
message = str(result["message"])
|
||
try:
|
||
record = self._make_operation_record(
|
||
operation_name=str(context["operation_name"]),
|
||
target=str(context["target"]),
|
||
parameters=dict(context["parameters"]),
|
||
result_message=message,
|
||
before_stats=result["before_stats"],
|
||
after_stats=result["after_stats"],
|
||
before_part_stats=result.get("before_part_stats"),
|
||
after_part_stats=result.get("after_part_stats"),
|
||
quality_warnings=list(result.get("quality_warnings", [])),
|
||
before_geometry=dict(result["before_geometry"]),
|
||
after_geometry=dict(result["after_geometry"]),
|
||
target_kind=context["target_kind"],
|
||
target_id=context["target_id"],
|
||
target_logical_id=context.get("target_logical_id"),
|
||
pick_position=context["pick_position"],
|
||
before_snapshot=result["snapshot"],
|
||
after_snapshot=result["after_snapshot"],
|
||
)
|
||
model_polydata = result.get("model_polydata")
|
||
edge_polydata = result.get("edge_polydata")
|
||
if model_polydata is None or edge_polydata is None:
|
||
model_polydata = self.model.build_face_polydata()
|
||
edge_polydata = self.model.build_edge_polydata(
|
||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False))
|
||
)
|
||
self.clear_edit_preview(render=False)
|
||
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)
|
||
QMessageBox.critical(self, "操作失败", f"编辑结果刷新失败:{exc}\n\n{rollback_message}")
|
||
self.statusBar().showMessage("编辑已完成,但刷新结果时失败")
|
||
return
|
||
|
||
self.undo_stack.append(result["snapshot"])
|
||
self.redo_stack.clear()
|
||
self.operation_history.append(record)
|
||
self.redo_history.clear()
|
||
self._clear_editable_candidates()
|
||
self._clear_cylinder_candidates()
|
||
self._refresh_history_list()
|
||
self._end_edit_task(clear_preview=False)
|
||
if result.get("quality_warnings"):
|
||
self.statusBar().showMessage("编辑完成,但有质量警告,请查看操作历史详情")
|
||
else:
|
||
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()
|
||
self._clear_cylinder_candidates()
|
||
self.statusBar().showMessage("操作失败,模型已保持在编辑前状态")
|
||
|
||
def _restore_failed_edit_snapshot(self, snapshot: object) -> str:
|
||
if self.model is None or not isinstance(snapshot, dict):
|
||
return "未找到可用的编辑前快照,请重新加载 STEP 文件确认状态。"
|
||
try:
|
||
self.model.restore_snapshot(snapshot)
|
||
self._reset_selection()
|
||
self._populate_part_tree()
|
||
self._rebuild_scene(reset_camera=False)
|
||
self._clear_editable_candidates()
|
||
self._clear_cylinder_candidates()
|
||
except Exception as rollback_exc:
|
||
return f"尝试回滚到编辑前状态失败:{rollback_exc}。建议重新加载 STEP 文件。"
|
||
return "模型已回滚到编辑前状态。"
|
||
|
||
def _end_edit_task(self, clear_preview: bool = True) -> None:
|
||
self.operation_in_progress = False
|
||
self.pending_edit_context = None
|
||
if clear_preview:
|
||
self.clear_edit_preview(render=False)
|
||
self._update_action_states()
|
||
self._request_thread_quit(self.edit_thread)
|
||
QApplication.processEvents()
|
||
|
||
def _forget_edit_thread(self) -> None:
|
||
self.edit_thread = None
|
||
self.edit_worker = None
|
||
|
||
def _make_operation_record(
|
||
self,
|
||
operation_name: str,
|
||
target: str,
|
||
parameters: dict[str, object],
|
||
result_message: str,
|
||
before_stats,
|
||
after_stats,
|
||
before_geometry: dict[str, object],
|
||
after_geometry: dict[str, object],
|
||
before_part_stats=None,
|
||
after_part_stats=None,
|
||
quality_warnings: list[str] | None = None,
|
||
target_kind: str | None = None,
|
||
target_id: int | None = None,
|
||
target_logical_id: int | None = None,
|
||
pick_position: tuple[float, float, float] | None = None,
|
||
before_snapshot: dict[int, object] | None = None,
|
||
after_snapshot: dict[int, object] | None = None,
|
||
) -> OperationRecord:
|
||
target_summary = target
|
||
if target_kind in {"face", "feature"} and target_logical_id is not None:
|
||
target_summary = f"{target_kind} logical {target_logical_id}"
|
||
if target_id is not None and target_id != target_logical_id:
|
||
target_summary += f" (topology {target_id})"
|
||
summary_parts = [operation_name, target_summary]
|
||
if "distance" in parameters:
|
||
summary_parts.append(f"distance={_format_value(parameters['distance'])}")
|
||
if "semantic_distance" in parameters:
|
||
summary_parts.append(f"distance={_format_value(parameters['semantic_distance'])}")
|
||
if "new_diameter" in parameters:
|
||
summary_parts.append(f"diameter={_format_value(parameters['new_diameter'])}")
|
||
if parameters.get("target_length") is not None:
|
||
summary_parts.append(f"target_length={_format_value(parameters['target_length'])}")
|
||
if parameters.get("resize_strategy"):
|
||
summary_parts.append(f"strategy={parameters['resize_strategy']}")
|
||
actual_edge_length = _message_field(result_message, "nearest_length")
|
||
target_error = _message_field(result_message, "target_error")
|
||
if actual_edge_length is not None:
|
||
summary_parts.append(f"actual_length={actual_edge_length}")
|
||
if target_error is not None:
|
||
summary_parts.append(f"target_error={target_error}")
|
||
summary = " | ".join(summary_parts)
|
||
|
||
lines = [
|
||
f"operation: {operation_name}",
|
||
f"target: {target}",
|
||
f"target_kind: {target_kind or ''}",
|
||
f"target_id: {target_id if target_id is not None else ''}",
|
||
"parameters:",
|
||
]
|
||
if target_logical_id is not None:
|
||
lines.insert(4, f"target_logical_id: {target_logical_id}")
|
||
if target_id is not None and target_id != target_logical_id:
|
||
lines.insert(5, f"target_topological_id_at_operation: {target_id}")
|
||
if pick_position is not None:
|
||
insert_at = 6 if target_logical_id is not None and target_id is not None and target_id != target_logical_id else 5 if target_logical_id is not None else 4
|
||
lines.insert(insert_at, f"pick_position: {_format_value(pick_position)}")
|
||
for key, value in parameters.items():
|
||
lines.append(f" {key}: {_format_value(value)}")
|
||
if before_part_stats is not None and after_part_stats is not None:
|
||
lines.extend(
|
||
[
|
||
"target part topology before:",
|
||
f" solids: {before_part_stats.solids}",
|
||
f" faces: {before_part_stats.faces}",
|
||
f" edges: {before_part_stats.edges}",
|
||
"target part topology after:",
|
||
f" solids: {after_part_stats.solids} ({_signed_delta(after_part_stats.solids - before_part_stats.solids)})",
|
||
f" faces: {after_part_stats.faces} ({_signed_delta(after_part_stats.faces - before_part_stats.faces)})",
|
||
f" edges: {after_part_stats.edges} ({_signed_delta(after_part_stats.edges - before_part_stats.edges)})",
|
||
]
|
||
)
|
||
if quality_warnings:
|
||
lines.append("quality warnings:")
|
||
for warning in quality_warnings:
|
||
lines.append(f" {warning}")
|
||
lines.extend(
|
||
[
|
||
"topology before:",
|
||
f" solids: {before_stats.solids}",
|
||
f" faces: {before_stats.faces}",
|
||
f" edges: {before_stats.edges}",
|
||
"topology after:",
|
||
f" solids: {after_stats.solids} ({_signed_delta(after_stats.solids - before_stats.solids)})",
|
||
f" faces: {after_stats.faces} ({_signed_delta(after_stats.faces - before_stats.faces)})",
|
||
f" edges: {after_stats.edges} ({_signed_delta(after_stats.edges - before_stats.edges)})",
|
||
"geometry before:",
|
||
f" volume: {_format_value(before_geometry.get('volume', ''))}",
|
||
f" surface_area: {_format_value(before_geometry.get('surface_area', ''))}",
|
||
f" bbox_size: {_format_value(before_geometry.get('bbox_size', ''))}",
|
||
f" bbox_diagonal: {_format_value(before_geometry.get('bbox_diagonal', ''))}",
|
||
"geometry after:",
|
||
f" volume: {_format_after_delta(before_geometry, after_geometry, 'volume')}",
|
||
f" surface_area: {_format_after_delta(before_geometry, after_geometry, 'surface_area')}",
|
||
f" bbox_size: {_format_after_delta(before_geometry, after_geometry, 'bbox_size')}",
|
||
f" bbox_diagonal: {_format_after_delta(before_geometry, after_geometry, 'bbox_diagonal')}",
|
||
"result:",
|
||
f" {result_message}",
|
||
]
|
||
)
|
||
return OperationRecord(
|
||
summary=summary,
|
||
detail="\n".join(lines),
|
||
operation_name=operation_name,
|
||
target=target,
|
||
parameters=dict(parameters),
|
||
result_message=result_message,
|
||
target_kind=target_kind,
|
||
target_id=target_id,
|
||
target_logical_id=target_logical_id,
|
||
pick_position=pick_position,
|
||
before_snapshot=before_snapshot,
|
||
after_snapshot=after_snapshot,
|
||
)
|
||
|