2274 lines
107 KiB
Python
2274 lines
107 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
|
|||
|
|
|
|||
|
|
|
|||
|
|
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 或 Feature 模式并选择一个 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, "未选择特征", "请先切换到 Feature 模式并选择一个局部特征。")
|
|||
|
|
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 {part_id}",
|
|||
|
|
parameters={
|
|||
|
|
"scope": "part",
|
|||
|
|
"part_id": part_id,
|
|||
|
|
"repair_strategy": "ShapeFix + UnifySameDomain",
|
|||
|
|
},
|
|||
|
|
target_kind="part",
|
|||
|
|
target_id=part_id,
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
QMessageBox.information(self, "未选择对象", "请先选择 part、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。")
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
distance = float(self.offset_input.text())
|
|||
|
|
except ValueError:
|
|||
|
|
QMessageBox.critical(self, "距离无效", "请输入数字形式的面偏移距离。")
|
|||
|
|
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_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
|
|||
|
|
info = self.model.face_info(self.selected_face_id)
|
|||
|
|
if "diameter" not in info:
|
|||
|
|
QMessageBox.information(self, "不是圆柱面", "当前选中的 face 不是圆柱面,不能调整圆柱孔径。")
|
|||
|
|
return
|
|||
|
|
plan = self.model.cylindrical_resize_plan(self.selected_face_id, diameter)
|
|||
|
|
if plan["status"] == "blocked":
|
|||
|
|
QMessageBox.information(self, "不能调整孔径", str(plan["message"]))
|
|||
|
|
self.statusBar().showMessage("圆柱孔径调整已阻止")
|
|||
|
|
return
|
|||
|
|
guess = str(info.get("feature_guess", "cylindrical face"))
|
|||
|
|
if plan["risk"] != "low":
|
|||
|
|
target_to_height_ratio = plan.get("target_to_height_ratio", "")
|
|||
|
|
target_to_height_line = (
|
|||
|
|
""
|
|||
|
|
if target_to_height_ratio is None or target_to_height_ratio == ""
|
|||
|
|
else f"目标直径/估算高度: {_format_value(target_to_height_ratio)}\n"
|
|||
|
|
)
|
|||
|
|
warnings = str(plan.get("warnings", ""))
|
|||
|
|
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
|||
|
|
result = QMessageBox.question(
|
|||
|
|
self,
|
|||
|
|
"确认圆柱孔径调整",
|
|||
|
|
(
|
|||
|
|
f"face: {plan['face_id']}\n"
|
|||
|
|
f"当前直径: {_format_value(plan['current_diameter'])}\n"
|
|||
|
|
f"目标直径: {_format_value(plan['target_diameter'])}\n"
|
|||
|
|
f"直径变化量: {_format_value(plan['delta_diameter'])}\n"
|
|||
|
|
f"直径变化比例: {_format_percent(plan['diameter_delta_ratio'])}\n"
|
|||
|
|
f"{target_to_height_line}"
|
|||
|
|
f"调整模式: {plan['resize_mode']}\n"
|
|||
|
|
f"候选判断: {plan['feature_guess']}\n"
|
|||
|
|
f"置信度: {plan['confidence']}\n"
|
|||
|
|
f"材料投票: {plan['material_vote_summary']}\n"
|
|||
|
|
f"端部类型: {plan['cylinder_end_type']}\n"
|
|||
|
|
f"深度估算: {_format_value(plan['hole_depth_estimate'])}\n"
|
|||
|
|
f"疑似底面 Face: {_format_value(plan.get('feature_bottom_face_ids', ''))}\n"
|
|||
|
|
f"开口端相邻 Face: {_format_value(plan.get('feature_opening_face_ids', ''))}\n"
|
|||
|
|
f"风险: {plan['risk']}\n\n"
|
|||
|
|
f"Cutter: {plan['cutter_strategy']}\n"
|
|||
|
|
f"Cutter 高度: {_format_value(plan['cutter_height'])}\n"
|
|||
|
|
f"Cutter 起点/终点余量: {_format_value(plan['cutter_start_margin'])} / "
|
|||
|
|
f"{_format_value(plan['cutter_end_margin'])}\n\n"
|
|||
|
|
f"底面保护: {_format_value(plan.get('cutter_bottom_protection', ''))}\n"
|
|||
|
|
f"{_format_value(plan.get('cutter_bottom_note', ''))}\n\n"
|
|||
|
|
f"补料策略: {plan.get('fill_strategy', '')}\n\n"
|
|||
|
|
f"{warnings_line}"
|
|||
|
|
f"{plan['message']}\n\n"
|
|||
|
|
"继续操作会对当前零件执行实验性布尔修改。\n\n"
|
|||
|
|
"确定继续吗?"
|
|||
|
|
),
|
|||
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|||
|
|
QMessageBox.StandardButton.No,
|
|||
|
|
)
|
|||
|
|
if result != QMessageBox.StandardButton.Yes:
|
|||
|
|
self.statusBar().showMessage("已取消圆柱孔径调整")
|
|||
|
|
return
|
|||
|
|
face_id = self.selected_face_id
|
|||
|
|
self._show_cylinder_resize_preview(face_id, diameter)
|
|||
|
|
|
|||
|
|
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": 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_bottom_face_ids": plan.get("feature_bottom_face_ids"),
|
|||
|
|
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
|
|||
|
|
"feature_bottom_note": plan.get("feature_bottom_note"),
|
|||
|
|
"feature_guess": guess,
|
|||
|
|
"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"),
|
|||
|
|
"start_end_state": plan.get("start_end_state"),
|
|||
|
|
"end_end_state": plan.get("end_end_state"),
|
|||
|
|
"cutter_strategy": plan.get("cutter_strategy"),
|
|||
|
|
"cutter_height": plan.get("cutter_height"),
|
|||
|
|
"cutter_margin": plan.get("cutter_margin"),
|
|||
|
|
"cutter_start_margin": plan.get("cutter_start_margin"),
|
|||
|
|
"cutter_end_margin": plan.get("cutter_end_margin"),
|
|||
|
|
"cutter_bottom_protection": plan.get("cutter_bottom_protection"),
|
|||
|
|
"cutter_protected_bottom_face_ids": plan.get("cutter_protected_bottom_face_ids"),
|
|||
|
|
"cutter_opening_face_ids": plan.get("cutter_opening_face_ids"),
|
|||
|
|
"cutter_bottom_note": plan.get("cutter_bottom_note"),
|
|||
|
|
"cutter_note": plan.get("cutter_note"),
|
|||
|
|
"fill_strategy": plan.get("fill_strategy"),
|
|||
|
|
"fill_height": plan.get("fill_height"),
|
|||
|
|
"fill_radius": plan.get("fill_radius"),
|
|||
|
|
"fill_radius_overlap": plan.get("fill_radius_overlap"),
|
|||
|
|
"fill_note": plan.get("fill_note"),
|
|||
|
|
},
|
|||
|
|
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.model.feature_info(face_id)
|
|||
|
|
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.model.cylindrical_resize_plan(face_id, target_diameter)
|
|||
|
|
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: {face_id}\n"
|
|||
|
|
f"当前槽宽估算: {_format_value(current_width)}\n"
|
|||
|
|
f"目标槽宽: {_format_value(target_width)}\n"
|
|||
|
|
f"槽圆弧角度: {_format_value(angular_span)}\n"
|
|||
|
|
f"换算目标圆柱直径: {_format_value(target_diameter)}\n"
|
|||
|
|
f"风险: {plan['risk']}\n\n"
|
|||
|
|
f"{warnings_line}"
|
|||
|
|
f"{plan['message']}\n\n"
|
|||
|
|
"第一版会把槽宽换算成圆柱直径后执行孔/槽重建,不是完整 CAD 槽参数编辑。确定继续吗?"
|
|||
|
|
),
|
|||
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|||
|
|
QMessageBox.StandardButton.No,
|
|||
|
|
)
|
|||
|
|
if result != QMessageBox.StandardButton.Yes:
|
|||
|
|
self.statusBar().showMessage("已取消槽/半孔宽度调整")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self._show_cylinder_resize_preview(face_id, target_diameter)
|
|||
|
|
|
|||
|
|
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": info.get("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"),
|
|||
|
|
"cutter_strategy": plan.get("cutter_strategy"),
|
|||
|
|
"cutter_height": plan.get("cutter_height"),
|
|||
|
|
"fill_strategy": plan.get("fill_strategy"),
|
|||
|
|
},
|
|||
|
|
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"
|
|||
|
|
"继续操作会对当前零件执行实验性布尔修改。\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"
|
|||
|
|
"继续操作会用补料体封堵当前孔,并对当前零件执行实验性 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 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
|
|||
|
|
|
|||
|
|
info = self.model.face_info(self.selected_face_id)
|
|||
|
|
if info.get("surface") != "cylinder":
|
|||
|
|
QMessageBox.information(self, "不是圆柱孔/槽", "当前选中的 face 不是可调整孔深的圆柱孔/槽。")
|
|||
|
|
return
|
|||
|
|
plan = self.model.cylindrical_depth_plan(
|
|||
|
|
self.selected_face_id,
|
|||
|
|
target_depth,
|
|||
|
|
bottom_face_id=bottom_face_id,
|
|||
|
|
)
|
|||
|
|
if plan["status"] == "blocked":
|
|||
|
|
QMessageBox.information(self, "不能调整孔深", str(plan["message"]))
|
|||
|
|
self.statusBar().showMessage("盲孔深度调整已阻止")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if plan["risk"] != "low":
|
|||
|
|
warnings = str(plan.get("warnings", ""))
|
|||
|
|
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
|||
|
|
result = QMessageBox.question(
|
|||
|
|
self,
|
|||
|
|
"确认盲孔深度调整",
|
|||
|
|
(
|
|||
|
|
f"face: {plan['face_id']}\n"
|
|||
|
|
f"当前深度: {_format_value(plan['current_depth'])}\n"
|
|||
|
|
f"目标深度: {_format_value(plan['target_depth'])}\n"
|
|||
|
|
f"深度变化量: {_format_value(plan['delta_depth'])}\n"
|
|||
|
|
f"深度变化比例: {_format_percent(plan['depth_delta_ratio'])}\n"
|
|||
|
|
f"调整模式: {plan['depth_mode']}\n"
|
|||
|
|
f"候选判断: {plan['feature_guess']}\n"
|
|||
|
|
f"置信度: {plan['confidence']}\n"
|
|||
|
|
f"材料投票: {plan['material_vote_summary']}\n"
|
|||
|
|
f"端部类型: {plan['cylinder_end_type']}\n"
|
|||
|
|
f"疑似底面 Face: {_format_value(plan.get('feature_bottom_face_ids', ''))}\n"
|
|||
|
|
f"手动底面 Face: {_format_value(plan.get('manual_bottom_face_id', ''))}\n"
|
|||
|
|
f"底面识别来源: {_format_value(plan.get('feature_bottom_detection', ''))}\n"
|
|||
|
|
f"当前深度来源: {_format_value(plan.get('depth_current_depth_source', ''))}\n"
|
|||
|
|
f"开口方向来源: {_format_value(plan.get('depth_open_direction_source', ''))}\n"
|
|||
|
|
f"开口端相邻 Face: {_format_value(plan.get('feature_opening_face_ids', ''))}\n"
|
|||
|
|
f"风险: {plan['risk']}\n\n"
|
|||
|
|
f"工具策略: {plan['depth_tool_strategy']}\n"
|
|||
|
|
f"工具类型: {plan['depth_tool_role']}\n"
|
|||
|
|
f"工具高度: {_format_value(plan['depth_tool_height'])}\n"
|
|||
|
|
f"工具半径: {_format_value(plan['depth_tool_radius'])}\n\n"
|
|||
|
|
f"{warnings_line}"
|
|||
|
|
f"{plan['message']}\n\n"
|
|||
|
|
"继续操作会对当前零件执行实验性布尔修改。\n\n"
|
|||
|
|
"确定继续吗?"
|
|||
|
|
),
|
|||
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|||
|
|
QMessageBox.StandardButton.No,
|
|||
|
|
)
|
|||
|
|
if result != QMessageBox.StandardButton.Yes:
|
|||
|
|
self.statusBar().showMessage("已取消盲孔深度调整")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
face_id = self.selected_face_id
|
|||
|
|
self._show_cylinder_depth_preview(face_id, target_depth, bottom_face_id=bottom_face_id)
|
|||
|
|
|
|||
|
|
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"),
|
|||
|
|
"depth_tool_strategy": plan.get("depth_tool_strategy"),
|
|||
|
|
"depth_tool_role": plan.get("depth_tool_role"),
|
|||
|
|
"depth_tool_height": plan.get("depth_tool_height"),
|
|||
|
|
"depth_tool_radius": plan.get("depth_tool_radius"),
|
|||
|
|
"depth_tool_radius_overlap": plan.get("depth_tool_radius_overlap"),
|
|||
|
|
"depth_current_depth": plan.get("depth_current_depth"),
|
|||
|
|
"depth_current_depth_source": plan.get("depth_current_depth_source"),
|
|||
|
|
"depth_bottom_parameter_source": plan.get("depth_bottom_parameter_source"),
|
|||
|
|
"depth_open_direction_source": plan.get("depth_open_direction_source"),
|
|||
|
|
"depth_open_point": plan.get("depth_open_point"),
|
|||
|
|
"depth_current_bottom_point": plan.get("depth_current_bottom_point"),
|
|||
|
|
"depth_target_bottom_point": plan.get("depth_target_bottom_point"),
|
|||
|
|
},
|
|||
|
|
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。")
|
|||
|
|
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,
|
|||
|
|
"确认给边添加圆角",
|
|||
|
|
(
|
|||
|
|
f"edge: {plan['edge_id']}\n"
|
|||
|
|
f"边长: {_format_value(plan['edge_length'])}\n"
|
|||
|
|
f"目标圆角半径: {_format_value(plan['target_radius'])}\n"
|
|||
|
|
f"半径/边长比例: {_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="给边添加圆角",
|
|||
|
|
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。")
|
|||
|
|
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,
|
|||
|
|
"确认给边添加倒角",
|
|||
|
|
(
|
|||
|
|
f"edge: {plan['edge_id']}\n"
|
|||
|
|
f"边长: {_format_value(plan['edge_length'])}\n"
|
|||
|
|
f"目标倒角距离: {_format_value(plan['target_distance'])}\n"
|
|||
|
|
f"倒角距离/边长比例: {_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="给边添加倒角",
|
|||
|
|
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("请等待当前编辑完成后再直接修改边长。"):
|
|||
|
|
return
|
|||
|
|
if self.selected_edge_id is None:
|
|||
|
|
QMessageBox.information(self, "未选择边", "请先选择一条 edge。")
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
target_length = float(self.edge_target_length_input.text())
|
|||
|
|
except ValueError:
|
|||
|
|
QMessageBox.critical(self, "目标长度无效", "请输入数字形式的目标边长。")
|
|||
|
|
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, "不能直接修改边长", 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"edge: {plan['edge_id']}\n"
|
|||
|
|
f"当前边长: {_format_value(plan['current_length'])}\n"
|
|||
|
|
f"目标边长: {_format_value(plan['target_length'])}\n"
|
|||
|
|
f"变化量: {_format_value(plan['delta_length'])}\n"
|
|||
|
|
f"边长基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
|||
|
|
f"策略: {_format_value(plan.get('resize_strategy', ''))}\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"圆边模式: {_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_target_kind', ''))}\n"
|
|||
|
|
f"风险: {plan['risk']}\n\n"
|
|||
|
|
f"{warnings_line}"
|
|||
|
|
f"{plan['message']}\n\n"
|
|||
|
|
"当前会优先通过端面推拉或相邻圆柱直径编辑改变边长;没有明确局部路径时会尝试几何缩放 fallback。"
|
|||
|
|
"这不是通用参数化边长编辑。\n\n"
|
|||
|
|
"确定继续吗?"
|
|||
|
|
),
|
|||
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|||
|
|
QMessageBox.StandardButton.No,
|
|||
|
|
)
|
|||
|
|
if result != QMessageBox.StandardButton.Yes:
|
|||
|
|
self.statusBar().showMessage("已取消边长直接修改")
|
|||
|
|
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="直接修改边长",
|
|||
|
|
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"),
|
|||
|
|
"end_face_id": plan.get("end_face_id"),
|
|||
|
|
"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_transform_kind": plan.get("affine_transform_kind"),
|
|||
|
|
"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"),
|
|||
|
|
"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("请等待当前编辑完成后再直接修改边长。"):
|
|||
|
|
return
|
|||
|
|
if self.selected_edge_id is None:
|
|||
|
|
QMessageBox.information(self, "未选择边", "请先选择一条 edge。")
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
target_length = float(self.edge_target_length_input.text())
|
|||
|
|
except ValueError:
|
|||
|
|
QMessageBox.critical(self, "目标长度无效", "请输入数字形式的目标边长。")
|
|||
|
|
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, "不能修改边长", str(plan["message"]))
|
|||
|
|
self.statusBar().showMessage("边长直接修改已阻止")
|
|||
|
|
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,
|
|||
|
|
"确认直接修改边长",
|
|||
|
|
(
|
|||
|
|
f"edge: {plan['edge_id']}\n"
|
|||
|
|
f"曲线类型: {_format_value(plan.get('curve', ''))}\n"
|
|||
|
|
f"当前边长: {_format_value(plan['current_length'])}\n"
|
|||
|
|
f"目标边长: {_format_value(plan['target_length'])}\n"
|
|||
|
|
f"变化量: {_format_value(plan['delta_length'])}\n"
|
|||
|
|
f"边长基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
|||
|
|
f"策略: {_format_value(strategy)}\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"圆边模式: {_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_kind', ''))}\n"
|
|||
|
|
f"缩放目标: {_format_value(plan.get('affine_target_kind', ''))}\n"
|
|||
|
|
f"缩放比例: {_format_value(plan.get('affine_scale', ''))}\n"
|
|||
|
|
f"轴线来源: {_format_value(plan.get('affine_axis_source', ''))}\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("已取消边长直接修改")
|
|||
|
|
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="直接修改边长",
|
|||
|
|
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"),
|
|||
|
|
"end_face_id": plan.get("end_face_id"),
|
|||
|
|
"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_transform_kind": plan.get("affine_transform_kind"),
|
|||
|
|
"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"),
|
|||
|
|
"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, "未选择零件", "请先选择一个 part,或选择属于某个 part 的对象。")
|
|||
|
|
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 {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"part: {_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, "未选择零件", "请先选择一个 part,或选择属于某个 part 的对象。")
|
|||
|
|
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 {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"part: {_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, finished_callback, failed_callback) -> 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._clear_hover(render=True)
|
|||
|
|
self._update_action_states()
|
|||
|
|
thread = QThread(self)
|
|||
|
|
worker = ScanWorker(action)
|
|||
|
|
worker.moveToThread(thread)
|
|||
|
|
thread.started.connect(worker.run)
|
|||
|
|
worker.finished.connect(finished_callback)
|
|||
|
|
worker.failed.connect(failed_callback)
|
|||
|
|
worker.finished.connect(thread.quit)
|
|||
|
|
worker.failed.connect(thread.quit)
|
|||
|
|
thread.finished.connect(worker.deleteLater)
|
|||
|
|
thread.finished.connect(thread.deleteLater)
|
|||
|
|
thread.finished.connect(self._forget_scan_thread)
|
|||
|
|
self.scan_thread = thread
|
|||
|
|
self.scan_worker = worker
|
|||
|
|
thread.start()
|
|||
|
|
|
|||
|
|
def _end_scan_task(self) -> None:
|
|||
|
|
self.scan_in_progress = False
|
|||
|
|
self.pending_scan_kind = None
|
|||
|
|
self._update_action_states()
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
lambda result: self._finish_editable_scan(result, show_info, scan_label, limit),
|
|||
|
|
self._fail_editable_scan,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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:
|
|||
|
|
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"part={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,
|
|||
|
|
lambda result: self._finish_cylinder_scan(result, show_info),
|
|||
|
|
self._fail_cylinder_scan,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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 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:
|
|||
|
|
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']}: part {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:
|
|||
|
|
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)
|
|||
|
|
worker.failed.connect(self._fail_edit_action)
|
|||
|
|
worker.finished.connect(thread.quit)
|
|||
|
|
worker.failed.connect(thread.quit)
|
|||
|
|
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
|
|||
|
|
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,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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:
|
|||
|
|
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:
|
|||
|
|
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:
|
|||
|
|
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._reset_selection()
|
|||
|
|
self._populate_part_tree()
|
|||
|
|
self._rebuild_scene_from_polydata(model_polydata, edge_polydata, reset_camera=False)
|
|||
|
|
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:
|
|||
|
|
self.statusBar().showMessage(message)
|
|||
|
|
self.set_plain_info(record.detail)
|
|||
|
|
|
|||
|
|
@Slot(str)
|
|||
|
|
def _fail_edit_action(self, message: str) -> None:
|
|||
|
|
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()
|
|||
|
|
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'])}")
|
|||
|
|
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),
|
|||
|
|
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,
|
|||
|
|
)
|
|||
|
|
|