Files
pythonocc-step-editor/step_editor/window_actions.py
T

6547 lines
317 KiB
Python
Raw Normal View History

from __future__ import annotations
from datetime import datetime
import math
from pathlib import Path
import vtk
from PySide6.QtCore import Qt, QThread, Slot
from PySide6.QtWidgets import (
QApplication,
QFileDialog,
QMessageBox,
QTableWidgetItem,
QTreeWidgetItem,
)
from .model import StepModel
from .records import OperationRecord
from .ui_helpers import * # noqa: F403
from .workers import EditWorker, LoadWorker, ScanWorker
def _message_field(message: str, key: str) -> str | None:
marker = f"{key}="
start = message.find(marker)
if start < 0:
return None
start += len(marker)
end = message.find(",", start)
if end < 0:
end = len(message)
value = message[start:end].strip().rstrip(".")
return value or None
def _triple_or_none(value: object) -> tuple[float, float, float] | None:
if isinstance(value, str):
text = value.strip().strip("()[]")
chunks = [chunk.strip() for chunk in text.replace(";", ",").split(",") if chunk.strip()]
elif isinstance(value, (list, tuple)):
chunks = list(value)
else:
return None
if len(chunks) != 3:
return None
try:
return (float(chunks[0]), float(chunks[1]), float(chunks[2]))
except (TypeError, ValueError):
return None
def _unit_triple_or_none(value: object) -> tuple[float, float, float] | None:
triple = _triple_or_none(value)
if triple is None:
return None
length = math.sqrt(triple[0] * triple[0] + triple[1] * triple[1] + triple[2] * triple[2])
if length <= 1e-12:
return None
return (triple[0] / length, triple[1] / length, triple[2] / length)
class WindowActionMixin:
def export_all(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再导出。"):
return
if not self._confirm_export_quality("all"):
return
target, _ = QFileDialog.getSaveFileName(
self,
"导出当前完整 STEP",
str(self.step_path.parent / f"{self.step_path.stem}_edited.step"),
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
)
if not target:
return
self._run_action(lambda: self.model.export_all(target), f"已导出 {Path(target).name}")
def export_selected_part(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再导出。"):
return
if self.selected_part_id is None:
QMessageBox.information(self, "未选择零件", "请先选择一个零件。")
return
if not self._confirm_export_quality("part", self.selected_part_id):
return
part = self.model.part_by_id(self.selected_part_id)
default_name = f"{part.name if part else 'part'}_export.step".replace(" ", "_")
target, _ = QFileDialog.getSaveFileName(
self,
"导出选中零件",
str(self.step_path.parent / default_name),
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
)
if not target:
return
self._run_action(
lambda: self.model.export_part(self.selected_part_id, target),
f"已导出选中零件到 {Path(target).name}",
)
def export_selected_solid(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再导出。"):
return
if self.selected_solid_id is None:
QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择一个属于 Solid 的 Face。")
return
if not self._confirm_export_quality("solid", self.selected_solid_id):
return
target, _ = QFileDialog.getSaveFileName(
self,
"导出选中 Solid",
str(self.step_path.parent / f"solid_{self.selected_solid_id}_export.step"),
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
)
if not target:
return
self._run_action(
lambda: self.model.export_solid(self.selected_solid_id, target),
f"已导出选中 Solid 到 {Path(target).name}",
)
def export_selected_face(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再导出。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择 Face", "请先切换到 Face 或特征模式并选择一个 Face。")
return
if not self._confirm_export_quality("face", self.selected_face_id):
return
target, _ = QFileDialog.getSaveFileName(
self,
"导出选中面区域",
str(self.step_path.parent / f"face_region_{self.selected_face_id}_export.step"),
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
)
if not target:
return
self._run_action(
lambda: self.model.export_face(self.selected_face_id, target),
f"已导出选中面区域到 {Path(target).name}",
)
def export_selected_feature(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再导出。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择特征", "请先切换到特征模式并选择一个特征。")
return
if not self._confirm_export_quality("feature", self.selected_face_id):
return
target, _ = QFileDialog.getSaveFileName(
self,
"导出选中特征区域",
str(self.step_path.parent / f"feature_face_{self.selected_face_id}_export.step"),
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
)
if not target:
return
self._run_action(
lambda: self.model.export_feature(self.selected_face_id, target),
f"已导出选中特征区域到 {Path(target).name}",
)
def export_selected_edge(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再导出。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择Edge", "请先切换到Edge模式并选择一个Edge。")
return
if not self._confirm_export_quality("edge", self.selected_edge_id):
return
target, _ = QFileDialog.getSaveFileName(
self,
"导出选中 Edge",
str(self.step_path.parent / f"edge_{self.selected_edge_id}_export.step"),
"STEP 文件 (*.step *.stp);;所有文件 (*.*)",
)
if not target:
return
self._run_action(
lambda: self.model.export_edge(self.selected_edge_id, target),
f"已导出选中 Edge 到 {Path(target).name}",
)
def check_export_quality(self) -> None:
if self.model is None:
return
if self._edit_busy("编辑计算中,暂时不能检查导出质量。"):
return
scope, target_id = self._current_export_quality_target()
try:
info = self.model.export_quality_info(scope, target_id)
except Exception as exc:
QMessageBox.critical(self, "质量检查失败", str(exc))
self.statusBar().showMessage("导出质量检查失败")
return
report = _export_quality_text(info)
self.set_plain_info(report)
if info.get("quality_status") == "ok":
self.statusBar().showMessage("导出质量检查通过")
else:
self.statusBar().showMessage("导出质量检查发现警告")
def _current_export_quality_target(self) -> tuple[str, int | None]:
if self.selected_kind == "feature" and self.selected_face_id is not None:
return "feature", self.selected_face_id
if self.selected_kind == "face" and self.selected_face_id is not None:
return "face", self.selected_face_id
if self.selected_kind == "edge" and self.selected_edge_id is not None:
return "edge", self.selected_edge_id
if self.selected_kind == "edge":
if self.selected_solid_id is not None:
return "solid", self.selected_solid_id
if self.selected_part_id is not None:
return "part", self.selected_part_id
if self.selected_kind == "solid" and self.selected_solid_id is not None:
return "solid", self.selected_solid_id
if self.selected_kind == "part" and self.selected_part_id is not None:
return "part", self.selected_part_id
return "all", None
def _confirm_export_quality(self, scope: str, target_id: int | None = None) -> bool:
if self.model is None:
return False
try:
info = self.model.export_quality_info(scope, target_id)
except Exception as exc:
QMessageBox.critical(self, "导出质量检查失败", str(exc))
self.statusBar().showMessage("导出质量检查失败")
return False
report = _export_quality_text(info)
self.set_plain_info(report)
if info.get("quality_status") == "ok":
return True
result = QMessageBox.question(
self,
"导出质量警告",
f"{report}\n\n仍然继续导出吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
return result == QMessageBox.StandardButton.Yes
def repair_model(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修复模型。"):
return
def action():
return self.model.repair_model()
self._run_edit_action(
action,
operation_name="修复当前模型",
target="当前完整模型",
parameters={"scope": "all", "repair_strategy": "ShapeFix + UnifySameDomain"},
target_kind=None,
target_id=None,
)
def repair_selected_shape(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修复选中对象。"):
return
if self.selected_solid_id is not None:
solid_id = self.selected_solid_id
def action():
return self.model.repair_solid(solid_id)
self._run_edit_action(
action,
operation_name="修复选中Solid",
target=f"Solid {solid_id}",
parameters={
"scope": "solid",
"solid_id": solid_id,
"part_id": self.selected_part_id,
"repair_strategy": "ShapeFix + UnifySameDomain",
},
target_kind="solid",
target_id=solid_id,
)
return
if self.selected_part_id is not None:
part_id = self.selected_part_id
def action():
return self.model.repair_part(part_id)
self._run_edit_action(
action,
operation_name="修复选中零件",
target=f"零件 {part_id}",
parameters={
"scope": "part",
"part_id": part_id,
"repair_strategy": "ShapeFix + UnifySameDomain",
},
target_kind="part",
target_id=part_id,
)
return
QMessageBox.information(self, "未选择对象", "请先选择零件、Solid、Face 或 Edge。")
self.statusBar().showMessage("未选择可修复对象")
def push_pull_face(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再执行新的推拉。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择 Face", "请先选择一个平面 Face。")
return
try:
distance = float(self.offset_input.text())
except ValueError:
QMessageBox.critical(self, "距离无效", "请输入数字形式的面移动距离。")
return
face_id = self.selected_face_id
2026-07-29 15:43:28 +08:00
plan = self._quick_push_pull_plan(face_id, distance)
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(distance)}\n"
f"当前法向位置: {_format_value(plan.get('current_plane_position'))}\n"
f"目标法向位置: {_format_value(plan.get('target_plane_position'))}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"影响范围: {_format_value(plan.get('edit_semantics'))}\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
2026-07-29 15:43:28 +08:00
self._show_push_pull_preview(face_id, distance, plan=plan)
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"),
2026-07-29 15:43:28 +08:00
"current_plane_position": plan.get("current_plane_position"),
"target_plane_position": plan.get("target_plane_position"),
"direction_confidence": plan.get("direction_confidence"),
"direction_note": plan.get("direction_note"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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,
)
2026-07-29 15:43:28 +08:00
def _quick_push_pull_plan(self, face_id: int, distance: float) -> dict[str, object]:
info = dict(getattr(self, "current_info_values", {}) or {})
surface = str(info.get("surface", ""))
if not surface and self.model is not None:
surface = self.model.face_surface_kind(face_id)
if surface != "plane":
return {
"status": "blocked",
"risk": "blocked",
"message": "当前选中的面不是平面,不能移动这个面。",
2026-07-29 15:43:28 +08:00
"face_id": face_id,
"distance": distance,
"surface": surface,
}
outward = info.get("push_pull_outward_direction")
if not isinstance(outward, (tuple, list)) or len(outward) != 3:
outward = info.get("oriented_normal") or info.get("normal")
if not isinstance(outward, (tuple, list)) or len(outward) != 3:
return {
"status": "blocked",
"risk": "blocked",
"message": "当前平面缺少稳定移动方向,请重新选择这个面后再试。",
2026-07-29 15:43:28 +08:00
"face_id": face_id,
"distance": distance,
"surface": surface,
}
bbox_diagonal = _float_or_none(info.get("bbox_diagonal"))
distance_abs = abs(float(distance))
risk = "low"
status = "ready"
warnings: list[str] = []
if distance_abs <= 1e-9:
return {
"status": "blocked",
"risk": "blocked",
"message": "面移动距离为 0,不需要修改。",
2026-07-29 15:43:28 +08:00
"face_id": face_id,
"distance": distance,
"surface": surface,
}
if bbox_diagonal is not None and bbox_diagonal > 0:
if distance_abs > bbox_diagonal * 0.2:
risk = "high"
status = "caution"
warnings.append("面移动距离相对当前面尺寸较大,布尔失败或大范围变形风险较高。")
2026-07-29 15:43:28 +08:00
elif distance_abs > bbox_diagonal * 0.08:
risk = "medium"
status = "caution"
warnings.append("面移动距离相对当前面尺寸偏大,请确认预览范围。")
2026-07-29 15:43:28 +08:00
scope_face_ids = (
_int_values(info.get("push_pull_scope_face_ids"))
or _int_values(info.get("feature_highlight_face_ids"))
or _int_values(info.get("same_domain_face_ids"))
or [face_id]
)
plane_origin = info.get("plane_origin")
current_plane_position = None
if isinstance(plane_origin, (tuple, list)) and len(plane_origin) == 3:
try:
current_plane_position = sum(float(plane_origin[index]) * float(outward[index]) for index in range(3))
except (TypeError, ValueError):
current_plane_position = None
target_plane_position = (
current_plane_position + float(distance)
if current_plane_position is not None
else None
)
part_id = info.get("part_id")
solid_id = info.get("solid_id")
if part_id is None and self.model is not None and 0 <= face_id < len(self.model.face_part_ids):
part_id = self.model.face_part_ids[face_id]
if solid_id is None and self.model is not None and 0 <= face_id < len(self.model.face_solid_ids):
solid_id = self.model.face_solid_ids[face_id]
message = " ".join(warnings) if warnings else "可以尝试偏移该平面;完整几何检查会在后台执行。"
return {
"status": status,
"risk": risk,
"message": message,
"warnings": "".join(warnings),
"blockers": "",
"face_id": face_id,
"part_id": part_id,
"solid_id": solid_id,
"distance": distance,
"surface": surface,
"area": info.get("area"),
"bbox_diagonal": bbox_diagonal,
"outward_direction": tuple(float(item) for item in outward),
"current_plane_position": current_plane_position,
"target_plane_position": target_plane_position,
"resize_strategy": "push-pull-planar-face-region",
"edit_strategy_label": "沿面法向推拉当前平面区域",
"edit_semantics": (
"沿当前 Face 的法向移动平面区域;正向通常表示向外加料,负向表示向内切削。"
"相邻侧面会跟随重建,但这不是平移整个所属对象,也不是只改面积。"
),
2026-07-29 15:43:28 +08:00
"direction_confidence": info.get("push_pull_confidence", "cached"),
"direction_note": info.get("push_pull_note", "使用当前选中 Face 的缓存方向,完整检查在后台执行。"),
"push_pull_scope_face_ids": tuple(scope_face_ids),
"push_pull_scope_face_count": len(scope_face_ids),
"push_pull_scope_note": "使用当前显示/选中缓存生成快速预览;后台会重新计算真实推拉区域。",
}
def resize_shell_thickness(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再调整薄壁厚度。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择平面", "请先选择一个已识别到相对面的薄壁/壳体平面候选。")
return
if not hasattr(self, "shell_thickness_input"):
return
try:
target_thickness = float(self.shell_thickness_input.text())
except ValueError:
QMessageBox.critical(self, "目标厚度无效", "请输入数字形式的目标薄壁厚度。")
return
face_id = self.selected_face_id
plan = self.model.shell_thickness_plan(face_id, target_thickness)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能调整薄壁厚度", str(plan["message"]))
self.statusBar().showMessage("薄壁厚度调整已阻止")
return
if plan["risk"] != "low":
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
"确认调整薄壁厚度",
(
f"face: {plan['face_id']}\n"
f"当前厚度估算: {_format_value(plan['shell_current_thickness'])}\n"
f"目标厚度: {_format_value(plan['shell_target_thickness'])}\n"
f"厚度变化量: {_format_value(plan['shell_delta_thickness'])}\n"
f"厚度变化比例: {_format_percent(plan['shell_delta_ratio'])}\n"
f"相对平面 Face: {_format_value(plan.get('shell_opposite_face_id', ''))}\n"
f"相对平面重叠率: {_format_percent(plan.get('shell_overlap_ratio_estimate'))}\n"
f"置信度: {_format_value(plan.get('shell_confidence', ''))}\n"
f"当前面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"当前版本会移动当前平面区域,让它和相对平面的距离接近目标厚度;这不是 CAD 壳命令参数编辑。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消薄壁厚度调整")
return
self._show_shell_thickness_preview(face_id, target_thickness)
def action():
return self.model.resize_shell_thickness(face_id, target_thickness)
self._run_edit_action(
action,
operation_name="调整薄壁厚度",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"shell_region_kind": plan.get("shell_region_kind"),
"shell_confidence": plan.get("shell_confidence"),
"shell_source_face_ids": plan.get("shell_source_face_ids"),
"shell_opposite_face_id": plan.get("shell_opposite_face_id"),
"shell_current_thickness": plan.get("shell_current_thickness"),
"shell_target_thickness": plan.get("shell_target_thickness"),
"shell_delta_thickness": plan.get("shell_delta_thickness"),
"shell_delta_ratio": plan.get("shell_delta_ratio"),
"shell_signed_thickness": plan.get("shell_signed_thickness"),
"shell_overlap_ratio_estimate": plan.get("shell_overlap_ratio_estimate"),
"shell_opposite_normal_dot": plan.get("shell_opposite_normal_dot"),
"shell_desired_movement_vector": plan.get("shell_desired_movement_vector"),
"shell_movement_alignment": plan.get("shell_movement_alignment"),
"push_pull_distance": plan.get("push_pull_distance"),
"outward_direction": plan.get("outward_direction"),
"push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"),
"push_pull_scope_face_count": plan.get("push_pull_scope_face_count"),
"push_pull_scope_note": plan.get("push_pull_scope_note"),
"resize_strategy": plan.get("resize_strategy"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
},
target_kind="feature" if self.selected_kind == "feature" else "face",
target_id=face_id,
)
def resize_shell_thickness_owning_scale(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再执行薄壁厚度(整体)。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择平面", "请先选择一个已识别到相对面的薄壁/壳体平面候选。")
return
if not hasattr(self, "shell_thickness_input"):
return
try:
target_thickness = float(self.shell_thickness_input.text())
except ValueError:
QMessageBox.critical(self, "目标厚度无效", "请输入数字形式的目标薄壁厚度。")
return
if target_thickness <= 0:
QMessageBox.critical(self, "目标厚度无效", "目标薄壁厚度必须大于 0。")
return
face_id = self.selected_face_id
plan = self.model.shell_thickness_owning_scale_plan(face_id, target_thickness)
lines = [
f"Face: {face_id}",
f"当前厚度: {_format_value(plan.get('shell_current_thickness'))}",
f"目标厚度: {_format_value(plan.get('shell_target_thickness'))}",
f"厚度变化量: {_format_value(plan.get('shell_delta_thickness'))}",
f"厚度变化比例: {_format_percent(plan.get('shell_delta_ratio'))}",
f"缩放比例: {_format_value(plan.get('affine_scale'))}",
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
f"厚度方向: {_format_value(plan.get('affine_axis_direction'))}",
f"相对平面 Face: {_format_value(plan.get('shell_opposite_face_id', ''))}",
"当前语义: 沿薄壁/壳体厚度方向整体缩放所属特征或 Solid;不是推拉当前平面区域。",
]
if not self._confirm_quick_edit_plan(
"薄壁厚度(整体)缩放所属对象",
plan,
lines,
"薄壁厚度(整体)已阻止",
"已取消薄壁厚度(整体)",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_shell_thickness_owning_scale(face_id, target_thickness)
self._run_edit_action(
action,
operation_name="薄壁厚度(整体)缩放所属对象",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface", "plane"),
"shell_region_kind": plan.get("shell_region_kind"),
"shell_confidence": plan.get("shell_confidence"),
"shell_source_face_ids": plan.get("shell_source_face_ids"),
"shell_opposite_face_id": plan.get("shell_opposite_face_id"),
"shell_current_thickness": plan.get("shell_current_thickness"),
"shell_target_thickness": plan.get("shell_target_thickness"),
"shell_delta_thickness": plan.get("shell_delta_thickness"),
"shell_delta_ratio": plan.get("shell_delta_ratio"),
"shell_signed_thickness": plan.get("shell_signed_thickness"),
"shell_overlap_ratio_estimate": plan.get("shell_overlap_ratio_estimate"),
"shell_opposite_normal_dot": plan.get("shell_opposite_normal_dot"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"part_solid_count": plan.get("part_solid_count"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
@staticmethod
def _max_quick_risk(current: str, candidate: str) -> str:
order = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
return current if order.get(current, 0) >= order.get(candidate, 0) else candidate
def _selected_face_info_snapshot(self) -> dict[str, object]:
info = dict(getattr(self, "current_info_values", {}) or {})
if self.model is None or self.selected_face_id is None:
return info
selected_face_id = int(self.selected_face_id)
possible_ids = (
info.get("face_id"),
info.get("topological_face_id"),
info.get("feature_source_face_id"),
info.get("face_region_source_face_id"),
)
has_selected_id = False
for value in possible_ids:
try:
if value is not None and int(value) == selected_face_id:
has_selected_id = True
break
except (TypeError, ValueError):
pass
has_any_face_id = any(value is not None for value in possible_ids)
if info and has_any_face_id and not has_selected_id:
info = {}
if info and str(info.get("kind", "")) in {"edge", "part", "solid"} and not has_selected_id:
info = {}
if "surface" not in info and "diameter" not in info:
try:
info = dict(self.model.face_info(selected_face_id))
except Exception:
pass
return info
def _quick_cylinder_resize_plan(
self,
face_id: int,
target_diameter: float,
info: dict[str, object] | None = None,
) -> dict[str, object]:
info = dict(info or self._selected_face_info_snapshot())
warnings: list[str] = []
blockers: list[str] = []
risk = "low"
status = "ready"
surface = str(info.get("surface", ""))
current_diameter = _float_or_none(info.get("diameter"))
height_estimate = _float_or_none(info.get("height_estimate"))
if height_estimate is None:
height_estimate = _float_or_none(info.get("same_domain_height_estimate"))
if height_estimate is None:
height_estimate = _float_or_none(info.get("hole_depth_estimate"))
guess = str(info.get("feature_guess", "cylindrical face"))
confidence = str(info.get("confidence", "low"))
angular_span = _float_or_none(info.get("angular_span"))
if surface != "cylinder" or current_diameter is None or current_diameter <= 1e-9:
blockers.append("当前选中对象不是可识别的圆柱面,不能调整孔/槽直径。")
if target_diameter <= 0:
blockers.append("目标直径必须大于 0。")
if guess == "round/fillet candidate":
risk = self._max_quick_risk(risk, "high")
warnings.append("当前圆柱面更像圆角/倒圆,调整孔径可能误切圆角。")
elif guess == "boss/outer-round candidate":
risk = self._max_quick_risk(risk, "high")
warnings.append("当前圆柱面更像凸台或外圆,调整孔径可能切掉外部结构。")
elif guess != "hole/groove candidate":
risk = self._max_quick_risk(risk, "high")
warnings.append("当前圆柱面还没有被稳定识别为孔/槽候选。")
if guess == "hole/groove candidate" and confidence == "low":
risk = self._max_quick_risk(risk, "medium")
warnings.append("孔/槽判断置信度较低。")
if (
guess == "hole/groove candidate"
and angular_span is not None
and angular_span < math.tau * 0.92
):
risk = self._max_quick_risk(risk, "medium")
warnings.append("这是局部圆柱面,更像槽或半孔,不是完整圆孔。")
delta_diameter = None
delta_ratio = None
target_to_height_ratio = None
resize_mode = "unknown"
if current_diameter is not None and current_diameter > 1e-9:
delta_diameter = target_diameter - current_diameter
delta_ratio = abs(delta_diameter) / current_diameter
resize_mode = "expand" if delta_diameter > 0 else "shrink"
if abs(delta_diameter) <= max(current_diameter * 1e-5, 1e-6):
blockers.append("目标直径与当前直径几乎相同,不需要修改。")
elif target_diameter > current_diameter * 2.0:
blockers.append(
"目标直径超过当前直径的 2 倍;当前版本会阻止这种极端放大,避免布尔计算长时间卡死。"
)
elif delta_ratio > 1.0:
risk = self._max_quick_risk(risk, "high")
warnings.append("目标直径变化超过当前直径的 100%,很可能导致大范围误切或布尔失败。")
elif delta_ratio > 0.35:
risk = self._max_quick_risk(risk, "medium")
warnings.append("目标直径变化超过当前直径的 35%。")
if target_diameter < current_diameter:
if guess != "hole/groove candidate":
blockers.append("缩小孔径当前版本只支持明确的孔/槽候选。")
else:
risk = self._max_quick_risk(risk, "high")
warnings.append("缩小孔径需要先补料再重切,属于高风险实验路径。")
if height_estimate is not None and height_estimate > 1e-9:
target_to_height_ratio = target_diameter / height_estimate
if target_diameter > height_estimate * 2.0:
blockers.append("目标直径超过圆柱面估算高度的 2 倍,当前版本暂不放行。")
elif target_diameter > height_estimate:
risk = self._max_quick_risk(risk, "medium")
warnings.append("目标直径大于圆柱面估算高度,请确认单位和目标。")
if blockers:
status = "blocked"
risk = "blocked"
elif risk in {"medium", "high"}:
status = "caution"
message = "".join(blockers + warnings) if blockers or warnings else "可以尝试调整孔/槽直径。"
return {
"status": status,
"risk": risk,
"message": message,
"warnings": "".join(warnings),
"blockers": "".join(blockers),
"face_id": face_id,
"part_id": info.get("part_id"),
"solid_id": info.get("solid_id"),
"current_diameter": current_diameter,
"target_diameter": target_diameter,
"delta_diameter": delta_diameter,
"diameter_delta_ratio": delta_ratio,
"target_to_height_ratio": target_to_height_ratio,
"resize_mode": resize_mode,
"feature_type": info.get("feature_type"),
"feature_guess": guess,
"confidence": confidence,
"angular_span": angular_span,
"height_estimate": height_estimate,
"material_vote_summary": info.get("material_vote_summary"),
"cylinder_end_type": info.get("cylinder_end_type"),
"hole_depth_estimate": info.get("hole_depth_estimate"),
"feature_bottom_face_ids": info.get("feature_bottom_face_ids"),
"feature_opening_face_ids": info.get("feature_opening_face_ids"),
"resize_strategy": "same-axis-bounded-cylinder-recut",
"edit_strategy_label": "同轴圆柱重切",
"edit_semantics": (
"缩小孔/槽直径:先补料再同轴重切;保持当前轴线和估算高度。"
if resize_mode == "shrink"
else "扩大孔/槽直径:沿当前圆柱轴线用有限长度 cutter 切到目标直径;保持当前轴线和估算高度。"
),
"quick_preflight": True,
}
def _confirm_quick_edit_plan(
self,
title: str,
plan: dict[str, object],
lines: list[str],
blocked_status: str,
cancelled_status: str,
) -> bool:
if plan.get("status") == "blocked":
QMessageBox.information(self, f"不能{title}", str(plan.get("message", "")))
self.statusBar().showMessage(blocked_status)
return False
if self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan):
return False
if plan.get("risk") == "low":
return True
warnings = str(plan.get("warnings", ""))
warnings_line = f"\n警告: {warnings}\n" if warnings else ""
semantic_lines = list(lines)
strategy_label = str(plan.get("edit_strategy_label") or "").strip()
edit_semantics = str(plan.get("edit_semantics") or "").strip()
if strategy_label:
semantic_lines.append(f"编辑策略: {strategy_label}")
if edit_semantics:
semantic_lines.append(f"编辑方式: {edit_semantics}")
result = QMessageBox.question(
self,
f"确认{title}",
(
"\n".join(semantic_lines)
+ f"\n风险: {plan.get('risk')}\n"
+ warnings_line
+ f"\n{plan.get('message', '')}\n\n"
"为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;"
"详细几何方案会进入后台计算,完成后自动刷新模型。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage(cancelled_status)
return False
return True
def resize_hole(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再调整孔径。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择圆柱面", "请先选择一个圆柱面。")
return
try:
diameter = float(self.hole_diameter_input.text())
except ValueError:
QMessageBox.critical(self, "直径无效", "请输入数字形式的目标直径。")
return
face_id = self.selected_face_id
info = self._selected_face_info_snapshot()
plan = self._quick_cylinder_resize_plan(face_id, diameter, info)
lines = [
f"Face: {face_id}",
f"当前直径: {_format_value(plan.get('current_diameter'))}",
f"目标直径: {_format_value(plan.get('target_diameter'))}",
f"直径变化量: {_format_value(plan.get('delta_diameter'))}",
f"直径变化比例: {_format_percent(plan.get('diameter_delta_ratio'))}",
f"候选判断: {plan.get('feature_guess')}",
f"置信度: {plan.get('confidence')}",
]
if plan.get("target_to_height_ratio") not in {None, ""}:
lines.append(f"目标直径/估算高度: {_format_value(plan.get('target_to_height_ratio'))}")
if not self._confirm_quick_edit_plan(
"圆柱孔径调整",
plan,
lines,
"圆柱孔径调整已阻止",
"已取消圆柱孔径调整",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_hole(face_id, diameter)
self._run_edit_action(
action,
operation_name="调整圆柱孔径",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"new_diameter": diameter,
"old_diameter": plan.get("current_diameter"),
"delta_diameter": plan.get("delta_diameter"),
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
"target_to_height_ratio": plan.get("target_to_height_ratio"),
"resize_mode": plan.get("resize_mode"),
"feature_type": plan.get("feature_type"),
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"material_vote_summary": plan.get("material_vote_summary"),
"cylinder_end_type": plan.get("cylinder_end_type"),
"hole_depth_estimate": plan.get("hole_depth_estimate"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def resize_cylindrical_owning_scale(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.hole_diameter_input.text())
except ValueError:
QMessageBox.critical(self, "圆柱尺寸无效", "请输入数字形式的目标圆柱直径或半径。")
return
face_id = self.selected_face_id
plan = self.model.cylindrical_owning_scale_plan(face_id, diameter)
lines = [
f"Face: {face_id}",
f"当前直径: {_format_value(plan.get('current_diameter'))}",
f"目标直径: {_format_value(plan.get('target_diameter'))}",
f"当前半径: {_format_value(plan.get('current_radius'))}",
f"目标半径: {_format_value(plan.get('target_radius'))}",
f"直径变化量: {_format_value(plan.get('delta_diameter'))}",
f"直径变化比例: {_format_percent(plan.get('diameter_delta_ratio'))}",
f"缩放比例: {_format_value(plan.get('affine_scale'))}",
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
]
if not self._confirm_quick_edit_plan(
"圆柱尺寸缩放所属对象",
plan,
lines,
"圆柱尺寸缩放所属对象已阻止",
"已取消圆柱尺寸缩放所属对象",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_owning_scale(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"),
"surface": plan.get("surface"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"new_diameter": diameter,
"old_diameter": plan.get("current_diameter"),
"delta_diameter": plan.get("delta_diameter"),
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
"target_radius": plan.get("target_radius"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def move_cylindrical_hole_axis(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_center = (
float(self.hole_center_x_input.text()),
float(self.hole_center_y_input.text()),
float(self.hole_center_z_input.text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "孔轴心坐标无效", "请输入 X, Y, Z 三个数字形式的目标坐标。")
return
face_id = self.selected_face_id
plan = self.model.cylindrical_axis_move_plan(face_id, target_center)
lines = [
f"Face: {face_id}",
f"当前轴心: {_format_value(plan.get('current_axis_center'))}",
f"目标轴心: {_format_value(plan.get('target_axis_center'))}",
f"移动向量: {_format_value(plan.get('axis_move_vector'))}",
f"移动距离: {_format_value(plan.get('axis_move_distance'))}",
f"孔直径: {_format_value(plan.get('target_diameter'))}",
f"候选判断: {plan.get('feature_guess')}",
f"置信度: {plan.get('confidence')}",
]
if not self._confirm_quick_edit_plan(
"圆柱孔轴心坐标修改",
plan,
lines,
"圆柱孔轴心坐标修改已阻止",
"已取消圆柱孔轴心坐标修改",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.move_cylindrical_hole_axis(face_id, target_center)
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"),
"surface": plan.get("surface", "cylinder"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"current_diameter": plan.get("current_diameter"),
"target_diameter": plan.get("target_diameter"),
"current_axis_center": plan.get("current_axis_center"),
"target_axis_center": plan.get("target_axis_center"),
"axis_move_vector": plan.get("axis_move_vector"),
"axis_move_distance": plan.get("axis_move_distance"),
"axis_move_radial_distance": plan.get("axis_move_radial_distance"),
"axis_move_axial_delta": plan.get("axis_move_axial_delta"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"move_axis_status": plan.get("status"),
"move_axis_risk": plan.get("risk"),
"move_axis_message": plan.get("message"),
"move_axis_warnings": plan.get("warnings"),
"move_axis_blockers": plan.get("blockers"),
"fill_strategy": plan.get("fill_strategy"),
"fill_height": plan.get("fill_height"),
"fill_radius": plan.get("fill_radius"),
"cutter_strategy": plan.get("cutter_strategy"),
"cutter_height": plan.get("cutter_height"),
"target_cutter_axis_point": plan.get("target_cutter_axis_point"),
"target_cutter_start_point": plan.get("target_cutter_start_point"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def move_cylindrical_slot_axis(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_center = (
float(self.slot_center_x_input.text()),
float(self.slot_center_y_input.text()),
float(self.slot_center_z_input.text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "槽/半孔轴心坐标无效", "请输入 X, Y, Z 三个数字形式的目标坐标。")
return
face_id = self.selected_face_id
plan = self.model.cylindrical_slot_axis_move_plan(face_id, target_center)
lines = [
f"Face: {face_id}",
f"当前轴心: {_format_value(plan.get('current_axis_center'))}",
f"目标轴心: {_format_value(plan.get('target_axis_center'))}",
f"移动向量: {_format_value(plan.get('axis_move_vector'))}",
f"移动距离: {_format_value(plan.get('axis_move_distance'))}",
f"槽宽/直径: {_format_value(plan.get('slot_target_diameter'))}",
f"槽圆弧角度: {_format_value(plan.get('slot_target_angular_span'))}",
f"候选判断: {plan.get('feature_guess')}",
f"置信度: {plan.get('confidence')}",
"当前版本会先填旧扇形槽,再在目标轴心切出同宽度、同角度的新扇形槽。",
]
if not self._confirm_quick_edit_plan(
"槽/半孔轴心坐标修改",
plan,
lines,
"槽/半孔轴心坐标修改已阻止",
"已取消槽/半孔轴心坐标修改",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.move_cylindrical_slot_axis(face_id, target_center)
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"),
2026-07-29 15:43:28 +08:00
"surface": plan.get("surface", "cylinder"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"slot_kind": plan.get("slot_kind"),
"slot_status": plan.get("slot_status"),
"current_diameter": plan.get("current_diameter"),
"target_diameter": plan.get("target_diameter"),
"slot_current_angular_span": plan.get("slot_current_angular_span"),
"slot_target_angular_span": plan.get("slot_target_angular_span"),
"current_axis_center": plan.get("current_axis_center"),
"target_axis_center": plan.get("target_axis_center"),
"axis_move_vector": plan.get("axis_move_vector"),
"axis_move_distance": plan.get("axis_move_distance"),
"axis_move_radial_distance": plan.get("axis_move_radial_distance"),
"axis_move_axial_delta": plan.get("axis_move_axial_delta"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"move_axis_status": plan.get("status"),
"move_axis_risk": plan.get("risk"),
"move_axis_message": plan.get("message"),
"move_axis_warnings": plan.get("warnings"),
"move_axis_blockers": plan.get("blockers"),
"fill_strategy": plan.get("fill_strategy"),
"fill_height": plan.get("fill_height"),
"fill_radius": plan.get("fill_radius"),
"cutter_strategy": plan.get("cutter_strategy"),
"cutter_height": plan.get("cutter_height"),
"target_cutter_axis_point": plan.get("target_cutter_axis_point"),
"target_cutter_start_point": plan.get("target_cutter_start_point"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def resize_slot_width_owning_scale(self) -> None:
self._resize_slot_metric_owning_scale("width")
def resize_slot_depth_owning_scale(self) -> None:
self._resize_slot_metric_owning_scale("depth")
def resize_slot_arc_length_owning_scale(self) -> None:
self._resize_slot_metric_owning_scale("arc_length")
def _resize_slot_metric_owning_scale(self, mode: str) -> None:
if self.model is None:
return
labels = {
"width": ("槽宽", "slot_width_input", "slot_chord_width_estimate"),
"depth": ("槽深", "slot_depth_input", "slot_sagitta_depth_estimate"),
"arc_length": ("弧长", "slot_arc_length_input", "slot_arc_length_estimate"),
}
label, input_attr, current_key = labels.get(mode, labels["width"])
operation_name = f"{label}(整体)"
if self._edit_busy(f"请等待当前编辑完成后再执行{operation_name}。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择槽/半孔", "请先选择一个槽/半孔候选 Face。")
return
if not hasattr(self, input_attr):
return
try:
target_value = float(getattr(self, input_attr).text())
except ValueError:
QMessageBox.critical(self, f"目标{label}无效", f"请输入数字形式的目标{label}。")
return
if target_value <= 0:
QMessageBox.critical(self, f"目标{label}无效", f"目标{label}必须大于 0。")
return
face_id = self.selected_face_id
info = self._selected_face_info_snapshot()
if info.get("surface") != "cylinder" or info.get("slot_kind") != "partial-cylindrical-groove":
QMessageBox.information(self, "不是槽/半孔候选", "当前选中 Face 没有被识别为槽/半孔候选。")
return
angular_span = _float_or_none(info.get("slot_angular_span"))
if angular_span is None:
angular_span = _float_or_none(info.get("angular_span"))
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
QMessageBox.information(self, f"不能整体修改{label}", "当前候选没有稳定的部分圆柱角度,不能换算为目标圆柱直径。")
return
span = min(max(float(angular_span), 1e-6), math.tau - 1e-6)
sin_half_span = math.sin(span * 0.5)
sagitta_factor = 1.0 - math.cos(min(span, math.pi) * 0.5)
if mode == "width":
if abs(sin_half_span) <= 1e-6:
QMessageBox.information(self, "不能整体修改槽宽", "当前槽圆弧角度过小,不能可靠换算槽宽。")
return
target_diameter = target_value / sin_half_span
elif mode == "depth":
if sagitta_factor <= 1e-6:
QMessageBox.information(self, "不能整体修改槽深", "当前槽圆弧角度过小,不能可靠换算槽深。")
return
target_diameter = 2.0 * target_value / sagitta_factor
else:
target_diameter = 2.0 * target_value / span
current_value = _float_or_none(info.get(current_key))
current_diameter = _float_or_none(info.get("diameter"))
plan = self.model.cylindrical_owning_scale_plan(face_id, target_diameter)
lines = [
f"Face: {face_id}",
f"当前{label}: {_format_value(current_value)}",
f"目标{label}: {_format_value(target_value)}",
f"圆弧角度: {_format_value(angular_span)} rad / {_format_value(math.degrees(float(angular_span)))} 度",
f"当前圆柱直径: {_format_value(current_diameter)}",
f"换算目标圆柱直径: {_format_value(target_diameter)}",
f"缩放比例: {_format_value(plan.get('affine_scale'))}",
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
"当前语义: 整体缩放所属特征或 Solid;不是局部重切槽,槽上其它尺寸也会跟随变化。",
]
if not self._confirm_quick_edit_plan(
f"{operation_name}缩放所属对象",
plan,
lines,
f"{operation_name}已阻止",
f"已取消{operation_name}",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_owning_scale(face_id, target_diameter)
slot_parameters = {
"slot_metric_mode": mode,
"slot_metric_label": label,
"slot_angular_span": angular_span,
"slot_current_value": current_value,
"slot_target_value": target_value,
"current_diameter": current_diameter,
"target_diameter": target_diameter,
"derived_new_diameter": target_diameter,
"old_diameter": plan.get("current_diameter"),
"new_diameter": target_diameter,
"delta_diameter": plan.get("delta_diameter"),
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
"target_radius": plan.get("target_radius"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": "按槽参数换算为目标圆柱直径后,整体缩放所属特征或 Solid;不是局部重切槽。",
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
}
if mode == "width":
slot_parameters.update({"old_slot_width": current_value, "new_slot_width": target_value})
elif mode == "depth":
slot_parameters.update({"old_slot_depth": current_value, "new_slot_depth": target_value})
else:
slot_parameters.update({"old_slot_arc_length": current_value, "new_slot_arc_length": target_value})
self._run_edit_action(
action,
operation_name=f"{operation_name}缩放所属对象",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface", "cylinder"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
**slot_parameters,
},
target_kind="face",
target_id=face_id,
)
def resize_slot_width(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再调整槽/半孔宽度。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择槽/半孔", "请先选择一个槽/半孔候选 Face。")
return
if not hasattr(self, "slot_width_input"):
return
try:
target_width = float(self.slot_width_input.text())
except ValueError:
QMessageBox.critical(self, "目标槽宽无效", "请输入数字形式的目标槽/半孔宽度。")
return
if target_width <= 0:
QMessageBox.critical(self, "目标槽宽无效", "目标槽/半孔宽度必须大于 0。")
return
face_id = self.selected_face_id
info = self._selected_face_info_snapshot()
if info.get("surface") != "cylinder" or info.get("slot_kind") != "partial-cylindrical-groove":
QMessageBox.information(self, "不是槽/半孔候选", "当前选中Face没有被识别为槽/半孔候选。")
return
angular_span = _float_or_none(info.get("slot_angular_span"))
if angular_span is None:
angular_span = _float_or_none(info.get("angular_span"))
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
QMessageBox.information(self, "不能调整槽宽", "当前候选没有稳定的部分圆柱角度,不能换算槽宽。")
return
sin_half_span = math.sin(min(max(angular_span, 1e-6), math.tau - 1e-6) / 2.0)
if abs(sin_half_span) <= 1e-6:
QMessageBox.information(self, "不能调整槽宽", "当前槽圆弧角度过小,不能可靠换算为圆柱直径。")
return
target_diameter = target_width / sin_half_span
current_width = _float_or_none(info.get("slot_chord_width_estimate"))
try:
pair_face_id = self._slot_pair_face_override(face_id)
except ValueError as exc:
QMessageBox.critical(self, "槽孔配对端无效", str(exc))
return
plan = self.model.cylindrical_slot_resize_plan(face_id, target_width, "width", pair_face_id=pair_face_id)
target_diameter = float(plan.get("slot_target_diameter") or plan.get("target_diameter") or target_diameter)
lines = [
f"Face: {face_id}",
f"当前槽宽估算: {_format_value(current_width)}",
f"目标槽宽: {_format_value(target_width)}",
f"槽圆弧角度: {_format_value(angular_span)}",
f"换算目标圆柱直径: {_format_value(target_diameter)}",
"当前版本会把槽宽换算成圆柱直径后执行孔/槽重建,不是完整 CAD 槽参数编辑。",
]
if not self._confirm_quick_edit_plan(
"槽/半孔宽度调整",
plan,
lines,
"槽/半孔宽度调整已阻止",
"已取消槽/半孔宽度调整",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_slot_width(face_id, target_width, pair_face_id=pair_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"),
"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,
"slot_resize_mode": "width",
"slot_resize_strategy": plan.get("slot_resize_strategy"),
"resize_strategy": plan.get("slot_resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"surface": plan.get("surface", "cylinder"),
"slot_pair_face_id": plan.get("slot_pair_face_id"),
"slot_pair_manual_face_id": pair_face_id,
"slot_pair_manual": plan.get("slot_pair_manual"),
"slot_pair_axis_distance": plan.get("slot_pair_axis_distance"),
"slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"),
"slot_target_diameter": target_diameter,
"derived_new_diameter": target_diameter,
"old_diameter": plan.get("current_diameter"),
"delta_diameter": plan.get("delta_diameter"),
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
"resize_mode": plan.get("resize_mode"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"slot_status": info.get("slot_status"),
"slot_kind": info.get("slot_kind"),
"feature_slot_boundary_face_ids": info.get("feature_slot_boundary_face_ids"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
"material_vote_summary": plan.get("material_vote_summary"),
"cylinder_end_type": plan.get("cylinder_end_type"),
"hole_depth_estimate": plan.get("hole_depth_estimate"),
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def resize_slot_depth(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_depth_input"):
return
try:
2026-07-29 15:43:28 +08:00
target_depth = float(self.slot_depth_input.text())
except ValueError:
2026-07-29 15:43:28 +08:00
QMessageBox.critical(self, "目标槽深无效", "请输入数字形式的目标槽/半孔深度。")
return
2026-07-29 15:43:28 +08:00
if target_depth <= 0:
QMessageBox.critical(self, "目标槽深无效", "目标槽/半孔深度必须大于 0。")
return
face_id = self.selected_face_id
2026-07-29 15:43:28 +08:00
info = self._selected_face_info_snapshot()
if info.get("surface") != "cylinder" or info.get("slot_kind") != "partial-cylindrical-groove":
QMessageBox.information(self, "不是槽/半孔候选", "当前选中Face没有被识别为槽/半孔候选。")
return
angular_span = _float_or_none(info.get("slot_angular_span"))
if angular_span is None:
angular_span = _float_or_none(info.get("angular_span"))
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
QMessageBox.information(self, "不能调整槽深", "当前候选没有稳定的部分圆柱角度,不能换算槽深。")
return
sagitta_factor = 1.0 - math.cos(min(max(angular_span, 1e-6), math.pi) / 2.0)
if sagitta_factor <= 1e-6:
QMessageBox.information(self, "不能调整槽深", "当前槽圆弧角度过小,不能可靠换算为圆柱直径。")
return
target_diameter = 2.0 * target_depth / sagitta_factor
current_depth = _float_or_none(info.get("slot_sagitta_depth_estimate"))
current_width = _float_or_none(info.get("slot_chord_width_estimate"))
try:
pair_face_id = self._slot_pair_face_override(face_id)
except ValueError as exc:
QMessageBox.critical(self, "槽孔配对端无效", str(exc))
return
plan = self.model.cylindrical_slot_resize_plan(face_id, target_depth, "depth", pair_face_id=pair_face_id)
target_diameter = float(plan.get("slot_target_diameter") or plan.get("target_diameter") or target_diameter)
lines = [
f"Face: {face_id}",
f"当前槽深估算: {_format_value(current_depth)}",
f"目标槽深: {_format_value(target_depth)}",
f"当前槽宽估算: {_format_value(current_width)}",
f"槽圆弧角度: {_format_value(angular_span)}",
f"换算目标圆柱直径: {_format_value(target_diameter)}",
"当前版本会固定圆弧角度,把槽深换算成圆柱直径后执行孔/槽重建,不是完整 CAD 槽参数编辑。",
]
if not self._confirm_quick_edit_plan(
"槽/半孔深度调整",
plan,
lines,
"槽/半孔深度调整已阻止",
"已取消槽/半孔深度调整",
):
return
self.clear_edit_preview(render=False)
def action():
2026-07-29 15:43:28 +08:00
return self.model.resize_cylindrical_slot_depth(face_id, target_depth, pair_face_id=pair_face_id)
self._run_edit_action(
action,
2026-07-29 15:43:28 +08:00
operation_name="调整槽/半孔深度",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
2026-07-29 15:43:28 +08:00
"new_slot_depth": target_depth,
"old_slot_depth": current_depth,
"slot_depth_delta": None if current_depth is None else target_depth - current_depth,
"old_slot_width": current_width,
"slot_angular_span": angular_span,
"slot_sagitta_factor": sagitta_factor,
"slot_resize_mode": "depth",
"slot_resize_strategy": plan.get("slot_resize_strategy"),
"resize_strategy": plan.get("slot_resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"surface": plan.get("surface", "cylinder"),
"slot_pair_face_id": plan.get("slot_pair_face_id"),
"slot_pair_manual_face_id": pair_face_id,
"slot_pair_manual": plan.get("slot_pair_manual"),
"slot_pair_axis_distance": plan.get("slot_pair_axis_distance"),
"slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"),
"slot_target_diameter": target_diameter,
"derived_new_diameter": target_diameter,
"old_diameter": plan.get("current_diameter"),
"delta_diameter": plan.get("delta_diameter"),
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
"resize_mode": plan.get("resize_mode"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
2026-07-29 15:43:28 +08:00
"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"),
2026-07-29 15:43:28 +08:00
"cylinder_end_type": plan.get("cylinder_end_type"),
"hole_depth_estimate": plan.get("hole_depth_estimate"),
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def resize_slot_arc_length(self) -> None:
if self.model is None:
return
2026-07-29 15:43:28 +08:00
if self._edit_busy("请等待当前编辑完成后再调整槽/半孔圆弧长度。"):
return
if self.selected_face_id is None:
2026-07-29 15:43:28 +08:00
QMessageBox.information(self, "未选择槽/半孔", "请先选择一个槽/半孔候选 Face。")
return
2026-07-29 15:43:28 +08:00
if not hasattr(self, "slot_arc_length_input"):
return
2026-07-29 15:43:28 +08:00
try:
target_arc_length = float(self.slot_arc_length_input.text())
except ValueError:
QMessageBox.critical(self, "目标圆弧长度无效", "请输入数字形式的目标槽/半孔圆弧长度。")
return
if target_arc_length <= 0:
QMessageBox.critical(self, "目标圆弧长度无效", "目标槽/半孔圆弧长度必须大于 0。")
return
face_id = self.selected_face_id
2026-07-29 15:43:28 +08:00
info = self._selected_face_info_snapshot()
if info.get("surface") != "cylinder" or info.get("slot_kind") != "partial-cylindrical-groove":
QMessageBox.information(self, "不是槽/半孔候选", "当前选中Face没有被识别为槽/半孔候选。")
return
angular_span = _float_or_none(info.get("slot_angular_span"))
if angular_span is None:
angular_span = _float_or_none(info.get("angular_span"))
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
QMessageBox.information(self, "不能调整圆弧长度", "当前候选没有稳定的部分圆柱角度,不能换算圆弧长度。")
return
2026-07-29 15:43:28 +08:00
target_diameter = 2.0 * target_arc_length / angular_span
current_arc_length = _float_or_none(info.get("slot_arc_length_estimate"))
current_width = _float_or_none(info.get("slot_chord_width_estimate"))
current_depth = _float_or_none(info.get("slot_sagitta_depth_estimate"))
try:
pair_face_id = self._slot_pair_face_override(face_id)
except ValueError as exc:
QMessageBox.critical(self, "槽孔配对端无效", str(exc))
return
plan = self.model.cylindrical_slot_resize_plan(
face_id,
target_arc_length,
"arc_length",
pair_face_id=pair_face_id,
)
target_diameter = float(plan.get("slot_target_diameter") or plan.get("target_diameter") or target_diameter)
lines = [
f"Face: {face_id}",
f"当前圆弧长度估算: {_format_value(current_arc_length)}",
f"目标圆弧长度: {_format_value(target_arc_length)}",
f"当前槽宽估算: {_format_value(current_width)}",
f"当前槽深估算: {_format_value(current_depth)}",
f"槽圆弧角度: {_format_value(angular_span)}",
f"换算目标圆柱直径: {_format_value(target_diameter)}",
"当前版本会固定圆弧角度,把槽/半孔圆弧长度换算成圆柱直径后执行孔/槽重建,不是完整 CAD 槽参数编辑。",
]
if not self._confirm_quick_edit_plan(
"槽/半孔圆弧长度调整",
plan,
lines,
"槽/半孔圆弧长度调整已阻止",
"已取消槽/半孔圆弧长度调整",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_slot_arc_length(
face_id,
target_arc_length,
pair_face_id=pair_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"),
2026-07-29 15:43:28 +08:00
"new_slot_arc_length": target_arc_length,
"old_slot_arc_length": current_arc_length,
"slot_arc_length_delta": None
if current_arc_length is None
else target_arc_length - current_arc_length,
"old_slot_width": current_width,
"old_slot_depth": current_depth,
"slot_angular_span": angular_span,
"slot_resize_mode": "arc_length",
"slot_resize_strategy": plan.get("slot_resize_strategy"),
"resize_strategy": plan.get("slot_resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"surface": plan.get("surface", "cylinder"),
"slot_pair_face_id": plan.get("slot_pair_face_id"),
"slot_pair_manual_face_id": pair_face_id,
"slot_pair_manual": plan.get("slot_pair_manual"),
"slot_pair_axis_distance": plan.get("slot_pair_axis_distance"),
"slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"),
"slot_target_diameter": target_diameter,
"derived_new_diameter": target_diameter,
"old_diameter": plan.get("current_diameter"),
"delta_diameter": plan.get("delta_diameter"),
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
"resize_mode": plan.get("resize_mode"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
2026-07-29 15:43:28 +08:00
"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"),
2026-07-29 15:43:28 +08:00
"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"),
2026-07-29 15:43:28 +08:00
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def resize_slot_angular_span(self) -> None:
if self.model is None:
return
2026-07-29 15:43:28 +08:00
if self._edit_busy("请等待当前编辑完成后再调整槽/半孔圆弧角度。"):
return
if self.selected_face_id is None:
2026-07-29 15:43:28 +08:00
QMessageBox.information(self, "未选择槽/半孔", "请先选择一个槽/半孔候选 Face。")
return
if not hasattr(self, "slot_angular_span_input"):
return
try:
2026-07-29 15:43:28 +08:00
target_angular_span = float(self.slot_angular_span_input.text())
except ValueError:
2026-07-29 15:43:28 +08:00
QMessageBox.critical(self, "目标圆弧角度无效", "请输入数字形式的目标槽/半孔圆弧角度。")
return
2026-07-29 15:43:28 +08:00
if target_angular_span <= 0:
QMessageBox.critical(self, "目标圆弧角度无效", "目标槽/半孔圆弧角度必须大于 0。")
return
if target_angular_span >= math.tau * 0.92:
QMessageBox.critical(self, "目标圆弧角度无效", "目标槽/半孔圆弧角度不能接近完整圆柱。")
return
face_id = self.selected_face_id
info = self._selected_face_info_snapshot()
2026-07-29 15:43:28 +08:00
if info.get("surface") != "cylinder" or info.get("slot_kind") != "partial-cylindrical-groove":
QMessageBox.information(self, "不是槽/半孔候选", "当前选中Face没有被识别为槽/半孔候选。")
return
current_angular_span = _float_or_none(info.get("slot_angular_span"))
if current_angular_span is None:
current_angular_span = _float_or_none(info.get("angular_span"))
if current_angular_span is None or current_angular_span <= 1e-6 or current_angular_span >= math.tau * 0.92:
QMessageBox.information(self, "不能调整圆弧角度", "当前候选没有稳定的部分圆柱角度。")
return
plan = self.model.cylindrical_slot_angular_span_plan(face_id, target_angular_span)
current_width = _float_or_none(plan.get("slot_current_width") or info.get("slot_chord_width_estimate"))
target_width = _float_or_none(plan.get("slot_target_width"))
current_depth = _float_or_none(plan.get("slot_current_depth") or info.get("slot_sagitta_depth_estimate"))
target_depth = _float_or_none(plan.get("slot_target_depth"))
current_arc_length = _float_or_none(plan.get("slot_current_arc_length") or info.get("slot_arc_length_estimate"))
target_arc_length = _float_or_none(plan.get("slot_target_arc_length"))
lines = [
f"Face: {face_id}",
2026-07-29 15:43:28 +08:00
f"当前圆弧角度: {_format_value(current_angular_span)} rad / {_format_value(math.degrees(current_angular_span))} 度",
f"目标圆弧角度: {_format_value(target_angular_span)} rad / {_format_value(math.degrees(target_angular_span))} 度",
f"当前槽宽估算: {_format_value(current_width)}",
f"目标槽宽估算: {_format_value(target_width)}",
f"当前槽深估算: {_format_value(current_depth)}",
f"目标槽深估算: {_format_value(target_depth)}",
f"当前圆弧长度估算: {_format_value(current_arc_length)}",
f"目标圆弧长度估算: {_format_value(target_arc_length)}",
"当前版本会保持圆柱半径不变,先补旧扇形槽,再按目标角度重切局部扇形槽。",
]
if not self._confirm_quick_edit_plan(
2026-07-29 15:43:28 +08:00
"槽/半孔圆弧角度调整",
plan,
lines,
2026-07-29 15:43:28 +08:00
"槽/半孔圆弧角度调整已阻止",
"已取消槽/半孔圆弧角度调整",
):
return
self.clear_edit_preview(render=False)
def action():
2026-07-29 15:43:28 +08:00
return self.model.resize_cylindrical_slot_angular_span(face_id, target_angular_span)
self._run_edit_action(
action,
2026-07-29 15:43:28 +08:00
operation_name="调整槽/半孔圆弧角度",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
2026-07-29 15:43:28 +08:00
"surface": plan.get("surface", "cylinder"),
"new_slot_angular_span": target_angular_span,
"new_slot_angular_span_degrees": math.degrees(target_angular_span),
"old_slot_angular_span": current_angular_span,
"old_slot_angular_span_degrees": math.degrees(current_angular_span),
"slot_angular_span_delta": plan.get("slot_angular_span_delta"),
"slot_angular_span_delta_ratio": plan.get("slot_angular_span_delta_ratio"),
"old_slot_width": current_width,
"new_slot_width_estimate": target_width,
"old_slot_depth": current_depth,
"new_slot_depth_estimate": target_depth,
"old_slot_arc_length": current_arc_length,
"new_slot_arc_length_estimate": target_arc_length,
"slot_target_diameter": plan.get("slot_target_diameter"),
"resize_mode": plan.get("resize_mode"),
"resize_strategy": plan.get("slot_resize_strategy"),
"slot_resize_strategy": plan.get("slot_resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"slot_resize_mode": plan.get("slot_resize_mode"),
"slot_pair_face_id": plan.get("slot_pair_face_id"),
"slot_pair_axis_distance": plan.get("slot_pair_axis_distance"),
"slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
2026-07-29 15:43:28 +08:00
"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"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def resize_slot_total_length(self) -> None:
if self.model is None:
return
2026-07-29 15:43:28 +08:00
if self._edit_busy("请等待当前编辑完成后再调整槽孔总长度。"):
return
if self.selected_face_id is None:
2026-07-29 15:43:28 +08:00
QMessageBox.information(self, "未选择槽孔", "请先选择一个已识别为长圆槽端部的 Face。")
return
if not hasattr(self, "slot_total_length_input"):
return
try:
2026-07-29 15:43:28 +08:00
target_total_length = float(self.slot_total_length_input.text())
except ValueError:
2026-07-29 15:43:28 +08:00
QMessageBox.critical(self, "目标槽孔总长度无效", "请输入数字形式的目标槽孔总长度。")
return
face_id = self.selected_face_id
2026-07-29 15:43:28 +08:00
try:
pair_face_id = self._slot_pair_face_override(face_id)
except ValueError as exc:
QMessageBox.critical(self, "槽孔配对端无效", str(exc))
return
2026-07-29 15:43:28 +08:00
plan = self.model.cylindrical_slot_total_length_plan(
face_id,
target_total_length,
pair_face_id=pair_face_id,
)
if plan["status"] == "blocked":
2026-07-29 15:43:28 +08:00
QMessageBox.information(self, "不能调整槽孔总长度", str(plan["message"]))
self.statusBar().showMessage("槽孔总长度调整已阻止")
return
2026-07-29 15:43:28 +08:00
lines = [
f"Face: {face_id}",
f"当前槽孔总长度: {_format_value(plan.get('slot_current_total_length'))}",
f"目标槽孔总长度: {_format_value(plan.get('slot_target_total_length'))}",
f"当前两端中心距: {_format_value(plan.get('slot_current_center_distance'))}",
f"目标两端中心距: {_format_value(plan.get('slot_target_center_distance'))}",
f"槽宽/直径: {_format_value(plan.get('slot_target_diameter'))}",
f"配对端 Face: {_format_value(plan.get('slot_pair_face_id'))}",
]
if not self._confirm_quick_edit_plan(
"槽孔总长度调整",
plan,
lines,
"槽孔总长度调整已阻止",
"已取消槽孔总长度调整",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_slot_total_length(
face_id,
target_total_length,
pair_face_id=pair_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"),
"surface": plan.get("surface", "cylinder"),
"new_slot_total_length": target_total_length,
"old_slot_total_length": plan.get("slot_current_total_length"),
"slot_total_length_delta": plan.get("slot_total_length_delta"),
"slot_total_length_delta_ratio": plan.get("slot_total_length_delta_ratio"),
"slot_current_center_distance": plan.get("slot_current_center_distance"),
"slot_target_center_distance": plan.get("slot_target_center_distance"),
"slot_target_diameter": plan.get("slot_target_diameter"),
"derived_new_diameter": plan.get("slot_target_diameter"),
"resize_strategy": plan.get("slot_resize_strategy"),
"slot_resize_strategy": plan.get("slot_resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"slot_resize_mode": plan.get("slot_resize_mode"),
"slot_pair_face_id": plan.get("slot_pair_face_id"),
"slot_pair_manual_face_id": pair_face_id,
"slot_pair_manual": plan.get("slot_pair_manual"),
"slot_pair_axis_distance": plan.get("slot_pair_axis_distance"),
"slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_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="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def resize_slot_center_distance(self) -> None:
2026-07-29 15:43:28 +08:00
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再调整槽孔中心距。"):
2026-07-29 15:43:28 +08:00
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择槽孔", "请先选择一个已识别为长圆槽端部的 Face。")
return
if not hasattr(self, "slot_center_distance_input"):
2026-07-29 15:43:28 +08:00
return
try:
target_center_distance = float(self.slot_center_distance_input.text())
2026-07-29 15:43:28 +08:00
except ValueError:
QMessageBox.critical(self, "目标槽孔中心距无效", "请输入数字形式的目标槽孔中心距。")
return
face_id = self.selected_face_id
try:
pair_face_id = self._slot_pair_face_override(face_id)
except ValueError as exc:
QMessageBox.critical(self, "槽孔配对端无效", str(exc))
2026-07-29 15:43:28 +08:00
return
plan = self.model.cylindrical_slot_center_distance_plan(
face_id,
target_center_distance,
pair_face_id=pair_face_id,
)
2026-07-29 15:43:28 +08:00
if plan["status"] == "blocked":
QMessageBox.information(self, "不能调整槽孔中心距", str(plan["message"]))
self.statusBar().showMessage("槽孔中心距调整已阻止")
2026-07-29 15:43:28 +08:00
return
lines = [
f"Face: {face_id}",
f"当前两端中心距: {_format_value(plan.get('slot_current_center_distance'))}",
f"目标两端中心距: {_format_value(plan.get('slot_target_center_distance'))}",
f"当前槽孔总长度: {_format_value(plan.get('slot_current_total_length'))}",
f"目标槽孔总长度: {_format_value(plan.get('slot_target_total_length'))}",
f"槽宽/直径: {_format_value(plan.get('slot_target_diameter'))}",
f"配对端 Face: {_format_value(plan.get('slot_pair_face_id'))}",
]
if not self._confirm_quick_edit_plan(
"槽孔中心距调整",
plan,
lines,
"槽孔中心距调整已阻止",
"已取消槽孔中心距调整",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_slot_center_distance(
face_id,
target_center_distance,
pair_face_id=pair_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"),
"surface": plan.get("surface", "cylinder"),
"new_slot_center_distance": target_center_distance,
"old_slot_center_distance": plan.get("slot_current_center_distance"),
"slot_center_distance_delta": plan.get("slot_center_distance_delta"),
"slot_center_distance_delta_ratio": plan.get("slot_center_distance_delta_ratio"),
"slot_current_total_length": plan.get("slot_current_total_length"),
"slot_target_total_length": plan.get("slot_target_total_length"),
"slot_current_center_distance": plan.get("slot_current_center_distance"),
"slot_target_center_distance": plan.get("slot_target_center_distance"),
"slot_target_diameter": plan.get("slot_target_diameter"),
"derived_new_diameter": plan.get("slot_target_diameter"),
"resize_strategy": plan.get("slot_resize_strategy"),
"slot_resize_strategy": plan.get("slot_resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"slot_resize_mode": plan.get("slot_resize_mode"),
"slot_pair_face_id": plan.get("slot_pair_face_id"),
"slot_pair_manual_face_id": pair_face_id,
"slot_pair_manual": plan.get("slot_pair_manual"),
"slot_pair_axis_distance": plan.get("slot_pair_axis_distance"),
"slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_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="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}"
2026-07-29 15:43:28 +08:00
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.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
2026-07-29 15:43:28 +08:00
f"工具高度: {_format_value(plan['boss_tool_height'])}\n"
f"轴向重叠余量: {_format_value(plan['boss_tool_axial_margin'])}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会对当前零件的局部几何执行受限B-Rep布尔修改。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消圆柱凸台直径调整")
return
2026-07-29 15:43:28 +08:00
face_id = self.selected_face_id
self._show_cylinder_boss_resize_preview(face_id, diameter)
def action():
2026-07-29 15:43:28 +08:00
return self.model.resize_cylindrical_boss(face_id, diameter)
self._run_edit_action(
action,
2026-07-29 15:43:28 +08:00
operation_name="调整圆柱凸台直径",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
2026-07-29 15:43:28 +08:00
"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"),
2026-07-29 15:43:28 +08:00
"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"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"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"),
2026-07-29 15:43:28 +08:00
},
target_kind="face",
target_id=face_id,
)
def move_cylindrical_boss_axis(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_center = (
float(self.boss_center_x_input.text()),
float(self.boss_center_y_input.text()),
float(self.boss_center_z_input.text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "凸台轴心坐标无效", "请输入 X, Y, Z 三个数字形式的目标坐标。")
return
face_id = self.selected_face_id
plan = self.model.cylindrical_boss_axis_move_plan(face_id, target_center)
lines = [
f"Face: {face_id}",
f"当前轴心: {_format_value(plan.get('current_axis_center'))}",
f"目标轴心: {_format_value(plan.get('target_axis_center'))}",
f"移动向量: {_format_value(plan.get('axis_move_vector'))}",
f"移动距离: {_format_value(plan.get('axis_move_distance'))}",
f"凸台直径: {_format_value(plan.get('target_diameter'))}",
f"工具高度: {_format_value(plan.get('boss_tool_height'))}",
f"候选判断: {plan.get('feature_guess')}",
f"置信度: {plan.get('confidence')}",
"当前版本会先移除旧凸台包络,再按同直径在目标轴心补出凸台。",
]
if not self._confirm_quick_edit_plan(
"圆柱凸台轴心坐标修改",
plan,
lines,
"圆柱凸台轴心坐标修改已阻止",
"已取消圆柱凸台轴心坐标修改",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.move_cylindrical_boss_axis(face_id, target_center)
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"),
"surface": plan.get("surface", "cylinder"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"current_diameter": plan.get("current_diameter"),
"target_diameter": plan.get("target_diameter"),
"current_axis_center": plan.get("current_axis_center"),
"target_axis_center": plan.get("target_axis_center"),
"axis_move_vector": plan.get("axis_move_vector"),
"axis_move_distance": plan.get("axis_move_distance"),
"axis_move_radial_distance": plan.get("axis_move_radial_distance"),
"axis_move_axial_delta": plan.get("axis_move_axial_delta"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"move_axis_status": plan.get("status"),
"move_axis_risk": plan.get("risk"),
"move_axis_message": plan.get("message"),
"move_axis_warnings": plan.get("warnings"),
"move_axis_blockers": plan.get("blockers"),
"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_outer_radius": plan.get("boss_tool_outer_radius"),
"target_boss_tool_radius": plan.get("target_boss_tool_radius"),
"target_boss_tool_axis_point": plan.get("target_boss_tool_axis_point"),
"target_boss_tool_start_point": plan.get("target_boss_tool_start_point"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def resize_boss_height(self) -> None:
if self.model is None:
return
2026-07-29 15:43:28 +08:00
if self._edit_busy("请等待当前编辑完成后再调整凸台高度。"):
return
2026-07-29 15:43:28 +08:00
if self.selected_face_id is None:
QMessageBox.information(self, "未选择圆柱凸台", "请先选择一个圆柱凸台候选 Face。")
return
if not hasattr(self, "boss_height_input"):
return
try:
2026-07-29 15:43:28 +08:00
target_height = float(self.boss_height_input.text())
except ValueError:
2026-07-29 15:43:28 +08:00
QMessageBox.critical(self, "高度无效", "请输入数字形式的目标凸台高度。")
return
2026-07-29 15:43:28 +08:00
face_id = self.selected_face_id
plan = self.model.cylindrical_boss_height_plan(face_id, target_height)
if plan["status"] == "blocked":
2026-07-29 15:43:28 +08:00
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,
2026-07-29 15:43:28 +08:00
"确认圆柱凸台高度调整",
(
2026-07-29 15:43:28 +08:00
f"face: {plan['face_id']}\n"
f"端盖Face: {_format_value(plan.get('boss_height_cap_face_id'))}\n"
f"端盖位置: {_format_value(plan.get('boss_height_endpoint_label'))}\n"
f"当前凸台高度: {_format_value(plan['current_height'])}\n"
f"目标凸台高度: {_format_value(plan['target_height'])}\n"
f"高度变化量: {_format_value(plan['delta_height'])}\n"
f"高度变化比例: {_format_percent(plan['height_delta_ratio'])}\n"
f"端盖推拉距离: {_format_value(plan.get('push_pull_distance'))}\n"
f"候选判断: {plan.get('feature_guess')}\n"
f"置信度: {plan.get('confidence')}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
2026-07-29 15:43:28 +08:00
"继续操作会推拉识别到的凸台端盖,真实修改当前 B-Rep 几何。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
2026-07-29 15:43:28 +08:00
self.statusBar().showMessage("已取消圆柱凸台高度调整")
return
2026-07-29 15:43:28 +08:00
self._show_cylinder_boss_height_preview(face_id, target_height)
def action():
2026-07-29 15:43:28 +08:00
return self.model.resize_cylindrical_boss_height(face_id, target_height)
self._run_edit_action(
action,
2026-07-29 15:43:28 +08:00
operation_name="调整圆柱凸台高度",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"new_height": target_height,
"old_height": plan.get("current_height"),
"delta_height": plan.get("delta_height"),
"height_delta_ratio": plan.get("height_delta_ratio"),
"boss_height_cap_face_id": plan.get("boss_height_cap_face_id"),
"boss_height_endpoint_role": plan.get("boss_height_endpoint_role"),
"boss_height_endpoint_label": plan.get("boss_height_endpoint_label"),
"boss_height_open_end": plan.get("boss_height_open_end"),
"push_pull_distance": plan.get("push_pull_distance"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"boss_height_status": plan.get("status"),
"boss_height_risk": plan.get("risk"),
"boss_height_message": plan.get("message"),
"boss_height_warnings": plan.get("warnings"),
"boss_height_blockers": plan.get("blockers"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"same_domain_face_ids": plan.get("same_domain_face_ids"),
"feature_start_end_face_ids": plan.get("feature_start_end_face_ids"),
"feature_end_end_face_ids": plan.get("feature_end_end_face_ids"),
},
target_kind="face",
target_id=face_id,
)
def resize_cylinder_height(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, "boss_height_input"):
return
try:
target_height = float(self.boss_height_input.text())
except ValueError:
QMessageBox.critical(self, "高度无效", "请输入数字形式的目标圆柱高度。")
return
face_id = self.selected_face_id
plan = self.model.cylindrical_height_plan(face_id, target_height)
lines = [
f"Face: {face_id}",
f"端盖Face: {_format_value(plan.get('boss_height_cap_face_id'))}",
f"端盖位置: {_format_value(plan.get('boss_height_endpoint_label'))}",
f"当前圆柱高度: {_format_value(plan.get('current_height'))}",
f"目标圆柱高度: {_format_value(plan.get('target_height'))}",
f"高度变化量: {_format_value(plan.get('delta_height'))}",
f"端盖推拉距离: {_format_value(plan.get('push_pull_distance'))}",
f"候选判断: {plan.get('feature_guess')}",
"当前版本会推拉识别到的圆柱端盖,不依赖 CAD 原始建模历史。",
]
if not self._confirm_quick_edit_plan(
"圆柱高度调整",
plan,
lines,
"圆柱高度调整已阻止",
"已取消圆柱高度调整",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_height(face_id, target_height)
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"),
"surface": plan.get("surface", "cylinder"),
"new_height": target_height,
"old_height": plan.get("current_height"),
"delta_height": plan.get("delta_height"),
"height_delta_ratio": plan.get("height_delta_ratio"),
"boss_height_cap_face_id": plan.get("boss_height_cap_face_id"),
"boss_height_endpoint_role": plan.get("boss_height_endpoint_role"),
"boss_height_endpoint_label": plan.get("boss_height_endpoint_label"),
"boss_height_open_end": plan.get("boss_height_open_end"),
"push_pull_distance": plan.get("push_pull_distance"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
"resize_strategy": plan.get("resize_strategy"),
"same_domain_face_ids": plan.get("same_domain_face_ids"),
"feature_start_end_face_ids": plan.get("feature_start_end_face_ids"),
"feature_end_end_face_ids": plan.get("feature_end_end_face_ids"),
},
target_kind="face",
target_id=face_id,
)
def resize_cylindrical_height_owning_scale(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, "boss_height_input"):
return
try:
target_height = float(self.boss_height_input.text())
except ValueError:
QMessageBox.critical(self, "高度无效", "请输入数字形式的目标圆柱高度。")
return
if target_height <= 0:
QMessageBox.critical(self, "高度无效", "目标圆柱高度必须大于 0。")
return
face_id = self.selected_face_id
plan = self.model.cylindrical_height_owning_scale_plan(face_id, target_height)
lines = [
f"Face: {face_id}",
f"当前高度: {_format_value(plan.get('current_height'))}",
f"目标高度: {_format_value(plan.get('target_height'))}",
f"高度变化量: {_format_value(plan.get('delta_height'))}",
f"高度变化比例: {_format_percent(plan.get('height_delta_ratio'))}",
f"缩放比例: {_format_value(plan.get('affine_scale'))}",
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
f"轴向: {_format_value(plan.get('affine_axis_direction'))}",
"当前语义: 沿圆柱轴向整体缩放所属特征或 Solid;不是推拉单个端盖。",
]
if not self._confirm_quick_edit_plan(
"高度(整体)缩放所属对象",
plan,
lines,
"高度(整体)已阻止",
"已取消高度(整体)",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_height_owning_scale(face_id, target_height)
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"),
"surface": plan.get("surface", "cylinder"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"new_height": target_height,
"old_height": plan.get("current_height"),
"delta_height": plan.get("delta_height"),
"height_delta_ratio": plan.get("height_delta_ratio"),
"diameter": plan.get("diameter"),
"radius": plan.get("radius"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def suppress_hole(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再封堵圆柱孔。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择圆柱面", "请先选择一个圆柱孔 Face。")
return
info = self.model.face_info(self.selected_face_id)
if info.get("surface") != "cylinder" or "diameter" not in info:
QMessageBox.information(self, "不是圆柱孔", "当前选中的 Face 不是可封堵的圆柱孔。")
return
plan = self.model.cylindrical_suppress_plan(self.selected_face_id)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能封堵圆柱孔", str(plan["message"]))
self.statusBar().showMessage("封堵圆柱孔已阻止")
return
if plan["risk"] != "low":
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
"确认封堵圆柱孔",
(
f"face: {plan['face_id']}\n"
f"直径: {_format_value(plan['diameter'])}\n"
f"估算高度: {_format_value(plan['height_estimate'])}\n"
f"候选判断: {plan['feature_guess']}\n"
f"置信度: {plan['confidence']}\n"
f"材料投票: {plan['material_vote_summary']}\n"
f"端部类型: {plan['cylinder_end_type']}\n"
f"疑似底面 Face: {_format_value(plan.get('feature_bottom_face_ids', ''))}\n"
f"风险: {plan['risk']}\n\n"
f"补料策略: {plan['fill_strategy']}\n"
f"补料半径: {_format_value(plan['fill_radius'])}\n"
f"补料高度: {_format_value(plan['fill_height'])}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会用补料体封堵当前孔,并对当前零件执行受限OCCT Fuse。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消封堵圆柱孔")
return
face_id = self.selected_face_id
self._show_cylinder_suppress_preview(face_id)
def action():
return self.model.suppress_cylindrical_hole(face_id)
self._run_edit_action(
action,
operation_name="封堵圆柱孔",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"diameter": plan.get("diameter"),
"height_estimate": plan.get("height_estimate"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"suppress_status": plan.get("status"),
"suppress_risk": plan.get("risk"),
"suppress_message": plan.get("message"),
"suppress_warnings": plan.get("warnings"),
"suppress_blockers": plan.get("blockers"),
"cylinder_end_type": plan.get("cylinder_end_type"),
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
"feature_bottom_note": plan.get("feature_bottom_note"),
"fill_strategy": plan.get("fill_strategy"),
"fill_height": plan.get("fill_height"),
"fill_radius": plan.get("fill_radius"),
"fill_radius_overlap": plan.get("fill_radius_overlap"),
"fill_note": plan.get("fill_note"),
},
target_kind="face",
target_id=face_id,
)
def _slot_pair_face_override(self, source_face_id: int | None = None) -> int | None:
if not hasattr(self, "slot_pair_face_input"):
return None
text = self.slot_pair_face_input.text().strip()
if not text:
return None
try:
pair_face_id = int(text)
except ValueError as exc:
raise ValueError("槽孔配对端 Face ID 必须是整数。") from exc
if source_face_id is not None and pair_face_id == int(source_face_id):
raise ValueError("槽孔配对端不能和当前槽端使用同一个 Face ID。")
if self.model is not None and (pair_face_id < 0 or pair_face_id >= len(self.model.faces)):
raise ValueError(f"槽孔配对端 Face ID {pair_face_id} 不存在。")
return pair_face_id
def _hole_bottom_face_override(self) -> int | None:
if not hasattr(self, "hole_bottom_face_input"):
return None
text = self.hole_bottom_face_input.text().strip()
if not text:
return None
try:
bottom_face_id = int(text)
except ValueError as exc:
raise ValueError("底面 Face ID 必须是整数。") from exc
if self.model is not None and (bottom_face_id < 0 or bottom_face_id >= len(self.model.faces)):
raise ValueError(f"底面 Face ID {bottom_face_id} 不存在。")
return bottom_face_id
def _quick_cylinder_depth_plan(
self,
face_id: int,
target_depth: float,
bottom_face_id: int | None = None,
info: dict[str, object] | None = None,
) -> dict[str, object]:
info = dict(info or self._selected_face_info_snapshot())
warnings: list[str] = []
blockers: list[str] = []
risk = "low"
status = "ready"
surface = str(info.get("surface", ""))
guess = str(info.get("feature_guess", "cylindrical face"))
confidence = str(info.get("confidence", "low"))
angular_span = _float_or_none(info.get("angular_span"))
end_type = str(info.get("cylinder_end_type", "unknown"))
current_depth = _float_or_none(info.get("hole_depth_estimate"))
if current_depth is None:
current_depth = _float_or_none(info.get("same_domain_height_estimate"))
manual_bottom = bottom_face_id is not None
if surface != "cylinder":
blockers.append("当前选中对象不是圆柱孔/槽,不能调整孔深。")
if guess != "hole/groove candidate":
blockers.append("孔深调整当前版本只支持明确的孔/槽候选。")
if end_type != "blind" and not manual_bottom:
blockers.append("孔深调整需要识别到盲孔/盲槽底面,或手动填写底面 Face ID。")
elif end_type != "blind" and manual_bottom:
risk = self._max_quick_risk(risk, "medium")
warnings.append("端部类型不是明确 blind,当前会按手动底面 Face ID 尝试推断孔深方向。")
if current_depth is None or current_depth <= 1e-9:
blockers.append("当前圆柱面没有可靠的深度估算。")
if target_depth <= 0:
blockers.append("目标深度必须大于 0。")
if guess == "hole/groove candidate" and confidence == "low":
risk = self._max_quick_risk(risk, "medium")
warnings.append("孔/槽判断置信度较低。")
if (
guess == "hole/groove candidate"
and angular_span is not None
and angular_span < math.tau * 0.92
):
risk = self._max_quick_risk(risk, "medium")
warnings.append("这是局部圆柱面,更像槽或半孔,孔深调整会按局部槽处理。")
delta_depth = None
delta_ratio = None
depth_mode = "unknown"
if current_depth is not None and current_depth > 1e-9:
delta_depth = target_depth - current_depth
delta_ratio = abs(delta_depth) / current_depth
depth_mode = "deepen" if delta_depth > 0 else "shallow"
if abs(delta_depth) <= max(current_depth * 1e-5, 1e-6):
blockers.append("目标深度与当前深度几乎相同,不需要修改。")
elif delta_ratio > 1.0:
risk = self._max_quick_risk(risk, "high")
warnings.append("目标深度变化超过当前深度的 100%,很可能贯穿、误切或布尔失败。")
elif delta_ratio > 0.35:
risk = self._max_quick_risk(risk, "medium")
warnings.append("目标深度变化超过当前深度的 35%。")
if target_depth < current_depth * 0.08:
risk = self._max_quick_risk(risk, "high")
warnings.append("目标深度非常浅,补料后可能生成很薄的局部面。")
if blockers:
status = "blocked"
risk = "blocked"
elif risk in {"medium", "high"}:
status = "caution"
message = "".join(blockers + warnings) if blockers or warnings else "可以尝试调整盲孔/盲槽深度。"
return {
"status": status,
"risk": risk,
"message": message,
"warnings": "".join(warnings),
"blockers": "".join(blockers),
"face_id": face_id,
"part_id": info.get("part_id"),
"solid_id": info.get("solid_id"),
"current_depth": current_depth,
"target_depth": target_depth,
"delta_depth": delta_depth,
"depth_delta_ratio": delta_ratio,
"depth_mode": depth_mode,
"diameter": info.get("diameter"),
"radius": info.get("radius"),
"feature_type": info.get("feature_type"),
"feature_guess": guess,
"confidence": confidence,
"angular_span": angular_span,
"cylinder_end_type": end_type,
"feature_bottom_face_ids": info.get("feature_bottom_face_ids"),
"manual_bottom_face_id": "" if bottom_face_id is None else bottom_face_id,
"manual_bottom_face_used": manual_bottom,
"feature_opening_face_ids": info.get("feature_opening_face_ids"),
"feature_bottom_confidence": info.get("feature_bottom_confidence"),
"feature_bottom_detection": info.get("feature_bottom_detection"),
"feature_bottom_note": info.get("feature_bottom_note"),
"resize_strategy": "bounded-blind-depth-cut-or-fill",
"edit_strategy_label": "盲孔/盲槽深度切削或补料",
"edit_semantics": (
"沿识别到的开口到底面方向调整深度:加深时切削,变浅时从新底面到旧底面补料。"
),
2026-07-29 15:43:28 +08:00
"quick_preflight": True,
}
def resize_hole_depth(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再调整孔深。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择圆柱面", "请先选择一个盲孔或盲槽的圆柱面。")
return
try:
target_depth = float(self.hole_depth_input.text())
except ValueError:
QMessageBox.critical(self, "深度无效", "请输入数字形式的目标孔深。")
return
try:
bottom_face_id = self._hole_bottom_face_override()
except ValueError as exc:
QMessageBox.critical(self, "底面 Face ID 无效", str(exc))
return
face_id = self.selected_face_id
info = self._selected_face_info_snapshot()
plan = self._quick_cylinder_depth_plan(face_id, target_depth, bottom_face_id=bottom_face_id, info=info)
lines = [
f"Face: {face_id}",
f"当前深度: {_format_value(plan.get('current_depth'))}",
f"目标深度: {_format_value(plan.get('target_depth'))}",
f"深度变化量: {_format_value(plan.get('delta_depth'))}",
f"深度变化比例: {_format_percent(plan.get('depth_delta_ratio'))}",
f"候选判断: {plan.get('feature_guess')}",
f"置信度: {plan.get('confidence')}",
f"端部类型: {plan.get('cylinder_end_type')}",
]
if bottom_face_id is not None:
lines.append(f"手动底面 Face: {bottom_face_id}")
if not self._confirm_quick_edit_plan(
"盲孔/盲槽深度调整",
plan,
lines,
"盲孔/盲槽深度调整已阻止",
"已取消盲孔/盲槽深度调整",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_depth(
face_id,
target_depth,
bottom_face_id=bottom_face_id,
)
self._run_edit_action(
action,
operation_name="调整盲孔/盲槽深度",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"old_depth": plan.get("current_depth"),
"new_depth": target_depth,
"delta_depth": plan.get("delta_depth"),
"depth_delta_ratio": plan.get("depth_delta_ratio"),
"depth_mode": plan.get("depth_mode"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"depth_status": plan.get("status"),
"depth_risk": plan.get("risk"),
"depth_message": plan.get("message"),
"depth_warnings": plan.get("warnings"),
"depth_blockers": plan.get("blockers"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"cylinder_end_type": plan.get("cylinder_end_type"),
"feature_bottom_face_ids": plan.get("feature_bottom_face_ids"),
"manual_bottom_face_id": plan.get("manual_bottom_face_id"),
"manual_bottom_face_used": plan.get("manual_bottom_face_used"),
"manual_bottom_face_note": plan.get("manual_bottom_face_note"),
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
"feature_bottom_confidence": plan.get("feature_bottom_confidence"),
"feature_bottom_detection": plan.get("feature_bottom_detection"),
"feature_bottom_note": plan.get("feature_bottom_note"),
"quick_preflight": True,
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def resize_hole_depth_owning_scale(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
if target_depth <= 0:
QMessageBox.critical(self, "深度无效", "目标盲孔/盲槽深度必须大于 0。")
return
try:
bottom_face_id = self._hole_bottom_face_override()
except ValueError as exc:
QMessageBox.critical(self, "底面 Face ID 无效", str(exc))
return
face_id = self.selected_face_id
plan = self.model.cylindrical_depth_owning_scale_plan(
face_id,
target_depth,
bottom_face_id=bottom_face_id,
)
lines = [
f"Face: {face_id}",
f"当前深度: {_format_value(plan.get('current_depth'))}",
f"目标深度: {_format_value(plan.get('target_depth'))}",
f"深度变化量: {_format_value(plan.get('delta_depth'))}",
f"深度变化比例: {_format_percent(plan.get('depth_delta_ratio'))}",
f"缩放比例: {_format_value(plan.get('affine_scale'))}",
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
f"缩放轴向: {_format_value(plan.get('affine_axis_direction'))}",
f"疑似底面 Face: {_format_value(plan.get('feature_bottom_face_ids'))}",
"当前语义: 沿盲孔/盲槽方向整体缩放所属特征或 Solid;不是加深切削,也不是变浅补料。",
]
if bottom_face_id is not None:
lines.append(f"手动底面 Face: {bottom_face_id}")
if not self._confirm_quick_edit_plan(
"盲孔/盲槽深度(整体)缩放所属对象",
plan,
lines,
"盲孔/盲槽深度(整体)已阻止",
"已取消盲孔/盲槽深度(整体)",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_depth_owning_scale(
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"),
"surface": plan.get("surface", "cylinder"),
"old_depth": plan.get("current_depth"),
"new_depth": target_depth,
"delta_depth": plan.get("delta_depth"),
"depth_delta_ratio": plan.get("depth_delta_ratio"),
"diameter": plan.get("diameter"),
"radius": plan.get("radius"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"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"),
"feature_opening_face_ids": plan.get("feature_opening_face_ids"),
"depth_open_point": plan.get("depth_open_point"),
"depth_current_bottom_point": plan.get("depth_current_bottom_point"),
"depth_current_depth_source": plan.get("depth_current_depth_source"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"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"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
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"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
2026-07-29 15:43:28 +08:00
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"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"resize_note": plan.get("resize_note"),
"axis_point": plan.get("axis_point"),
"axis": plan.get("axis"),
"height_estimate": plan.get("height_estimate"),
"angular_span": plan.get("angular_span"),
},
target_kind="face",
target_id=face_id,
)
def fillet_edge(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再添加圆角。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。")
return
try:
radius = float(self.edge_fillet_radius_input.text())
except ValueError:
QMessageBox.critical(self, "半径无效", "请输入数字形式的圆角半径。")
return
edge_id = self.selected_edge_id
plan = self.model.edge_fillet_plan(edge_id, radius)
if self._block_unisolated_high_risk_operation("已阻止高风险边圆角", plan):
return
if plan["status"] == "blocked":
QMessageBox.information(self, "不能添加圆角", str(plan["message"]))
self.statusBar().showMessage("添加圆角已阻止")
return
if plan["risk"] != "low":
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
"确认给Edge添加圆角",
(
f"Edge: {plan['edge_id']}\n"
f"Edge长度: {_format_value(plan['edge_length'])}\n"
f"目标圆角半径: {_format_value(plan['target_radius'])}\n"
f"半径/Edge长度比例: {_format_percent(plan['radius_to_length_ratio'])}\n"
f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n"
f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
2026-07-29 15:43:28 +08:00
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会调用OCCT倒圆并真实修改当前零件的局部几何。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消添加圆角")
return
self._show_edge_fillet_preview(edge_id, radius)
def action():
return self.model.fillet_edge(edge_id, radius)
self._run_edit_action(
action,
operation_name="给Edge添加圆角",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"edge_length": plan.get("edge_length"),
"fillet_radius": radius,
"radius_to_length_ratio": plan.get("radius_to_length_ratio"),
"curve": plan.get("curve"),
"adjacent_face_ids": plan.get("adjacent_face_ids"),
"adjacent_face_count": plan.get("adjacent_face_count"),
"fillet_status": plan.get("status"),
"fillet_risk": plan.get("risk"),
"fillet_message": plan.get("message"),
"fillet_warnings": plan.get("warnings"),
"fillet_blockers": plan.get("blockers"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
},
target_kind="edge",
target_id=edge_id,
)
def chamfer_edge(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再添加倒角。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。")
return
try:
distance = float(self.edge_chamfer_distance_input.text())
except ValueError:
QMessageBox.critical(self, "距离无效", "请输入数字形式的倒角距离。")
return
edge_id = self.selected_edge_id
plan = self.model.edge_chamfer_plan(edge_id, distance)
if self._block_unisolated_high_risk_operation("已阻止高风险边倒角", plan):
return
if plan["status"] == "blocked":
QMessageBox.information(self, "不能添加倒角", str(plan["message"]))
self.statusBar().showMessage("添加倒角已阻止")
return
if plan["risk"] != "low":
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
"确认给Edge添加倒角",
(
f"Edge: {plan['edge_id']}\n"
f"Edge长度: {_format_value(plan['edge_length'])}\n"
f"目标倒角距离: {_format_value(plan['target_distance'])}\n"
f"倒角距离/Edge长度比例: {_format_percent(plan['distance_to_length_ratio'])}\n"
f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n"
f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
2026-07-29 15:43:28 +08:00
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会调用OCCT倒角并真实修改当前零件的局部几何。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消添加倒角")
return
self._show_edge_chamfer_preview(edge_id, distance)
def action():
return self.model.chamfer_edge(edge_id, distance)
self._run_edit_action(
action,
operation_name="给Edge添加倒角",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"edge_length": plan.get("edge_length"),
"chamfer_distance": distance,
"distance_to_length_ratio": plan.get("distance_to_length_ratio"),
"curve": plan.get("curve"),
"adjacent_face_ids": plan.get("adjacent_face_ids"),
"adjacent_face_count": plan.get("adjacent_face_count"),
"chamfer_status": plan.get("status"),
"chamfer_risk": plan.get("risk"),
"chamfer_message": plan.get("message"),
"chamfer_warnings": plan.get("warnings"),
"chamfer_blockers": plan.get("blockers"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
},
target_kind="edge",
target_id=edge_id,
)
def chamfer_edge_asymmetric(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再添加不等距倒角。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。")
return
try:
distance1 = float(self.edge_chamfer_distance1_input.text())
distance2 = float(self.edge_chamfer_distance2_input.text())
except (AttributeError, ValueError):
QMessageBox.critical(self, "距离无效", "请输入数字形式的不等距倒角 D1, D2。")
return
reference_face_id = None
if hasattr(self, "edge_chamfer_reference_face_input"):
text = self.edge_chamfer_reference_face_input.text().strip()
if text:
try:
reference_face_id = int(text)
except ValueError:
QMessageBox.critical(self, "参考Face无效", "倒角参考 Face ID 必须是整数。")
return
edge_id = self.selected_edge_id
plan = self.model.edge_asymmetric_chamfer_plan(edge_id, distance1, distance2, reference_face_id)
if self._block_unisolated_high_risk_operation("已阻止高风险不等距倒角", plan):
return
lines = [
f"Edge: {edge_id}",
f"Edge长度: {_format_value(plan.get('edge_length'))}",
f"D1: {_format_value(plan.get('target_distance1'))}",
f"D2: {_format_value(plan.get('target_distance2'))}",
f"参考Face: {_format_value(plan.get('reference_face_id'))}",
f"可用相邻Face: {_format_value(plan.get('reference_face_candidates'))}",
f"D1/Edge长度比例: {_format_percent(plan.get('distance1_to_length_ratio'))}",
f"D2/Edge长度比例: {_format_percent(plan.get('distance2_to_length_ratio'))}",
]
if not self._confirm_quick_edit_plan(
"给Edge添加不等距倒角",
plan,
lines,
blocked_status="不等距倒角已阻止",
cancelled_status="已取消不等距倒角",
):
return
self._show_edge_chamfer_preview(edge_id, max(distance1, distance2))
def action():
return self.model.chamfer_edge_asymmetric(edge_id, distance1, distance2, reference_face_id)
self._run_edit_action(
action,
operation_name="给Edge添加不等距倒角",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"edge_length": plan.get("edge_length"),
"chamfer_mode": plan.get("chamfer_mode"),
"chamfer_distance1": distance1,
"chamfer_distance2": distance2,
"distance1_to_length_ratio": plan.get("distance1_to_length_ratio"),
"distance2_to_length_ratio": plan.get("distance2_to_length_ratio"),
"reference_face_id": plan.get("reference_face_id"),
"reference_face_candidates": plan.get("reference_face_candidates"),
"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"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
},
target_kind="edge",
target_id=edge_id,
)
def chamfer_edge_distance_angle(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再添加距离+角度倒角。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。")
return
try:
distance = float(self.edge_chamfer_angle_distance_input.text())
angle_degrees = float(self.edge_chamfer_angle_degrees_input.text())
except (AttributeError, ValueError):
QMessageBox.critical(self, "目标值无效", "请输入数字形式的距离和角度,例如 1, 45。")
return
reference_face_id = None
if hasattr(self, "edge_chamfer_reference_face_input"):
text = self.edge_chamfer_reference_face_input.text().strip()
if text:
try:
reference_face_id = int(text)
except ValueError:
QMessageBox.critical(self, "参考Face无效", "倒角参考 Face ID 必须是整数。")
return
edge_id = self.selected_edge_id
plan = self.model.edge_distance_angle_chamfer_plan(edge_id, distance, angle_degrees, reference_face_id)
if self._block_unisolated_high_risk_operation("已阻止高风险距离+角度倒角", plan):
return
lines = [
f"Edge: {edge_id}",
f"Edge长度: {_format_value(plan.get('edge_length'))}",
f"距离D: {_format_value(plan.get('target_distance'))}",
f"角度: {_format_value(plan.get('target_angle_degrees'))} 度",
f"参考Face: {_format_value(plan.get('reference_face_id'))}",
f"可用相邻Face: {_format_value(plan.get('reference_face_candidates'))}",
f"D/Edge长度比例: {_format_percent(plan.get('distance_to_length_ratio'))}",
]
if not self._confirm_quick_edit_plan(
"给Edge添加距离+角度倒角",
plan,
lines,
blocked_status="距离+角度倒角已阻止",
cancelled_status="已取消距离+角度倒角",
):
return
self._show_edge_chamfer_preview(edge_id, distance)
def action():
return self.model.chamfer_edge_distance_angle(edge_id, distance, angle_degrees, reference_face_id)
self._run_edit_action(
action,
operation_name="给Edge添加距离+角度倒角",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"edge_length": plan.get("edge_length"),
"chamfer_mode": plan.get("chamfer_mode"),
"chamfer_distance": distance,
"chamfer_angle_degrees": angle_degrees,
"chamfer_angle_radians": plan.get("target_angle_radians"),
"distance_to_length_ratio": plan.get("distance_to_length_ratio"),
"reference_face_id": plan.get("reference_face_id"),
"reference_face_candidates": plan.get("reference_face_candidates"),
"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"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
},
target_kind="edge",
target_id=edge_id,
)
def resize_edge_length(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修改Edge长度。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择Edge", "请先选择一条Edge。")
return
try:
target_length = float(self.edge_target_length_input.text())
except ValueError:
QMessageBox.critical(self, "目标长度无效", "请输入数字形式的Edge目标长度。")
return
edge_id = self.selected_edge_id
anchor_mode = self._edge_length_anchor_mode_from_ui()
strategy_mode = self._edge_length_strategy_mode_from_ui()
plan = self.model.general_edge_length_plan(
edge_id,
target_length,
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
2026-07-29 15:43:28 +08:00
if plan["status"] == "blocked":
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
self.statusBar().showMessage("Edge长度修改已阻止")
return
if plan["risk"] != "low":
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
"确认修改Edge长度",
(
f"Edge: {plan['edge_id']}\n"
f"当前Edge长度: {_format_value(plan['current_length'])}\n"
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
f"变化量: {_format_value(plan['delta_length'])}\n"
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
f"Edge修改策略: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n"
f"影响范围: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
2026-07-29 15:43:28 +08:00
f"策略: {_format_value(plan.get('resize_strategy', ''))}\n"
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
f"重建面数: {_format_value(plan.get('local_edge_deform_face_count', ''))}\n"
f"端面Face: {_format_value(plan.get('end_face_id', ''))}\n"
f"端面位置: {_format_value(plan.get('end_face_label', ''))}\n"
f"端面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n"
f"圆柱Face: {_format_value(plan.get('cylinder_resize_face_id', ''))}\n"
f"圆形Edge模式: {_format_value(plan.get('circular_edge_cylinder_mode_label', ''))}\n"
f"换算目标直径: {_format_value(plan.get('cylinder_resize_target_diameter', ''))}\n"
f"缩放类型: {_format_value(plan.get('affine_transform_label') or plan.get('affine_transform_kind', ''))}\n"
f"缩放说明: {_format_value(plan.get('affine_transform_note', ''))}\n"
f"缩放目标: {_format_value(plan.get('affine_target_kind', ''))}\n"
f"预估Edge长度: {_format_value(plan.get('affine_predicted_edge_length', ''))}\n"
f"缩放校正: {_format_value(plan.get('affine_scale_correction', ''))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"当前会优先只移动被选直线Edge的端点并重建周边平面;没有明确局部路径时会尝试端面推拉、相邻圆柱直径编辑或几何缩放 fallback。"
"这不是通用参数化Edge长度编辑。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消Edge长度修改")
return
self._show_edge_length_preview(edge_id, target_length, anchor_mode=anchor_mode, strategy_mode=strategy_mode)
2026-07-29 15:43:28 +08:00
def action():
return self.model.resize_general_edge_length(
edge_id,
target_length,
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
2026-07-29 15:43:28 +08:00
self._run_edit_action(
action,
operation_name="修改Edge长度",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"current_length": plan.get("current_length"),
"target_length": plan.get("target_length"),
"delta_length": plan.get("delta_length"),
"length_change_ratio": plan.get("length_change_ratio"),
"edge_length_anchor_mode": plan.get("edge_length_anchor_mode"),
"edge_length_anchor_label": plan.get("edge_length_anchor_label"),
"edge_length_strategy_mode": plan.get("edge_length_strategy_mode"),
"edge_length_strategy_label": plan.get("edge_length_strategy_label"),
"edge_length_constraint_summary": plan.get("edge_length_constraint_summary"),
"edge_length_impact_summary": plan.get("edge_length_impact_summary"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
"local_edge_deform_anchor": plan.get("local_edge_deform_anchor"),
"local_edge_deform_face_count": plan.get("local_edge_deform_face_count"),
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
"local_edge_deform_moved_endpoint_count": plan.get("local_edge_deform_moved_endpoint_count"),
"local_edge_deform_risk": plan.get("local_edge_deform_risk"),
"local_edge_deform_note": plan.get("local_edge_deform_note"),
"end_face_id": plan.get("end_face_id"),
"end_face_endpoint_role": plan.get("end_face_endpoint_role"),
"end_face_label": plan.get("end_face_label"),
"push_pull_distance": plan.get("push_pull_distance"),
"resize_strategy": plan.get("resize_strategy"),
"affine_scale": plan.get("affine_scale"),
"affine_initial_scale": plan.get("affine_initial_scale"),
"affine_scale_correction": plan.get("affine_scale_correction"),
"affine_predicted_edge_length": plan.get("affine_predicted_edge_length"),
"affine_scale_refine_iterations": plan.get("affine_scale_refine_iterations"),
"affine_scale_refine_note": plan.get("affine_scale_refine_note"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_transform_note": plan.get("affine_transform_note"),
"affine_transform_warning": plan.get("affine_transform_warning"),
"affine_transform_risk": plan.get("affine_transform_risk"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_axis_source": plan.get("affine_axis_source"),
"affine_anchor_source": plan.get("affine_anchor_source"),
"affine_target_kind": plan.get("affine_target_kind"),
"planar_edge_scale_sample_count": plan.get("planar_edge_scale_sample_count"),
"planar_edge_scale_plane_deviation": plan.get("planar_edge_scale_plane_deviation"),
"circular_edge_current_radius": plan.get("circular_edge_current_radius"),
"circular_edge_target_radius": plan.get("circular_edge_target_radius"),
"circular_edge_length_scale": plan.get("circular_edge_length_scale"),
"circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"),
"circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"),
"circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"),
"cylinder_resize_face_id": plan.get("cylinder_resize_face_id"),
"cylinder_resize_operation": plan.get("cylinder_resize_operation"),
"cylinder_resize_current_diameter": plan.get("cylinder_resize_current_diameter"),
"cylinder_resize_target_diameter": plan.get("cylinder_resize_target_diameter"),
"cylinder_resize_delta_diameter": plan.get("cylinder_resize_delta_diameter"),
"cylinder_resize_delta_ratio": plan.get("cylinder_resize_delta_ratio"),
"cylinder_resize_status": plan.get("cylinder_resize_status"),
"cylinder_resize_risk": plan.get("cylinder_resize_risk"),
"cylinder_resize_feature_guess": plan.get("cylinder_resize_feature_guess"),
"cylinder_resize_confidence": plan.get("cylinder_resize_confidence"),
"cylinder_resize_same_domain_face_ids": plan.get("cylinder_resize_same_domain_face_ids"),
"cylinder_resize_same_domain_face_count": plan.get("cylinder_resize_same_domain_face_count"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
},
target_kind="edge",
target_id=edge_id,
)
def resize_any_edge_length(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修改Edge长度。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择Edge", "请先选择一条Edge。")
return
try:
target_length = float(self.edge_target_length_input.text())
except ValueError:
QMessageBox.critical(self, "目标长度无效", "请输入数字形式的Edge目标长度。")
return
edge_id = self.selected_edge_id
anchor_mode = self._edge_length_anchor_mode_from_ui()
strategy_mode = self._edge_length_strategy_mode_from_ui()
plan = self.model.general_edge_length_plan(
edge_id,
target_length,
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
2026-07-29 15:43:28 +08:00
if plan["status"] == "blocked":
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
self.statusBar().showMessage("Edge长度修改已阻止")
return
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
strategy = str(plan.get("resize_strategy", ""))
if plan["risk"] != "low":
result = QMessageBox.question(
self,
"确认修改Edge长度",
(
f"Edge: {plan['edge_id']}\n"
f"曲线类型: {_format_value(plan.get('curve', ''))}\n"
f"当前Edge长度: {_format_value(plan['current_length'])}\n"
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
f"变化量: {_format_value(plan['delta_length'])}\n"
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
f"Edge修改策略: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n"
f"影响范围: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
2026-07-29 15:43:28 +08:00
f"策略: {_format_value(strategy)}\n"
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
f"重建面数: {_format_value(plan.get('local_edge_deform_face_count', ''))}\n"
f"端面Face: {_format_value(plan.get('end_face_id', ''))}\n"
f"端面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n"
f"圆柱Face: {_format_value(plan.get('cylinder_resize_face_id', ''))}\n"
f"圆形Edge模式: {_format_value(plan.get('circular_edge_cylinder_mode_label', ''))}\n"
f"换算目标直径: {_format_value(plan.get('cylinder_resize_target_diameter', ''))}\n"
f"缩放类型: {_format_value(plan.get('affine_transform_label') or plan.get('affine_transform_kind', ''))}\n"
f"缩放说明: {_format_value(plan.get('affine_transform_note', ''))}\n"
f"缩放目标: {_format_value(plan.get('affine_target_kind', ''))}\n"
f"缩放比例: {_format_value(plan.get('affine_scale', ''))}\n"
f"预估Edge长度: {_format_value(plan.get('affine_predicted_edge_length', ''))}\n"
f"缩放校正: {_format_value(plan.get('affine_scale_correction', ''))}\n"
f"轴线来源: {_format_value(plan.get('affine_axis_source', ''))}\n"
f"平面采样偏差: {_format_value(plan.get('planar_edge_scale_plane_deviation', ''))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消Edge长度修改")
return
self._show_edge_length_preview(edge_id, target_length, anchor_mode=anchor_mode, strategy_mode=strategy_mode)
2026-07-29 15:43:28 +08:00
def action():
return self.model.resize_general_edge_length(
edge_id,
target_length,
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
2026-07-29 15:43:28 +08:00
self._run_edit_action(
action,
operation_name="修改Edge长度",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"curve": plan.get("curve"),
"current_length": plan.get("current_length"),
"target_length": plan.get("target_length"),
"delta_length": plan.get("delta_length"),
"length_change_ratio": plan.get("length_change_ratio"),
"edge_length_anchor_mode": plan.get("edge_length_anchor_mode"),
"edge_length_anchor_label": plan.get("edge_length_anchor_label"),
"edge_length_strategy_mode": plan.get("edge_length_strategy_mode"),
"edge_length_strategy_label": plan.get("edge_length_strategy_label"),
"edge_length_constraint_summary": plan.get("edge_length_constraint_summary"),
"edge_length_impact_summary": plan.get("edge_length_impact_summary"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
"local_edge_deform_anchor": plan.get("local_edge_deform_anchor"),
"local_edge_deform_face_count": plan.get("local_edge_deform_face_count"),
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
"local_edge_deform_moved_endpoint_count": plan.get("local_edge_deform_moved_endpoint_count"),
"local_edge_deform_risk": plan.get("local_edge_deform_risk"),
"local_edge_deform_note": plan.get("local_edge_deform_note"),
"end_face_id": plan.get("end_face_id"),
"end_face_endpoint_role": plan.get("end_face_endpoint_role"),
"end_face_label": plan.get("end_face_label"),
"push_pull_distance": plan.get("push_pull_distance"),
"resize_strategy": plan.get("resize_strategy"),
"affine_scale": plan.get("affine_scale"),
"affine_initial_scale": plan.get("affine_initial_scale"),
"affine_scale_correction": plan.get("affine_scale_correction"),
"affine_predicted_edge_length": plan.get("affine_predicted_edge_length"),
"affine_scale_refine_iterations": plan.get("affine_scale_refine_iterations"),
"affine_scale_refine_note": plan.get("affine_scale_refine_note"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_transform_note": plan.get("affine_transform_note"),
"affine_transform_warning": plan.get("affine_transform_warning"),
"affine_transform_risk": plan.get("affine_transform_risk"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_axis_source": plan.get("affine_axis_source"),
"affine_anchor_source": plan.get("affine_anchor_source"),
"affine_target_kind": plan.get("affine_target_kind"),
"planar_edge_scale_sample_count": plan.get("planar_edge_scale_sample_count"),
"planar_edge_scale_plane_deviation": plan.get("planar_edge_scale_plane_deviation"),
"circular_edge_current_radius": plan.get("circular_edge_current_radius"),
"circular_edge_target_radius": plan.get("circular_edge_target_radius"),
"circular_edge_length_scale": plan.get("circular_edge_length_scale"),
"circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"),
"circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"),
"circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"),
"cylinder_resize_face_id": plan.get("cylinder_resize_face_id"),
"cylinder_resize_operation": plan.get("cylinder_resize_operation"),
"cylinder_resize_current_diameter": plan.get("cylinder_resize_current_diameter"),
"cylinder_resize_target_diameter": plan.get("cylinder_resize_target_diameter"),
"cylinder_resize_delta_diameter": plan.get("cylinder_resize_delta_diameter"),
"cylinder_resize_delta_ratio": plan.get("cylinder_resize_delta_ratio"),
"cylinder_resize_status": plan.get("cylinder_resize_status"),
"cylinder_resize_risk": plan.get("cylinder_resize_risk"),
"cylinder_resize_feature_guess": plan.get("cylinder_resize_feature_guess"),
"cylinder_resize_confidence": plan.get("cylinder_resize_confidence"),
"cylinder_resize_same_domain_face_ids": plan.get("cylinder_resize_same_domain_face_ids"),
"cylinder_resize_same_domain_face_count": plan.get("cylinder_resize_same_domain_face_count"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
},
target_kind="edge",
target_id=edge_id,
)
def resize_ellipse_edge_major_radius(self) -> None:
self._resize_ellipse_edge_axis_radius("major")
2026-07-29 15:43:28 +08:00
def resize_ellipse_edge_minor_radius(self) -> None:
self._resize_ellipse_edge_axis_radius("minor")
def _resize_ellipse_edge_axis_radius(self, axis_kind: str) -> None:
if self.model is None:
return
axis_kind = "minor" if str(axis_kind).lower() == "minor" else "major"
axis_label = "小半径" if axis_kind == "minor" else "主半径"
target_attr = "ellipse_edge_minor_radius_input" if axis_kind == "minor" else "ellipse_edge_major_radius_input"
if self._edit_busy(f"请等待当前编辑完成后再修改椭圆Edge{axis_label}。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "未选择椭圆Edge", "请先选择一条椭圆Edge。")
return
if not hasattr(self, target_attr):
return
try:
target_radius = float(getattr(self, target_attr).text())
except ValueError:
QMessageBox.critical(self, f"目标{axis_label}无效", f"请输入数字形式的椭圆Edge目标{axis_label}。")
return
edge_id = self.selected_edge_id
plan = self.model.ellipse_edge_axis_radius_plan(edge_id, target_radius, axis_kind=axis_kind)
if plan["status"] == "blocked":
QMessageBox.information(self, f"不能修改椭圆Edge{axis_label}", str(plan["message"]))
self.statusBar().showMessage(f"椭圆Edge{axis_label}修改已阻止")
return
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
if plan["risk"] != "low":
result = QMessageBox.question(
self,
f"确认修改椭圆Edge{axis_label}",
(
f"Edge: {plan['edge_id']}\n"
f"当前主半径: {_format_value(plan.get('ellipse_current_major_radius'))}\n"
f"目标主半径: {_format_value(plan.get('ellipse_target_major_radius'))}\n"
f"当前小半径: {_format_value(plan.get('ellipse_current_minor_radius'))}\n"
f"目标小半径: {_format_value(plan.get('ellipse_target_minor_radius'))}\n"
f"缩放方向: {_format_value(plan.get('ellipse_axis_direction_label'))}\n"
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
f"影响范围: {_format_value(plan.get('edit_semantics'))}\n"
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
"继续操作会对当前 B-Rep 结果几何做单轴仿射缩放,并支持失败回滚/撤销。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage(f"已取消椭圆Edge{axis_label}修改")
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_ellipse_edge_axis_radius(edge_id, target_radius, axis_kind=axis_kind)
self._run_edit_action(
action,
operation_name=f"修改椭圆Edge{axis_label}",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": "ellipse-edge",
"ellipse_axis_kind": plan.get("ellipse_axis_kind"),
"ellipse_axis_label": plan.get("ellipse_axis_label"),
"ellipse_current_radius": plan.get("ellipse_current_radius"),
"ellipse_target_radius": target_radius,
"ellipse_radius_delta": plan.get("ellipse_radius_delta"),
"ellipse_radius_delta_ratio": plan.get("ellipse_radius_delta_ratio"),
"ellipse_current_major_radius": plan.get("ellipse_current_major_radius"),
"ellipse_target_major_radius": plan.get("ellipse_target_major_radius"),
"ellipse_current_minor_radius": plan.get("ellipse_current_minor_radius"),
"ellipse_target_minor_radius": plan.get("ellipse_target_minor_radius"),
"affine_scale": plan.get("affine_scale"),
"affine_target_kind": plan.get("affine_target_kind"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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 _edge_length_strategy_mode_from_ui(self) -> str:
combo = getattr(self, "edge_length_strategy_combo", None)
if combo is None:
return "auto"
data = combo.currentData()
return str(data or "auto")
def _edge_endpoint_target_from_inputs(self, endpoint_role: str) -> tuple[float, float, float] | None:
prefix = "edge_start" if endpoint_role == "start" else "edge_end"
try:
return (
float(getattr(self, f"{prefix}_x_input").text()),
float(getattr(self, f"{prefix}_y_input").text()),
2026-07-29 15:43:28 +08:00
float(getattr(self, f"{prefix}_z_input").text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "Edge endpoint coordinate invalid", "Please enter numeric X, Y and Z values.")
return None
def move_edge_start_point(self) -> None:
self._move_edge_endpoint("start")
def move_edge_end_point(self) -> None:
self._move_edge_endpoint("end")
def move_edge_center_point(self) -> None:
if self.model is None:
return
if self._edit_busy("Please wait for the current edit to finish before moving an Edge center."):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "No Edge selected", "Please select an Edge first.")
return
try:
target_center = (
float(self.edge_center_x_input.text()),
float(self.edge_center_y_input.text()),
float(self.edge_center_z_input.text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "Edge center coordinate invalid", "Please enter numeric X, Y and Z values.")
return
edge_id = self.selected_edge_id
plan = self.model.edge_center_move_plan(edge_id, target_center)
lines = [
f"Edge: {edge_id}",
f"Current center: {_format_value(plan.get('current_edge_center'))}",
f"Target center: {_format_value(plan.get('target_edge_center'))}",
f"Move vector: {_format_value(plan.get('moved_edge_center_delta'))}",
f"Edge length: {_format_value(plan.get('current_length'))}",
f"Strategy: {_format_value(plan.get('resize_strategy'))}",
f"Rebuilt faces: {_format_value(plan.get('local_edge_deform_face_count'))}",
]
if not self._confirm_quick_edit_plan(
"移动Edge中心",
plan,
lines,
blocked_status="移动Edge中心已阻止",
cancelled_status="已取消移动Edge中心",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.move_edge_center(edge_id, target_center)
self._run_edit_action(
action,
operation_name="移动Edge中心",
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"curve": plan.get("curve"),
"current_edge_center": plan.get("current_edge_center"),
"target_edge_center": plan.get("target_edge_center"),
"moved_edge_center_delta": plan.get("moved_edge_center_delta"),
"moved_edge_center_distance": plan.get("moved_edge_center_distance"),
"moved_edge_center_ratio": plan.get("moved_edge_center_ratio"),
"edge_length": plan.get("current_length"),
"expected_start_point": plan.get("expected_start_point"),
"expected_end_point": plan.get("expected_end_point"),
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
"local_edge_deform_face_count": plan.get("local_edge_deform_face_count"),
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
"resize_strategy": plan.get("resize_strategy"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
},
target_kind="edge",
target_id=edge_id,
)
def _move_edge_endpoint(self, endpoint_role: str) -> None:
if self.model is None:
return
if self._edit_busy("Please wait for the current edit to finish before moving an Edge endpoint."):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "No Edge selected", "Please select an Edge first.")
return
target_point = self._edge_endpoint_target_from_inputs(endpoint_role)
if target_point is None:
return
edge_id = self.selected_edge_id
plan = self.model.edge_endpoint_move_plan(edge_id, endpoint_role, target_point)
title = "移动Edge起点" if endpoint_role == "start" else "移动Edge终点"
lines = [
f"Edge: {edge_id}",
f"Endpoint: {_format_value(plan.get('edge_endpoint_label'))}",
f"Current endpoint: {_format_value(plan.get('current_endpoint_point'))}",
f"Target endpoint: {_format_value(plan.get('target_endpoint_point'))}",
f"Move vector: {_format_value(plan.get('moved_endpoint_delta'))}",
f"Current Edge length: {_format_value(plan.get('current_length'))}",
f"Result Edge length: {_format_value(plan.get('target_length'))}",
f"Strategy: {_format_value(plan.get('resize_strategy'))}",
f"Rebuilt faces: {_format_value(plan.get('local_edge_deform_face_count'))}",
]
if not self._confirm_quick_edit_plan(
title,
plan,
lines,
blocked_status=f"{title}已阻止",
cancelled_status=f"已取消{title}",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.move_edge_endpoint(edge_id, endpoint_role, target_point)
self._run_edit_action(
action,
operation_name=title,
target=f"Edge {edge_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"curve": plan.get("curve"),
2026-07-29 15:43:28 +08:00
"endpoint_role": plan.get("edge_endpoint_role"),
"current_endpoint_point": plan.get("current_endpoint_point"),
"target_endpoint_point": plan.get("target_endpoint_point"),
"fixed_endpoint_point": plan.get("fixed_endpoint_point"),
"moved_endpoint_delta": plan.get("moved_endpoint_delta"),
"moved_endpoint_distance": plan.get("moved_endpoint_distance"),
"moved_endpoint_ratio": plan.get("moved_endpoint_ratio"),
"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"),
"expected_start_point": plan.get("expected_start_point"),
"expected_end_point": plan.get("expected_end_point"),
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
"local_edge_deform_face_count": plan.get("local_edge_deform_face_count"),
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
"resize_strategy": plan.get("resize_strategy"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
"resize_warnings": plan.get("warnings"),
"resize_blockers": plan.get("blockers"),
},
target_kind="edge",
target_id=edge_id,
)
2026-07-29 15:43:28 +08:00
def resize_cone_reference_radius(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修改圆锥面(整体)。"):
return
2026-07-29 15:43:28 +08:00
if self.selected_face_id is None:
QMessageBox.information(self, "未选择圆锥面", "请先选择一个圆锥面 Face 或圆锥面特征。")
return
if not hasattr(self, "cone_reference_radius_input"):
return
try:
2026-07-29 15:43:28 +08:00
target_radius = float(self.cone_reference_radius_input.text())
except ValueError:
2026-07-29 15:43:28 +08:00
QMessageBox.critical(self, "圆锥参考半径无效", "请输入数字形式的目标圆锥参考半径。")
return
2026-07-29 15:43:28 +08:00
face_id = self.selected_face_id
plan = self.model.conical_reference_radius_plan(face_id, target_radius)
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,
"确认修改圆锥面(整体)",
(
2026-07-29 15:43:28 +08:00
f"Face: {face_id}\n"
f"当前参考半径: {_format_value(plan.get('current_reference_radius'))}\n"
f"目标参考半径: {_format_value(plan.get('target_reference_radius'))}\n"
f"变化比例: {_format_percent(plan.get('reference_radius_delta_ratio'))}\n"
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
2026-07-29 15:43:28 +08:00
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消圆锥面(整体)修改")
return
2026-07-29 15:43:28 +08:00
self.clear_edit_preview(render=False)
def action():
2026-07-29 15:43:28 +08:00
return self.model.resize_conical_reference_radius(face_id, target_radius)
self._run_edit_action(
action,
operation_name="修改圆锥面参数(整体)",
2026-07-29 15:43:28 +08:00
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
2026-07-29 15:43:28 +08:00
"surface": "cone",
"current_reference_radius": plan.get("current_reference_radius"),
"target_reference_radius": plan.get("target_reference_radius"),
"current_reference_diameter": plan.get("current_reference_diameter"),
"target_reference_diameter": plan.get("target_reference_diameter"),
"delta_reference_radius": plan.get("delta_reference_radius"),
"reference_radius_delta_ratio": plan.get("reference_radius_delta_ratio"),
"semi_angle": plan.get("semi_angle"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"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"),
},
2026-07-29 15:43:28 +08:00
target_kind="face",
target_id=face_id,
)
2026-07-29 15:43:28 +08:00
def resize_sphere_radius(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修改球面(整体)。"):
return
2026-07-29 15:43:28 +08:00
if self.selected_face_id is None:
QMessageBox.information(self, "未选择球面", "请先选择一个球面 Face 或球面特征。")
return
if not hasattr(self, "sphere_radius_input"):
return
try:
2026-07-29 15:43:28 +08:00
target_radius = float(self.sphere_radius_input.text())
except ValueError:
2026-07-29 15:43:28 +08:00
QMessageBox.critical(self, "球面半径无效", "请输入数字形式的目标球面半径。")
return
2026-07-29 15:43:28 +08:00
face_id = self.selected_face_id
plan = self.model.spherical_radius_plan(face_id, target_radius)
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,
"确认修改球面(整体)",
(
2026-07-29 15:43:28 +08:00
f"Face: {face_id}\n"
f"当前半径: {_format_value(plan.get('current_radius'))}\n"
f"目标半径: {_format_value(plan.get('target_radius'))}\n"
f"变化比例: {_format_percent(plan.get('radius_delta_ratio'))}\n"
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
2026-07-29 15:43:28 +08:00
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消球面(整体)修改")
return
2026-07-29 15:43:28 +08:00
self.clear_edit_preview(render=False)
def action():
2026-07-29 15:43:28 +08:00
return self.model.resize_spherical_radius(face_id, target_radius)
self._run_edit_action(
action,
operation_name="修改球面半径(整体)",
2026-07-29 15:43:28 +08:00
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
2026-07-29 15:43:28 +08:00
"surface": "sphere",
"current_radius": plan.get("current_radius"),
"target_radius": plan.get("target_radius"),
"current_diameter": plan.get("current_diameter"),
"target_diameter": plan.get("target_diameter"),
"delta_radius": plan.get("delta_radius"),
"radius_delta_ratio": plan.get("radius_delta_ratio"),
"center": plan.get("center"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_target_kind": plan.get("affine_target_kind"),
2026-07-29 15:43:28 +08:00
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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="face",
target_id=face_id,
)
def resize_torus_major_radius(self) -> None:
self._resize_torus_radius("major")
def resize_torus_minor_radius(self) -> None:
self._resize_torus_radius("minor")
def _resize_torus_radius(self, mode: str) -> 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, "torus_radius_input"):
return
try:
target_radius = float(self.torus_radius_input.text())
except ValueError:
QMessageBox.critical(self, "环面半径无效", "请输入数字形式的目标环面半径。")
return
mode_key = "major" if mode == "major" else "minor"
mode_label = "主半径(整体)" if mode_key == "major" else "小半径(整体)"
face_id = self.selected_face_id
plan = self.model.toroidal_radius_plan(face_id, target_radius, mode_key)
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"确认修改环面{mode_label}",
(
f"Face: {face_id}\n"
f"当前主半径: {_format_value(plan.get('current_major_radius'))}\n"
f"目标主半径: {_format_value(plan.get('target_major_radius'))}\n"
f"当前小半径: {_format_value(plan.get('current_minor_radius'))}\n"
f"目标小半径: {_format_value(plan.get('target_minor_radius'))}\n"
f"变化比例: {_format_percent(plan.get('radius_delta_ratio'))}\n"
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\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.clear_edit_preview(render=False)
def action():
return self.model.resize_toroidal_radius(face_id, target_radius, mode_key)
self._run_edit_action(
action,
operation_name=f"修改环面{mode_label}",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": "torus",
"torus_radius_mode": plan.get("torus_radius_mode"),
"current_major_radius": plan.get("current_major_radius"),
"target_major_radius": plan.get("target_major_radius"),
"current_minor_radius": plan.get("current_minor_radius"),
"target_minor_radius": plan.get("target_minor_radius"),
"target_radius": plan.get("target_radius"),
"delta_radius": plan.get("delta_radius"),
"radius_delta_ratio": plan.get("radius_delta_ratio"),
"center": plan.get("center"),
"axis": plan.get("axis"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_axis_direction": plan.get("affine_axis_direction"),
"affine_target_kind": plan.get("affine_target_kind"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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="face",
target_id=face_id,
)
def resize_face_area(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修改面面积。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个 Face。")
return
if not hasattr(self, "face_area_input"):
return
try:
target_area = float(self.face_area_input.text())
except ValueError:
QMessageBox.critical(self, "面面积无效", "请输入数字形式的目标面面积。")
return
face_id = self.selected_face_id
plan = self.model.face_area_scale_plan(face_id, target_area)
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: {face_id}\n"
f"曲面类型: {_format_value(plan.get('surface'))}\n"
f"当前面积: {_format_value(plan.get('current_area'))}\n"
f"目标面积: {_format_value(plan.get('target_area'))}\n"
f"变化比例: {_format_percent(plan.get('area_delta_ratio'))}\n"
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
f"影响说明: {_format_value(plan.get('affine_transform_note'))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"这不是只改变一个面的 CAD 历史面积参数;继续操作会缩放所属对象并刷新 B-Rep 结果几何,支持失败回滚/撤销。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消面面积修改")
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_face_area(face_id, target_area)
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"),
"surface": plan.get("surface"),
"current_area": plan.get("current_area"),
"target_area": plan.get("target_area"),
"area_delta": plan.get("area_delta"),
"area_delta_ratio": plan.get("area_delta_ratio"),
"area_center": plan.get("area_center"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_transform_note": plan.get("affine_transform_note"),
"affine_axis_point": plan.get("affine_axis_point"),
"affine_target_kind": plan.get("affine_target_kind"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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"),
},
2026-07-29 15:43:28 +08:00
target_kind="face",
target_id=face_id,
)
def resize_face_area_local(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再只缩放当前Face面积。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
return
if not hasattr(self, "face_area_input"):
return
try:
target_area = float(self.face_area_input.text())
except ValueError:
QMessageBox.critical(self, "面面积无效", "请输入数字形式的目标面面积。")
return
face_id = self.selected_face_id
plan = self.model.face_area_local_resize_plan(face_id, target_area)
lines = [
f"Face: {face_id}",
f"当前面积: {_format_value(plan.get('current_area'))}",
f"目标面积: {_format_value(plan.get('target_area'))}",
f"面积变化量: {_format_value(plan.get('area_delta'))}",
f"面积变化比例: {_format_percent(plan.get('area_delta_ratio'))}",
f"面内缩放比例: {_format_value(plan.get('local_face_area_scale'))}",
f"重建Face数: {_format_value(plan.get('local_face_deform_face_count'))}",
f"移动顶点数: {_format_value(plan.get('local_face_deform_moved_point_count'))}",
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
]
if not self._confirm_quick_edit_plan(
"只缩放当前Face面积",
plan,
lines,
"只缩放当前Face面积已阻止",
"已取消只缩放当前Face面积",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_face_area_local(face_id, target_area)
self._run_edit_action(
action,
operation_name="只缩放当前Face面积",
target=f"Face {face_id}",
parameters={
"face_id": face_id,
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface"),
"current_area": plan.get("current_area"),
"target_area": plan.get("target_area"),
"area_delta": plan.get("area_delta"),
"area_delta_ratio": plan.get("area_delta_ratio"),
"area_center": plan.get("area_center"),
"local_face_area_scale": plan.get("local_face_area_scale"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"local_face_deform_target_kind": plan.get("local_face_deform_target_kind"),
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def resize_face_width_local(self) -> None:
self._resize_face_size_local("width")
def resize_face_height_local(self) -> None:
self._resize_face_size_local("height")
def resize_face_width_owning_scale(self) -> None:
self._resize_face_size_owning_scale("width")
def resize_face_height_owning_scale(self) -> None:
self._resize_face_size_owning_scale("height")
def _resize_face_size_local(self, axis: str) -> None:
if self.model is None:
return
axis_key = "height" if str(axis).lower() == "height" else "width"
axis_label = "面高" if axis_key == "height" else "面宽"
if self._edit_busy(f"请等待当前编辑完成后再修改{axis_label}(当前面)。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
return
input_attr = "face_height_input" if axis_key == "height" else "face_width_input"
if not hasattr(self, input_attr):
return
try:
target_size = float(getattr(self, input_attr).text())
except ValueError:
QMessageBox.critical(self, f"{axis_label}无效", f"请输入数字形式的目标{axis_label}。")
return
face_id = self.selected_face_id
plan = self.model.face_size_local_resize_plan(face_id, target_size, axis_key)
lines = [
f"Face: {face_id}",
f"修改对象: {axis_label}(当前面)",
f"当前面宽: {_format_value(plan.get('current_face_width'))}",
f"目标面宽: {_format_value(plan.get('target_face_width'))}",
f"当前面高: {_format_value(plan.get('current_face_height'))}",
f"目标面高: {_format_value(plan.get('target_face_height'))}",
f"变化量: {_format_value(plan.get('face_size_delta'))}",
f"变化比例: {_format_percent(plan.get('face_size_delta_ratio'))}",
f"缩放比例: {_format_value(plan.get('face_size_scale'))}",
f"缩放方向: {_format_value(plan.get('face_size_axis_direction'))}",
f"重建Face数: {_format_value(plan.get('local_face_deform_face_count'))}",
f"移动顶点数: {_format_value(plan.get('local_face_deform_moved_point_count'))}",
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
]
if not self._confirm_quick_edit_plan(
f"{axis_label}(当前面)",
plan,
lines,
f"{axis_label}(当前面)已阻止",
f"已取消{axis_label}(当前面)",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_face_size_local(face_id, target_size, axis_key)
self._run_edit_action(
action,
operation_name=f"修改{axis_label}(当前面)",
target=f"Face {face_id}",
parameters={
"face_id": face_id,
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface"),
"face_size_axis": plan.get("face_size_axis"),
"face_size_label": plan.get("face_size_label"),
"current_face_width": plan.get("current_face_width"),
"target_face_width": plan.get("target_face_width"),
"current_face_height": plan.get("current_face_height"),
"target_face_height": plan.get("target_face_height"),
"current_face_size": plan.get("current_face_size"),
"target_face_size": plan.get("target_face_size"),
"face_size_delta": plan.get("face_size_delta"),
"face_size_delta_ratio": plan.get("face_size_delta_ratio"),
"face_size_scale": plan.get("face_size_scale"),
"face_size_center": plan.get("face_size_center"),
"face_size_axis_direction": plan.get("face_size_axis_direction"),
"face_width_direction": plan.get("face_width_direction"),
"face_height_direction": plan.get("face_height_direction"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"local_face_deform_target_kind": plan.get("local_face_deform_target_kind"),
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def _resize_face_size_owning_scale(self, axis: str) -> None:
if self.model is None:
return
axis_key = "height" if str(axis).lower() == "height" else "width"
axis_label = "面高" if axis_key == "height" else "面宽"
if self._edit_busy(f"请等待当前编辑完成后再修改{axis_label}(整体)。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
return
input_attr = "face_height_input" if axis_key == "height" else "face_width_input"
if not hasattr(self, input_attr):
return
try:
target_size = float(getattr(self, input_attr).text())
except ValueError:
QMessageBox.critical(self, f"{axis_label}无效", f"请输入数字形式的目标{axis_label}。")
return
face_id = self.selected_face_id
plan = self.model.face_size_owning_scale_plan(face_id, target_size, axis_key)
lines = [
f"Face: {face_id}",
f"修改对象: {axis_label}(整体)",
f"当前面宽: {_format_value(plan.get('current_face_width'))}",
f"目标面宽: {_format_value(plan.get('target_face_width'))}",
f"当前面高: {_format_value(plan.get('current_face_height'))}",
f"目标面高: {_format_value(plan.get('target_face_height'))}",
f"变化量: {_format_value(plan.get('face_size_delta'))}",
f"变化比例: {_format_percent(plan.get('face_size_delta_ratio'))}",
f"缩放比例: {_format_value(plan.get('face_size_scale'))}",
f"缩放方向: {_format_value(plan.get('face_size_axis_direction'))}",
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
f"缩放类型: {_format_value(plan.get('affine_transform_label'))}",
f"重建模式: {_format_value(plan.get('owning_face_size_rebuild_mode'))}",
f"重建Face数: {_format_value(plan.get('local_face_deform_face_count'))}",
f"移动顶点数: {_format_value(plan.get('local_face_deform_moved_point_count'))}",
f"影响说明: {_format_value(plan.get('affine_transform_note'))}",
]
if not self._confirm_quick_edit_plan(
f"{axis_label}(整体)",
plan,
lines,
f"{axis_label}(整体)已阻止",
f"已取消{axis_label}(整体)",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.resize_face_size_owning_scale(face_id, target_size, axis_key)
self._run_edit_action(
action,
operation_name=f"修改{axis_label}(整体)",
target=f"Face {face_id}",
parameters={
"face_id": face_id,
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface"),
"face_size_axis": plan.get("face_size_axis"),
"face_size_label": plan.get("face_size_label"),
"current_face_width": plan.get("current_face_width"),
"target_face_width": plan.get("target_face_width"),
"current_face_height": plan.get("current_face_height"),
"target_face_height": plan.get("target_face_height"),
"current_face_size": plan.get("current_face_size"),
"target_face_size": plan.get("target_face_size"),
"face_size_delta": plan.get("face_size_delta"),
"face_size_delta_ratio": plan.get("face_size_delta_ratio"),
"face_size_scale": plan.get("face_size_scale"),
"face_size_center": plan.get("face_size_center"),
"face_size_axis_direction": plan.get("face_size_axis_direction"),
"face_width_direction": plan.get("face_width_direction"),
"face_height_direction": plan.get("face_height_direction"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"affine_scale": plan.get("affine_scale"),
"affine_transform_kind": plan.get("affine_transform_kind"),
"affine_transform_label": plan.get("affine_transform_label"),
"affine_transform_note": plan.get("affine_transform_note"),
"affine_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"),
"owning_face_size_rebuild_mode": plan.get("owning_face_size_rebuild_mode"),
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def move_selected_face_plane_position_by_translation(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再按面偏移平移所属对象。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
return
try:
distance = float(self.offset_input.text())
except (AttributeError, ValueError):
QMessageBox.critical(self, "面偏移无效", "请输入数字形式的目标面偏移。")
return
face_id = self.selected_face_id
frame = self._selected_plane_offset_frame(face_id)
if frame is None:
QMessageBox.information(self, "不能按面偏移平移", "当前 Face 缺少稳定平面方向或基准点。")
return
plane_origin, plane_direction, current_position = frame
target_position = current_position + distance
vector = (
plane_direction[0] * distance,
plane_direction[1] * distance,
plane_direction[2] * distance,
)
if self.selected_solid_id is not None:
moved_kind = "solid"
moved_id = self.selected_solid_id
plan = self.model.translate_solid_plan(moved_id, vector)
def action():
return self.model.translate_solid(moved_id, vector)
elif self.selected_part_id is not None:
moved_kind = "part"
moved_id = self.selected_part_id
plan = self.model.translate_part_plan(moved_id, vector)
def action():
return self.model.translate_part(moved_id, vector)
else:
QMessageBox.information(self, "不能按面偏移平移", "当前 Face 没有关联到稳定的所属对象。")
return
if plan["status"] == "blocked":
QMessageBox.information(self, "不能按面偏移平移", str(plan["message"]))
self.statusBar().showMessage("面偏移整体平移已阻止")
return
if not self._confirm_face_plane_position_translation_plan(
plan,
face_id,
current_position,
target_position,
plane_direction,
):
self.statusBar().showMessage("已取消面偏移整体平移")
return
self.clear_edit_preview(render=False)
self._run_edit_action(
action,
operation_name="面偏移(整体)",
target=f"Face {face_id}",
parameters={
"face_id": face_id,
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"moved_target_kind": moved_kind,
"moved_target_id": moved_id,
"current_plane_position": current_position,
"target_plane_position": target_position,
"plane_origin": plane_origin,
"plane_direction": plane_direction,
"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"),
"resize_strategy": "translate-owning-shape-from-plane-offset",
"edit_strategy_label": "按面偏移平移所属对象",
"edit_semantics": "把目标面偏移换算成沿当前平面方向的平移量,并平移所属特征或 Solid;不推拉当前面,不切削,也不补料。",
},
target_kind="face",
target_id=face_id,
)
def move_selected_face_plane_position_local(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再按面偏移只移动当前Face。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
return
try:
distance = float(self.offset_input.text())
except (AttributeError, ValueError):
QMessageBox.critical(self, "面偏移无效", "请输入数字形式的目标面偏移。")
return
face_id = self.selected_face_id
frame = self._selected_plane_offset_frame(face_id)
if frame is None:
QMessageBox.information(self, "不能只移动当前Face", "当前 Face 缺少稳定平面方向或基准点。")
return
plane_origin, plane_direction, current_position = frame
current_center = self._selected_face_center_for_translation(face_id)
if current_center is None:
QMessageBox.information(self, "不能只移动当前Face", "当前 Face 缺少稳定中心坐标。")
return
target_position = current_position + distance
vector = (
plane_direction[0] * distance,
plane_direction[1] * distance,
plane_direction[2] * distance,
)
target_center = (
current_center[0] + vector[0],
current_center[1] + vector[1],
current_center[2] + vector[2],
)
plan = self.model.face_center_local_move_plan(face_id, target_center)
lines = [
f"Face: {face_id}",
f"当前面偏移: {_format_value(current_position)}",
f"目标面偏移: {_format_value(target_position)}",
f"平面方向: {_format_value(plane_direction)}",
f"移动向量: {_format_value(plan.get('face_center_move_vector'))}",
f"移动距离: {_format_value(plan.get('face_center_move_distance'))}",
f"移动/所属对象尺寸: {_format_percent(plan.get('face_center_move_ratio'))}",
f"重建Face数: {_format_value(plan.get('local_face_deform_face_count'))}",
f"移动顶点数: {_format_value(plan.get('local_face_deform_moved_point_count'))}",
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
]
if not self._confirm_quick_edit_plan(
"面偏移(当前面)",
plan,
lines,
"面偏移(当前面)已阻止",
"已取消面偏移(当前面)",
):
return
self.clear_edit_preview(render=False)
def action():
return self.model.move_face_center_local(face_id, target_center)
self._run_edit_action(
action,
operation_name="面偏移(当前面)",
target=f"Face {face_id}",
parameters={
"face_id": face_id,
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface"),
"current_plane_position": current_position,
"target_plane_position": target_position,
"plane_origin": plane_origin,
"plane_direction": plane_direction,
"plane_offset_distance": distance,
"current_face_center": plan.get("current_face_center"),
"target_face_center": plan.get("target_face_center"),
"face_center_move_vector": plan.get("face_center_move_vector"),
"face_center_move_distance": plan.get("face_center_move_distance"),
"face_center_move_ratio": plan.get("face_center_move_ratio"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"local_face_deform_target_kind": plan.get("local_face_deform_target_kind"),
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
"resize_strategy": "local-face-plane-offset-deform",
"edit_strategy_label": "按面偏移只移动当前Face",
"edit_semantics": (
"把目标面偏移换算成沿当前平面方向的移动量,只移动当前 Face 的顶点并重建相邻平面;"
"不推拉加料/切削,也不平移所属对象。"
),
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
},
target_kind="face",
target_id=face_id,
)
def move_selected_face_center(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再移动Face中心。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个 Face。")
return
try:
vector = self._translation_vector_from_inputs()
except ValueError as exc:
QMessageBox.critical(self, "Face中心坐标无效", str(exc))
return
face_id = self.selected_face_id
current_center = self._selected_face_center_for_translation(face_id)
target_center = (
(current_center[0] + vector[0], current_center[1] + vector[1], current_center[2] + vector[2])
if current_center is not None
else None
)
if self.selected_solid_id is not None:
moved_kind = "solid"
moved_id = self.selected_solid_id
plan = self.model.translate_solid_plan(moved_id, vector)
def action():
return self.model.translate_solid(moved_id, vector)
elif self.selected_part_id is not None:
moved_kind = "part"
moved_id = self.selected_part_id
plan = self.model.translate_part_plan(moved_id, vector)
def action():
return self.model.translate_part(moved_id, vector)
else:
QMessageBox.information(self, "不能移动Face中心", "当前 Face 没有关联到稳定的所属对象。")
return
if plan["status"] == "blocked":
QMessageBox.information(self, "不能移动Face中心", str(plan["message"]))
self.statusBar().showMessage("Face中心移动已阻止")
return
if not self._confirm_face_center_move_plan(plan, face_id, current_center, target_center):
self.statusBar().showMessage("已取消Face中心移动")
return
self._run_edit_action(
action,
operation_name="移动Face中心",
target=f"Face {face_id}",
parameters={
"face_id": face_id,
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"moved_target_kind": moved_kind,
"moved_target_id": moved_id,
"current_face_center": current_center,
"target_face_center": target_center,
"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"),
"edit_strategy_label": "平移Face所属对象",
"edit_semantics": "把目标 Face 的中心坐标换算成平移量,并平移所属特征或 Solid;不做单面局部扭曲。",
},
target_kind="face",
target_id=face_id,
)
def move_selected_face_center_local(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再只移动当前Face。"):
return
2026-07-29 15:43:28 +08:00
if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
return
try:
vector = self._translation_vector_from_inputs()
except ValueError as exc:
QMessageBox.critical(self, "Face中心坐标无效", str(exc))
return
2026-07-29 15:43:28 +08:00
face_id = self.selected_face_id
current_center = self._selected_face_center_for_translation(face_id)
if current_center is None:
QMessageBox.information(self, "不能只移动当前Face", "当前 Face 缺少稳定中心坐标。")
return
target_center = (
current_center[0] + vector[0],
current_center[1] + vector[1],
current_center[2] + vector[2],
)
plan = self.model.face_center_local_move_plan(face_id, target_center)
lines = [
f"Face: {face_id}",
f"当前Face中心: {_format_value(plan.get('current_face_center'))}",
f"目标Face中心: {_format_value(plan.get('target_face_center'))}",
f"移动向量: {_format_value(plan.get('face_center_move_vector'))}",
f"移动距离: {_format_value(plan.get('face_center_move_distance'))}",
f"移动/所属对象尺寸: {_format_percent(plan.get('face_center_move_ratio'))}",
f"重建Face数: {_format_value(plan.get('local_face_deform_face_count'))}",
f"移动顶点数: {_format_value(plan.get('local_face_deform_moved_point_count'))}",
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
]
if not self._confirm_quick_edit_plan(
"只移动当前Face",
plan,
lines,
"只移动当前Face已阻止",
"已取消只移动当前Face",
):
return
2026-07-29 15:43:28 +08:00
self.clear_edit_preview(render=False)
def action():
return self.model.move_face_center_local(face_id, target_center)
2026-07-29 15:43:28 +08:00
self._run_edit_action(
action,
operation_name="只移动当前Face",
2026-07-29 15:43:28 +08:00
target=f"Face {face_id}",
parameters={
"face_id": face_id,
2026-07-29 15:43:28 +08:00
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": plan.get("surface"),
"current_face_center": plan.get("current_face_center"),
"target_face_center": plan.get("target_face_center"),
"face_center_move_vector": plan.get("face_center_move_vector"),
"face_center_move_distance": plan.get("face_center_move_distance"),
"face_center_move_ratio": plan.get("face_center_move_ratio"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"local_face_deform_target_kind": plan.get("local_face_deform_target_kind"),
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
2026-07-29 15:43:28 +08:00
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
2026-07-29 15:43:28 +08:00
"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"),
"ui_preview": "skipped-to-avoid-ui-freeze",
2026-07-29 15:43:28 +08:00
},
target_kind="face",
target_id=face_id,
)
def move_selected_axis_center_by_translation(self) -> None:
2026-07-29 15:43:28 +08:00
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再按轴心平移所属对象。"):
2026-07-29 15:43:28 +08:00
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择圆柱特征", "请先选择一个带稳定轴心的孔、槽或凸台 Face。")
2026-07-29 15:43:28 +08:00
return
try:
vector = self._translation_vector_from_inputs()
except ValueError as exc:
QMessageBox.critical(self, "轴心坐标无效", str(exc))
2026-07-29 15:43:28 +08:00
return
face_id = self.selected_face_id
info = self._selected_face_info_snapshot()
current_center = self._selected_axis_center_for_translation(info)
if current_center is None:
QMessageBox.information(self, "不能按轴心平移", "当前对象缺少稳定轴心坐标。")
return
target_center = (
current_center[0] + vector[0],
current_center[1] + vector[1],
current_center[2] + vector[2],
)
if self.selected_solid_id is not None:
moved_kind = "solid"
moved_id = self.selected_solid_id
plan = self.model.translate_solid_plan(moved_id, vector)
def action():
return self.model.translate_solid(moved_id, vector)
elif self.selected_part_id is not None:
moved_kind = "part"
moved_id = self.selected_part_id
plan = self.model.translate_part_plan(moved_id, vector)
def action():
return self.model.translate_part(moved_id, vector)
else:
QMessageBox.information(self, "不能按轴心平移", "当前对象没有关联到稳定的所属特征或 Solid。")
return
2026-07-29 15:43:28 +08:00
if plan["status"] == "blocked":
QMessageBox.information(self, "不能按轴心平移", str(plan["message"]))
self.statusBar().showMessage("按轴心平移所属对象已阻止")
2026-07-29 15:43:28 +08:00
return
if not self._confirm_axis_center_translation_plan(plan, face_id, current_center, target_center):
self.statusBar().showMessage("已取消按轴心平移所属对象")
return
self.clear_edit_preview(render=False)
self._run_edit_action(
action,
operation_name="按轴心平移所属对象",
target=f"Face {face_id}",
parameters={
"face_id": face_id,
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"surface": info.get("surface"),
"feature_type": info.get("feature_type"),
"feature_guess": info.get("feature_guess"),
"moved_target_kind": moved_kind,
"moved_target_id": moved_id,
"current_axis_center": current_center,
"target_axis_center": target_center,
"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"),
"resize_strategy": "translate-owning-shape-from-axis-center",
"edit_strategy_label": "按轴心平移所属对象",
"edit_semantics": "把目标轴心坐标换算成平移量,并平移所属特征或 Solid;不填旧孔、不切新孔、不重建局部凸台。",
},
target_kind="face",
target_id=face_id,
)
def _selected_axis_center_for_translation(self, info: dict[str, object]) -> tuple[float, float, float] | None:
center = _triple_or_none(info.get("axis_center"))
if center is not None:
return center
axis_point = _triple_or_none(info.get("axis_point"))
axis_direction = _triple_or_none(info.get("axis"))
axis_range = info.get("same_domain_v_range") or info.get("v_range")
if (
axis_point is not None
and axis_direction is not None
and isinstance(axis_range, (list, tuple))
and len(axis_range) >= 2
):
v_min = _float_or_none(axis_range[0])
v_max = _float_or_none(axis_range[1])
if v_min is not None and v_max is not None:
v_mid = (v_min + v_max) * 0.5
return (
axis_point[0] + axis_direction[0] * v_mid,
axis_point[1] + axis_direction[1] * v_mid,
axis_point[2] + axis_direction[2] * v_mid,
)
return _triple_or_none(info.get("area_center")) or _triple_or_none(info.get("bbox_center"))
def _confirm_axis_center_translation_plan(
self,
plan: dict[str, object],
face_id: int,
current_center: tuple[float, float, float] | None,
target_center: tuple[float, float, float] | None,
) -> bool:
if plan["risk"] == "low":
return True
2026-07-29 15:43:28 +08:00
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
"确认按轴心平移所属对象",
2026-07-29 15:43:28 +08:00
(
f"Face: {face_id}\n"
f"移动对象: {_format_value(plan.get('target_kind', ''))}\n"
f"零件: {_format_value(plan.get('part_id', ''))}\n"
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
f"当前轴心: {_format_value(current_center)}\n"
f"目标轴心: {_format_value(target_center)}\n"
f"平移向量: {_format_value(plan['translation_vector'])}\n"
f"平移距离: {_format_value(plan['translation_distance'])}\n"
f"编辑策略: 按轴心平移所属对象\n"
f"编辑方式: 平移所属特征或 Solid;不填旧孔、不切新孔、不重建局部凸台。\n"
2026-07-29 15:43:28 +08:00
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"这个语义适合整体搬动对象;如果你想只移动孔/槽/凸台本身,请使用不带“平移所属对象”的轴心坐标行。确定继续吗?"
2026-07-29 15:43:28 +08:00
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
return result == QMessageBox.StandardButton.Yes
def _selected_face_center_for_translation(self, face_id: int) -> tuple[float, float, float] | None:
info = dict(getattr(self, "current_info_values", {}) or {})
try:
current_face_id = int(info.get("face_id", -1))
except (TypeError, ValueError):
current_face_id = -1
if current_face_id != face_id and self.model is not None:
try:
info = self.model.quick_face_info(face_id)
except Exception:
info = {}
center = _triple_or_none(info.get("area_center"))
if center is not None:
return center
bbox_min = _triple_or_none(info.get("bbox_min"))
bbox_max = _triple_or_none(info.get("bbox_max"))
if bbox_min is not None and bbox_max is not None:
return (
(bbox_min[0] + bbox_max[0]) * 0.5,
(bbox_min[1] + bbox_max[1]) * 0.5,
(bbox_min[2] + bbox_max[2]) * 0.5,
)
return None
def _selected_plane_offset_frame(
self,
face_id: int,
) -> tuple[tuple[float, float, float], tuple[float, float, float], float] | None:
info = dict(getattr(self, "current_info_values", {}) or {})
try:
current_face_id = int(info.get("face_id", -1))
except (TypeError, ValueError):
current_face_id = -1
if current_face_id != face_id and self.model is not None:
try:
info = self.model.quick_face_info(face_id)
except Exception:
info = {}
if str(info.get("surface", "")) != "plane":
return None
origin = _triple_or_none(info.get("plane_origin"))
direction = (
_unit_triple_or_none(info.get("push_pull_outward_direction"))
or _unit_triple_or_none(info.get("oriented_normal"))
or _unit_triple_or_none(info.get("normal"))
)
if origin is None or direction is None:
return None
current_position = (
origin[0] * direction[0]
+ origin[1] * direction[1]
+ origin[2] * direction[2]
)
return origin, direction, current_position
def _confirm_face_center_move_plan(
self,
plan: dict[str, object],
face_id: int,
current_center: tuple[float, float, float] | None,
target_center: tuple[float, float, float] | None,
) -> 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,
"确认移动Face中心",
(
f"Face: {face_id}\n"
f"移动对象: {_format_value(plan.get('target_kind', ''))}\n"
f"零件: {_format_value(plan.get('part_id', ''))}\n"
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
f"当前Face中心: {_format_value(current_center)}\n"
f"目标Face中心: {_format_value(target_center)}\n"
f"平移向量: {_format_value(plan['translation_vector'])}\n"
f"平移距离: {_format_value(plan['translation_distance'])}\n"
f"编辑策略: 平移Face所属对象\n"
f"编辑方式: 把目标 Face 的中心坐标换算成平移量,并平移所属特征或 Solid;不做单面局部扭曲。\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"当前语义是平移这个 Face 所属的特征或 Solid,让 Face 中心到达目标坐标;"
"不会只扭曲单个 Face。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
return result == QMessageBox.StandardButton.Yes
def _confirm_face_plane_position_translation_plan(
self,
plan: dict[str, object],
face_id: int,
current_position: float,
target_position: float,
plane_direction: tuple[float, float, float],
) -> 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,
"确认面偏移整体平移",
(
f"Face: {face_id}\n"
f"移动对象: {_format_value(plan.get('target_kind', ''))}\n"
f"零件: {_format_value(plan.get('part_id', ''))}\n"
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
f"当前面偏移: {_format_value(current_position)}\n"
f"目标面偏移: {_format_value(target_position)}\n"
f"平面方向: {_format_value(plane_direction)}\n"
f"平移向量: {_format_value(plan['translation_vector'])}\n"
f"平移距离: {_format_value(plan['translation_distance'])}\n"
f"编辑策略: 按面偏移平移所属对象\n"
f"编辑方式: 沿当前平面方向平移所属特征或 Solid;不推拉当前面,不切削,也不补料。\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"如果你想改变厚度或把这个面推出/切入,请使用不带“整体”的面偏移或偏移距离行。确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
return result == QMessageBox.StandardButton.Yes
def translate_selected_part(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再平移零件。"):
return
if self.selected_part_id is None:
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
return
try:
vector = self._translation_vector_from_inputs()
except ValueError as exc:
QMessageBox.critical(self, "平移向量无效", str(exc))
return
part_id = self.selected_part_id
plan = self.model.translate_part_plan(part_id, vector)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能平移零件", str(plan["message"]))
self.statusBar().showMessage("平移零件已阻止")
return
if not self._confirm_translation_plan("确认平移零件", plan):
self.statusBar().showMessage("已取消平移零件")
return
def action():
return self.model.translate_part(part_id, vector)
self._run_edit_action(
action,
operation_name="平移零件",
target=f"零件 {part_id}",
parameters={
"part_id": part_id,
"translation_vector": vector,
"translation_distance": plan.get("translation_distance"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"translate_status": plan.get("status"),
"translate_risk": plan.get("risk"),
"translate_message": plan.get("message"),
"translate_warnings": plan.get("warnings"),
"translate_blockers": plan.get("blockers"),
},
target_kind="part",
target_id=part_id,
)
def translate_selected_solid(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再平移Solid。"):
return
if self.selected_solid_id is None:
QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择属于 Solid 的 Face/Edge。")
return
try:
vector = self._translation_vector_from_inputs()
except ValueError as exc:
QMessageBox.critical(self, "平移向量无效", str(exc))
return
solid_id = self.selected_solid_id
plan = self.model.translate_solid_plan(solid_id, vector)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能平移Solid", str(plan["message"]))
self.statusBar().showMessage("平移Solid已阻止")
return
if not self._confirm_translation_plan("确认平移Solid", plan):
self.statusBar().showMessage("已取消平移Solid")
return
def action():
return self.model.translate_solid(solid_id, vector)
self._run_edit_action(
action,
operation_name="平移Solid",
target=f"Solid {solid_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": solid_id,
"part_solid_count": plan.get("part_solid_count"),
"translation_vector": vector,
"translation_distance": plan.get("translation_distance"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"translate_status": plan.get("status"),
"translate_risk": plan.get("risk"),
"translate_message": plan.get("message"),
"translate_warnings": plan.get("warnings"),
"translate_blockers": plan.get("blockers"),
},
target_kind="solid",
target_id=solid_id,
)
def _translation_vector_from_inputs(self) -> tuple[float, float, float]:
try:
return (
float(self.translate_x_input.text()),
float(self.translate_y_input.text()),
float(self.translate_z_input.text()),
)
except ValueError as exc:
raise ValueError("请输入数字形式的 X/Y/Z 平移量。") from exc
def _confirm_translation_plan(self, title: str, plan: dict[str, object]) -> bool:
if plan["risk"] == "low":
return True
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
title,
(
f"对象: {plan.get('target_kind', '')}\n"
f"零件: {_format_value(plan.get('part_id', ''))}\n"
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
f"平移向量: {_format_value(plan['translation_vector'])}\n"
f"平移距离: {_format_value(plan['translation_distance'])}\n"
f"对象 bbox_diagonal: {_format_value(plan.get('bbox_diagonal', ''))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会移动当前对象的 B-Rep 形状,并刷新模型拓扑。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
return result == QMessageBox.StandardButton.Yes
def rotate_selected_part(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再旋转零件。"):
return
if self.selected_part_id is None:
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
return
try:
axis, angle = self._rotation_values_from_inputs()
except ValueError as exc:
QMessageBox.critical(self, "旋转参数无效", str(exc))
return
part_id = self.selected_part_id
plan = self.model.rotate_part_plan(part_id, axis, angle)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能旋转零件", str(plan["message"]))
self.statusBar().showMessage("旋转零件已阻止")
return
if not self._confirm_rotation_plan("确认旋转零件", plan):
self.statusBar().showMessage("已取消旋转零件")
return
def action():
return self.model.rotate_part(part_id, axis, angle)
self._run_edit_action(
action,
operation_name="旋转零件",
target=f"零件 {part_id}",
parameters={
"part_id": part_id,
"rotation_axis": plan.get("rotation_axis"),
"rotation_angle_degrees": angle,
"rotation_center": plan.get("rotation_center"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"rotate_status": plan.get("status"),
"rotate_risk": plan.get("risk"),
"rotate_message": plan.get("message"),
"rotate_warnings": plan.get("warnings"),
"rotate_blockers": plan.get("blockers"),
},
target_kind="part",
target_id=part_id,
)
def rotate_selected_solid(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再旋转Solid。"):
return
if self.selected_solid_id is None:
QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择属于 Solid 的 Face/Edge。")
return
try:
axis, angle = self._rotation_values_from_inputs()
except ValueError as exc:
QMessageBox.critical(self, "旋转参数无效", str(exc))
return
solid_id = self.selected_solid_id
plan = self.model.rotate_solid_plan(solid_id, axis, angle)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能旋转Solid", str(plan["message"]))
self.statusBar().showMessage("旋转Solid已阻止")
return
if not self._confirm_rotation_plan("确认旋转Solid", plan):
self.statusBar().showMessage("已取消旋转Solid")
return
def action():
return self.model.rotate_solid(solid_id, axis, angle)
self._run_edit_action(
action,
operation_name="旋转Solid",
target=f"Solid {solid_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": solid_id,
"part_solid_count": plan.get("part_solid_count"),
"rotation_axis": plan.get("rotation_axis"),
"rotation_angle_degrees": angle,
"rotation_center": plan.get("rotation_center"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"rotate_status": plan.get("status"),
"rotate_risk": plan.get("risk"),
"rotate_message": plan.get("message"),
"rotate_warnings": plan.get("warnings"),
"rotate_blockers": plan.get("blockers"),
},
target_kind="solid",
target_id=solid_id,
)
2026-07-29 15:43:28 +08:00
def scale_selected_part(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再缩放零件。"):
return
if self.selected_part_id is None:
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
return
try:
target_diagonal = float(self.scale_target_diagonal_input.text())
except ValueError:
QMessageBox.critical(self, "目标尺寸无效", "请输入数字形式的目标包围盒对角线。")
return
part_id = self.selected_part_id
plan = self.model.scale_part_plan(part_id, target_diagonal)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能缩放零件", str(plan["message"]))
self.statusBar().showMessage("缩放零件已阻止")
return
if not self._confirm_scale_plan("确认缩放零件", plan):
self.statusBar().showMessage("已取消缩放零件")
return
def action():
return self.model.scale_part(part_id, target_diagonal)
self._run_edit_action(
action,
operation_name="缩放零件",
target=f"零件 {part_id}",
parameters={
"part_id": part_id,
"current_bbox_diagonal": plan.get("current_bbox_diagonal"),
"target_bbox_diagonal": plan.get("target_bbox_diagonal"),
"scale_factor": plan.get("scale_factor"),
"scale_delta_ratio": plan.get("scale_delta_ratio"),
"scale_center": plan.get("scale_center"),
"resize_strategy": plan.get("resize_strategy"),
"scale_status": plan.get("status"),
"scale_risk": plan.get("risk"),
"scale_message": plan.get("message"),
"scale_warnings": plan.get("warnings"),
"scale_blockers": plan.get("blockers"),
},
target_kind="part",
target_id=part_id,
)
def scale_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:
target_diagonal = float(self.scale_target_diagonal_input.text())
except ValueError:
QMessageBox.critical(self, "目标尺寸无效", "请输入数字形式的目标包围盒对角线。")
return
solid_id = self.selected_solid_id
plan = self.model.scale_solid_plan(solid_id, target_diagonal)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能缩放Solid", str(plan["message"]))
self.statusBar().showMessage("缩放Solid已阻止")
return
if not self._confirm_scale_plan("确认缩放Solid", plan):
self.statusBar().showMessage("已取消缩放Solid")
return
def action():
return self.model.scale_solid(solid_id, target_diagonal)
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"),
"current_bbox_diagonal": plan.get("current_bbox_diagonal"),
"target_bbox_diagonal": plan.get("target_bbox_diagonal"),
"scale_factor": plan.get("scale_factor"),
"scale_delta_ratio": plan.get("scale_delta_ratio"),
"scale_center": plan.get("scale_center"),
"resize_strategy": plan.get("resize_strategy"),
"scale_status": plan.get("status"),
"scale_risk": plan.get("risk"),
"scale_message": plan.get("message"),
"scale_warnings": plan.get("warnings"),
"scale_blockers": plan.get("blockers"),
},
target_kind="solid",
target_id=solid_id,
)
def scale_selected_part_x_size(self) -> None:
self._scale_selected_axis_size("part", "X")
def scale_selected_part_y_size(self) -> None:
self._scale_selected_axis_size("part", "Y")
def scale_selected_part_z_size(self) -> None:
self._scale_selected_axis_size("part", "Z")
def scale_selected_solid_x_size(self) -> None:
self._scale_selected_axis_size("solid", "X")
def scale_selected_solid_y_size(self) -> None:
self._scale_selected_axis_size("solid", "Y")
def scale_selected_solid_z_size(self) -> None:
self._scale_selected_axis_size("solid", "Z")
def _scale_selected_axis_size(self, target_kind: str, axis: str) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再按轴缩放。"):
return
input_name = {
"X": "scale_x_size_input",
"Y": "scale_y_size_input",
"Z": "scale_z_size_input",
}.get(axis.upper())
input_widget = getattr(self, input_name or "", None)
if input_widget is None:
return
try:
target_size = float(input_widget.text())
except ValueError:
QMessageBox.critical(self, "目标尺寸无效", f"请输入数字形式的目标 {axis.upper()} 向尺寸。")
return
if target_kind == "part":
if self.selected_part_id is None:
QMessageBox.information(self, "未选择零件", "请先选择一个零件,或选择属于某个零件的对象。")
return
target_id = self.selected_part_id
plan = self.model.scale_part_axis_plan(target_id, axis, target_size)
title = f"确认缩放零件{axis.upper()}向尺寸"
def action():
return self.model.scale_part_axis(target_id, axis, target_size)
operation_name = f"缩放零件{axis.upper()}向尺寸"
target_label = f"零件 {target_id}"
run_target_kind = "part"
else:
if self.selected_solid_id is None:
QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择属于 Solid 的 Face/Edge。")
return
target_id = self.selected_solid_id
plan = self.model.scale_solid_axis_plan(target_id, axis, target_size)
title = f"确认缩放Solid{axis.upper()}向尺寸"
def action():
return self.model.scale_solid_axis(target_id, axis, target_size)
operation_name = f"缩放Solid{axis.upper()}向尺寸"
target_label = f"Solid {target_id}"
run_target_kind = "solid"
if plan["status"] == "blocked":
QMessageBox.information(self, "不能按轴缩放", str(plan["message"]))
self.statusBar().showMessage("按轴缩放已阻止")
return
if not self._confirm_axis_scale_plan(title, plan):
self.statusBar().showMessage("已取消按轴缩放")
return
self._run_edit_action(
action,
operation_name=operation_name,
target=target_label,
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"part_solid_count": plan.get("part_solid_count"),
"scale_axis": plan.get("scale_axis"),
"current_axis_size": plan.get("current_axis_size"),
"target_axis_size": plan.get("target_axis_size"),
"axis_scale_factor": plan.get("axis_scale_factor"),
"axis_scale_delta_ratio": plan.get("axis_scale_delta_ratio"),
"scale_center": plan.get("scale_center"),
"bbox_size": plan.get("bbox_size"),
"resize_strategy": plan.get("resize_strategy"),
"scale_status": plan.get("status"),
"scale_risk": plan.get("risk"),
"scale_message": plan.get("message"),
"scale_warnings": plan.get("warnings"),
"scale_blockers": plan.get("blockers"),
},
target_kind=run_target_kind,
target_id=target_id,
)
def _confirm_scale_plan(self, title: str, plan: dict[str, object]) -> bool:
if plan["risk"] == "low":
return True
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
title,
(
f"对象: {plan.get('target_kind', '')}\n"
f"零件: {_format_value(plan.get('part_id', ''))}\n"
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
f"当前包围盒对角线: {_format_value(plan.get('current_bbox_diagonal'))}\n"
f"目标包围盒对角线: {_format_value(plan.get('target_bbox_diagonal'))}\n"
f"缩放比例: {_format_value(plan.get('scale_factor'))}\n"
f"缩放中心: {_format_value(plan.get('scale_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 _confirm_axis_scale_plan(self, title: str, plan: dict[str, object]) -> bool:
if plan["risk"] == "low":
return True
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
title,
(
f"对象: {plan.get('target_kind', '')}\n"
f"零件: {_format_value(plan.get('part_id', ''))}\n"
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
f"缩放轴: {_format_value(plan.get('scale_axis'))}\n"
f"当前轴向尺寸: {_format_value(plan.get('current_axis_size'))}\n"
f"目标轴向尺寸: {_format_value(plan.get('target_axis_size'))}\n"
f"缩放比例: {_format_value(plan.get('axis_scale_factor'))}\n"
f"缩放中心: {_format_value(plan.get('scale_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 _rotation_values_from_inputs(self) -> tuple[str, float]:
try:
angle = float(self.rotate_angle_input.text())
except ValueError as exc:
raise ValueError("请输入数字形式的旋转角度。") from exc
return self.rotate_axis_combo.currentText(), angle
def _confirm_rotation_plan(self, title: str, plan: dict[str, object]) -> bool:
if plan["risk"] == "low":
return True
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
title,
(
f"对象: {plan.get('target_kind', '')}\n"
f"零件: {_format_value(plan.get('part_id', ''))}\n"
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
f"旋转轴: {_format_value(plan['rotation_axis'])}\n"
f"旋转角度: {_format_value(plan['rotation_angle_degrees'])}\n"
f"旋转中心: {_format_value(plan['rotation_center'])}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会旋转当前对象的 B-Rep 形状,并刷新模型拓扑。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
return result == QMessageBox.StandardButton.Yes
def _run_scan_task(self, scan_kind: str, action, context: dict[str, object] | None = None) -> None:
if self.scan_in_progress:
self.statusBar().showMessage("扫描中,请等待当前扫描完成。")
return
if self.operation_in_progress:
self.statusBar().showMessage("编辑计算中,请等待当前操作完成后再扫描。")
return
self.scan_in_progress = True
self.pending_scan_kind = scan_kind
self.pending_scan_context = dict(context or {})
self._clear_hover(render=True)
self._update_action_states()
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
self.scan_wait_cursor_active = True
thread = QThread(self)
worker = ScanWorker(action)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(self._finish_scan_task_result, Qt.ConnectionType.QueuedConnection)
worker.failed.connect(self._fail_scan_task_result, Qt.ConnectionType.QueuedConnection)
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 _run_scan_task_sync(self, action) -> None:
if not self.scan_in_progress:
return
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
QApplication.processEvents()
try:
result = action()
except Exception as exc:
self._fail_scan_task_result(str(exc))
else:
self._finish_scan_task_result(result)
finally:
QApplication.restoreOverrideCursor()
@Slot(object)
def _finish_scan_task_result(self, result: object) -> None:
scan_kind = self.pending_scan_kind
context = dict(self.pending_scan_context or {})
if scan_kind == "editable":
self._finish_editable_scan(
result,
bool(context.get("show_info", True)),
str(context.get("scan_label", "扫描")),
int(context.get("limit", 0)),
)
return
if scan_kind == "cylinder":
self._finish_cylinder_scan(result, bool(context.get("show_info", True)))
return
self._end_scan_task()
self.statusBar().showMessage("扫描已完成,但扫描类型已丢失。")
@Slot(str)
def _fail_scan_task_result(self, message: str) -> None:
scan_kind = self.pending_scan_kind
if scan_kind == "editable":
self._fail_editable_scan(message)
return
if scan_kind == "cylinder":
self._fail_cylinder_scan(message)
return
self._end_scan_task()
QMessageBox.critical(self, "扫描失败", message)
self.statusBar().showMessage("扫描失败")
def _end_scan_task(self) -> None:
if getattr(self, "scan_wait_cursor_active", False):
try:
QApplication.restoreOverrideCursor()
except Exception:
pass
self.scan_wait_cursor_active = False
self.scan_in_progress = False
self.pending_scan_kind = None
self.pending_scan_context = None
self._update_action_states()
self._request_thread_quit(self.scan_thread)
def _forget_scan_thread(self) -> None:
self.scan_thread = None
self.scan_worker = None
def list_cylinders(self) -> None:
self.refresh_cylinder_candidates(show_info=True)
def _clear_editable_candidates(self) -> None:
if hasattr(self, "editable_table"):
self.editable_table.setRowCount(0)
def refresh_editable_candidates(self, show_info: bool = True, deep_scan: bool = False) -> None:
if self.model is None:
return
if self._edit_busy("编辑计算中,暂时不能扫描可编辑对象。"):
return
limit = 48 if deep_scan else 24
max_scan_faces = 1600 if deep_scan else 700
max_scan_edges = 2500 if deep_scan else 1200
scan_label = "深度扫描" if deep_scan else "扫描"
self.statusBar().showMessage(f"正在{scan_label}可编辑对象...")
def action():
if self.model is None:
return []
return self.model.editable_feature_candidates(
limit=limit,
max_scan_faces=max_scan_faces,
max_scan_edges=max_scan_edges,
)
self._run_scan_task(
"editable",
action,
{
"show_info": show_info,
"scan_label": scan_label,
"limit": limit,
},
)
def _finish_editable_scan(self, result: object, show_info: bool, scan_label: str, limit: int) -> None:
try:
candidates = list(result) if isinstance(result, (list, tuple)) else []
self._populate_editable_candidates(candidates, show_info, scan_label, limit)
finally:
self._end_scan_task()
def _fail_editable_scan(self, message: str) -> None:
self._end_scan_task()
QMessageBox.critical(self, "扫描失败", message)
self.statusBar().showMessage("可编辑对象扫描失败")
def _populate_editable_candidates(
self,
candidates: list[dict[str, object]],
show_info: bool,
scan_label: str,
limit: int,
) -> None:
if not hasattr(self, "editable_table"):
if show_info:
self.set_plain_info("可编辑对象面板当前未启用。")
return
self.editable_table.setRowCount(len(candidates))
lines = [f"可编辑对象:显示 {len(candidates)} 个({scan_label}上限 {limit}"]
for row, item in enumerate(candidates):
target_kind = str(item.get("target_kind", "face"))
target_id = int(item.get("target_id", item.get("face_id", item.get("edge_id", -1))))
operation_item = QTableWidgetItem(str(item["operation"]))
operation_item.setData(EDITABLE_TARGET_ID_ROLE, target_id)
operation_item.setData(EDITABLE_TARGET_KIND_ROLE, target_kind)
operation_item.setData(EDITABLE_ACTION_ROLE, str(item["operation_key"]))
id_item = QTableWidgetItem(f"{target_kind} {target_id}")
object_item = QTableWidgetItem(str(item["feature_guess"]))
current_item = QTableWidgetItem(
f"{item['current_value_label']}={_format_value(item['current_value'])}"
)
status_item = QTableWidgetItem(str(item["status"]))
risk_item = QTableWidgetItem(str(item["risk"]))
confidence_item = QTableWidgetItem(str(item["confidence"]))
note_item = QTableWidgetItem(str(item["note"]))
row_items = [
operation_item,
id_item,
object_item,
current_item,
status_item,
risk_item,
confidence_item,
note_item,
]
for column, table_item in enumerate(row_items):
table_item.setToolTip(table_item.text())
self.editable_table.setItem(row, column, table_item)
lines.append(
f"{target_kind} {target_id}: {item['operation']}, "
f"零件={item['part_id']}, Solid={item['solid_id']}, "
f"object={item['feature_guess']}, "
f"{item['current_value_label']}={_format_value(item['current_value'])}, "
f"status={item['status']}, risk={item['risk']}, "
f"confidence={item['confidence']}, note={item['note']}"
)
self.editable_table.resizeColumnsToContents()
self.statusBar().showMessage(f"已{scan_label}可编辑对象:{len(candidates)} 个")
if show_info:
self.set_plain_info("\n".join(lines))
def _clear_cylinder_candidates(self) -> None:
self.cylinder_candidate_cache = []
self.cylinder_candidates_loaded = False
if hasattr(self, "cylinder_table"):
self.cylinder_table.setRowCount(0)
def refresh_cylinder_candidates(self, show_info: bool = True) -> None:
if self.model is None:
return
if self._edit_busy("编辑计算中,暂时不能扫描圆柱面候选。"):
return
self.statusBar().showMessage("正在扫描圆柱面候选...")
def action():
if self.model is None:
return []
return self.model.cylindrical_feature_candidates(limit=60, max_scan_faces=1200)
self._run_scan_task(
"cylinder",
action,
{"show_info": show_info},
)
def _finish_cylinder_scan(self, result: object, show_info: bool) -> None:
try:
self.cylinder_candidate_cache = list(result) if isinstance(result, (list, tuple)) else []
self.cylinder_candidates_loaded = True
self._filter_cached_cylinder_candidates(show_info=show_info)
finally:
self._end_scan_task()
def _fail_cylinder_scan(self, message: str) -> None:
self._end_scan_task()
self._clear_cylinder_candidates()
QMessageBox.critical(self, "扫描失败", message)
self.statusBar().showMessage("圆柱面候选扫描失败")
def _filter_cached_cylinder_candidates(self, show_info: bool = False) -> None:
if not hasattr(self, "cylinder_table"):
if show_info:
self.set_plain_info("圆柱面候选面板当前未启用。")
return
if not self.cylinder_candidates_loaded:
self.cylinder_table.setRowCount(0)
if show_info:
self.set_plain_info("圆柱面候选尚未扫描。点击 `扫描圆柱面` 后再切换筛选类型。")
return
candidates = [
item
for item in self.cylinder_candidate_cache
if self._candidate_matches_filter(str(item["feature_guess"]))
]
self._populate_cylinder_table(candidates, show_info=show_info)
def _populate_cylinder_table(self, candidates: list[dict[str, object]], show_info: bool = True) -> None:
if not hasattr(self, "cylinder_table"):
if show_info:
self.set_plain_info("圆柱面候选面板当前未启用。")
return
self.cylinder_table.setRowCount(len(candidates))
lines = [f"圆柱面候选:显示 {len(candidates)} 个"]
for row, item in enumerate(candidates):
face_item = QTableWidgetItem(str(item["face_id"]))
face_item.setData(Qt.UserRole, int(item["face_id"]))
guess_item = QTableWidgetItem(str(item["feature_guess"]))
diameter_item = QTableWidgetItem(_format_float(float(item["diameter"])))
span_item = QTableWidgetItem(_format_float(float(item["angular_span"])))
height_item = QTableWidgetItem(_format_float(float(item["height_estimate"])))
confidence_item = QTableWidgetItem(str(item["confidence"]))
risk_item = QTableWidgetItem(str(item["resize_risk"]))
part_item = QTableWidgetItem(str(item["part_id"]))
self.cylinder_table.setItem(row, 0, face_item)
self.cylinder_table.setItem(row, 1, guess_item)
self.cylinder_table.setItem(row, 2, diameter_item)
self.cylinder_table.setItem(row, 3, span_item)
self.cylinder_table.setItem(row, 4, height_item)
self.cylinder_table.setItem(row, 5, confidence_item)
self.cylinder_table.setItem(row, 6, risk_item)
self.cylinder_table.setItem(row, 7, part_item)
lines.append(
f"Face {item['face_id']}: 零件 {item['part_id']}, "
f"guess={item['feature_guess']}, "
f"diameter={_format_float(float(item['diameter']))}, "
f"height={_format_float(float(item['height_estimate']))}, "
f"confidence={item['confidence']}, "
f"risk={item['resize_risk']}"
)
self.cylinder_table.resizeColumnsToContents()
self.statusBar().showMessage(f"已显示圆柱面候选:{len(candidates)} 个")
if show_info:
self.set_plain_info("\n".join(lines))
def _candidate_matches_filter(self, feature_guess: str) -> bool:
if not hasattr(self, "candidate_filter_combo"):
return True
current = self.candidate_filter_combo.currentText()
if current == "All":
return True
if current == "Hole/Groove":
return feature_guess == "hole/groove candidate"
if current == "Round/Fillet":
return feature_guess == "round/fillet candidate"
if current == "Boss/Outer":
return feature_guess == "boss/outer-round candidate"
if current == "Unclear":
return feature_guess == "cylindrical face"
return True
def _run_action(self, action, success_message: str | None) -> None:
try:
result = action()
except Exception as exc:
QMessageBox.critical(self, "操作失败", str(exc))
self.statusBar().showMessage("操作失败")
return
message = success_message or str(result)
self.statusBar().showMessage(message)
self.set_plain_info(message)
def _block_unisolated_high_risk_operation(self, title: str, plan: dict[str, object]) -> bool:
if str(plan.get("risk", "")) != "high":
return False
QMessageBox.information(
self,
title,
(
"该操作被稳定性保护阻止。\n\n"
"当前计划被判定为 high risk;这类 OCCT 布尔、倒圆或局部重建在复杂 STEP 上"
"可能不是普通失败,而是让进程卡死或直接退出。请先尝试更小的参数、修复模型、"
"选择更明确的面/边,或等后续子进程隔离执行通道实现后再开放。"
),
)
self.statusBar().showMessage("高风险操作已被稳定性保护阻止")
return True
def _run_edit_action(
self,
action,
operation_name: str,
target: str,
parameters: dict[str, object],
target_kind: str | None = None,
target_id: int | None = None,
) -> None:
if self.model is None:
return
if self.operation_in_progress:
self.statusBar().showMessage("后台编辑正在计算,请等待当前操作完成。")
return
target_logical_id = self._edit_target_logical_id(target_kind, target_id)
2026-07-29 15:43:28 +08:00
result_deflection = float(getattr(self, "edit_result_deflection", 1.6))
if operation_name == "推拉平面":
result_deflection = max(result_deflection, 2.4)
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(),
2026-07-29 15:43:28 +08:00
"edit_result_deflection": result_deflection,
}
self._begin_edit_task(operation_name)
self.pending_edit_context = context
thread = QThread(self)
worker = EditWorker(self._make_edit_job(action, context))
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(self._finish_edit_action, Qt.ConnectionType.QueuedConnection)
worker.failed.connect(self._fail_edit_action, Qt.ConnectionType.QueuedConnection)
thread.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(self._forget_edit_thread)
self.edit_thread = thread
self.edit_worker = worker
thread.start()
def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None:
if self.model is None or target_id is None or target_kind not in {"face", "feature"}:
return None
try:
numeric_id = int(target_id)
except (TypeError, ValueError):
return None
if 0 <= numeric_id < len(self.model.faces):
try:
return self.model.face_region_logical_id(numeric_id)
except Exception:
return None
return None
def _make_edit_job(self, action, context: dict[str, object]):
def job():
if self.model is None:
raise RuntimeError("Model is not loaded.")
snapshot = self.model.snapshot()
target_part_id = self._edit_context_part_id(context)
before_stats = self.model.stats()
before_part_stats = self._part_stats_or_none(target_part_id)
2026-07-29 15:43:28 +08:00
before_geometry = {}
try:
result = action()
after_snapshot = self.model.snapshot()
after_stats = self.model.stats()
after_part_stats = self._part_stats_or_none(target_part_id)
2026-07-29 15:43:28 +08:00
after_geometry = {}
except Exception as exc:
try:
self.model.restore_snapshot(snapshot)
except Exception as rollback_exc:
raise RuntimeError(
f"编辑失败,且回滚到操作前状态也失败:{rollback_exc}\n原始错误:{exc}"
) from exc
raise RuntimeError(f"编辑失败,模型已恢复到操作前状态:{exc}") from exc
model_polydata = None
edge_polydata = None
try:
2026-07-29 15:43:28 +08:00
deflection = float(context.get("edit_result_deflection", 1.6))
model_polydata = self.model.build_face_polydata(deflection=deflection)
edge_polydata = self.model.build_edge_polydata(
2026-07-29 15:43:28 +08:00
deflection=deflection,
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False))
)
except Exception:
model_polydata = None
edge_polydata = None
return {
"message": str(result),
"snapshot": snapshot,
"before_stats": before_stats,
"before_part_stats": before_part_stats,
"before_geometry": before_geometry,
"after_snapshot": after_snapshot,
"after_stats": after_stats,
"after_part_stats": after_part_stats,
"quality_warnings": _edit_quality_warnings(before_part_stats, after_part_stats),
"after_geometry": after_geometry,
"model_polydata": model_polydata,
"edge_polydata": edge_polydata,
}
return job
def _edit_context_part_id(self, context: dict[str, object]) -> int | None:
if self.model is None:
return None
parameters = context.get("parameters")
if isinstance(parameters, dict):
part_id = parameters.get("part_id")
if part_id not in {"", None}:
try:
return int(part_id)
except (TypeError, ValueError):
pass
target_kind = context.get("target_kind")
target_id = context.get("target_id")
if target_id is None:
return None
try:
numeric_id = int(target_id)
except (TypeError, ValueError):
return None
if target_kind == "part":
return numeric_id
if target_kind in {"face", "feature"} and 0 <= numeric_id < len(self.model.face_part_ids):
return int(self.model.face_part_ids[numeric_id])
if target_kind == "edge" and 0 <= numeric_id < len(self.model.edge_part_ids):
return int(self.model.edge_part_ids[numeric_id])
if target_kind == "solid" and 0 <= numeric_id < len(self.model.solids):
return int(self.model.solids[numeric_id][0])
return None
def _part_stats_or_none(self, part_id: int | None):
if self.model is None or part_id is None:
return None
try:
return self.model.part_topology_stats(part_id)
except Exception:
return None
def _begin_edit_task(self, operation_name: str) -> None:
self.operation_in_progress = True
self._update_action_states()
if "推拉" in operation_name:
progress_text = f"{operation_name} 正在后台计算,半透明预览会持续渲染,3D 视图仍可旋转查看。"
elif "孔径" in operation_name:
progress_text = f"{operation_name} 正在后台计算;为避免复杂模型卡顿,已跳过同步红/绿预览。"
elif "圆柱尺寸缩放所属对象" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会均匀缩放所属特征或 Solid,其它尺寸也会跟随变化。"
elif "槽/半孔宽度" in operation_name:
progress_text = f"{operation_name} 正在后台计算;槽宽会在后台换算并重建孔/槽几何。"
2026-07-29 15:43:28 +08:00
elif "槽/半孔深度" in operation_name:
progress_text = f"{operation_name} 正在后台计算;槽深会在后台换算并重建孔/槽几何。"
elif "槽/半孔圆弧长度" in operation_name:
progress_text = f"{operation_name} 正在后台计算;圆弧长度会在后台换算并重建孔/槽几何。"
elif "凸台高度" in operation_name:
progress_text = f"{operation_name} 正在后台计算,预览表示将推拉的凸台端盖范围。"
elif "凸台" in operation_name:
progress_text = f"{operation_name} 正在后台计算,绿色预览表示扩大补料范围,红色预览表示缩小切削范围。"
elif "封堵" in operation_name:
progress_text = f"{operation_name} 正在后台计算,绿色预览表示封堵补料范围。"
elif "孔深" in operation_name:
progress_text = f"{operation_name} 正在后台计算;为避免复杂模型卡顿,已跳过同步孔深预览。"
elif "圆角" in operation_name:
if "已有" in operation_name:
progress_text = f"{operation_name} 正在后台计算,蓝色预览表示将移除并重建的已有圆角面。"
else:
progress_text = f"{operation_name} 正在后台计算,蓝色预览表示目标边和半径范围。"
elif "倒角" in operation_name:
progress_text = f"{operation_name} 正在后台计算,橙色预览表示目标边和倒角距离范围。"
elif "边长" in operation_name or "Edge长度" in operation_name:
progress_text = f"{operation_name} 正在后台计算,预览表示局部边形变、端面推拉或后备策略后的目标范围。"
elif "只缩放当前Face面积" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会在面内移动所选 Face 顶点并重建相邻平面。"
elif "面宽(整体)" in operation_name or "面高(整体)" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会沿选中 Face 的平面内方向缩放所属特征或 Solid,其它几何会跟随变化。"
elif "面宽(当前面)" in operation_name or "面高(当前面)" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会只沿选中 Face 的一个平面内方向缩放顶点并重建相邻平面。"
elif "面偏移(当前面)" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会沿平面方向移动所选 Face 顶点并重建相邻平面。"
elif "面偏移(整体)" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会沿平面方向平移所属特征或 Solid,不推拉当前面。"
elif "Face中心" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会平移所属特征或 Solid,不做单面局部扭曲。"
elif "只移动当前Face" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会移动所选 Face 顶点并重建相邻平面。"
elif "按轴心平移" in operation_name:
progress_text = f"{operation_name} 正在后台计算;当前会平移所属特征或 Solid,不重切孔/槽/凸台。"
elif "平移" in operation_name:
progress_text = f"{operation_name} 正在后台计算,完成后会刷新模型位置和拓扑索引。"
elif "旋转" in operation_name:
progress_text = f"{operation_name} 正在后台计算,完成后会刷新模型姿态和拓扑索引。"
else:
progress_text = f"{operation_name} 正在后台计算。"
2026-07-29 15:43:28 +08:00
self._start_edit_busy_status(progress_text)
if self.edit_preview_timer is not None:
self.edit_preview_timer.stop()
QApplication.processEvents()
2026-07-29 15:43:28 +08:00
def _start_edit_busy_status(self, text: str) -> None:
self.edit_busy_base_text = text
self.edit_busy_started_at = datetime.now()
self.edit_busy_phase = 0
self.statusBar().showMessage(text)
if hasattr(self, "edit_busy_timer"):
self.edit_busy_timer.start(350)
def _pulse_edit_busy_status(self) -> None:
if not self.operation_in_progress or not getattr(self, "edit_busy_base_text", ""):
if hasattr(self, "edit_busy_timer"):
self.edit_busy_timer.stop()
return
self.edit_busy_phase = int(getattr(self, "edit_busy_phase", 0)) + 1
elapsed = ""
if getattr(self, "edit_busy_started_at", None) is not None:
seconds = max(0.0, (datetime.now() - self.edit_busy_started_at).total_seconds())
elapsed = f" {seconds:.1f}s"
dots = "." * (self.edit_busy_phase % 4)
self.statusBar().showMessage(f"{self.edit_busy_base_text}{dots}{elapsed}")
def _stop_edit_busy_status(self) -> None:
if hasattr(self, "edit_busy_timer"):
self.edit_busy_timer.stop()
self.edit_busy_base_text = ""
self.edit_busy_started_at = None
self.edit_busy_phase = 0
def _set_edit_status_text(self, text: str) -> None:
self.statusBar().showMessage(text)
QApplication.processEvents()
@Slot(object)
def _finish_edit_action(self, result: object) -> None:
if hasattr(self, "_is_ui_thread") and not self._is_ui_thread():
self._invoke_on_ui_thread(lambda result=result: self._finish_edit_action(result))
return
context = self.pending_edit_context
if context is None:
self._end_edit_task(clear_preview=True)
self.statusBar().showMessage("编辑已完成,但上下文丢失")
return
if self.model is None:
self._end_edit_task(clear_preview=True)
return
self._set_edit_status_text("布尔计算已完成,正在刷新模型显示和历史记录...")
if not isinstance(result, dict):
self._end_edit_task(clear_preview=True)
QMessageBox.critical(self, "操作失败", "后台编辑返回了无法识别的结果。")
self.statusBar().showMessage("编辑结果无法识别")
return
message = str(result["message"])
try:
record = self._make_operation_record(
operation_name=str(context["operation_name"]),
target=str(context["target"]),
parameters=dict(context["parameters"]),
result_message=message,
before_stats=result["before_stats"],
after_stats=result["after_stats"],
before_part_stats=result.get("before_part_stats"),
after_part_stats=result.get("after_part_stats"),
quality_warnings=list(result.get("quality_warnings", [])),
before_geometry=dict(result["before_geometry"]),
after_geometry=dict(result["after_geometry"]),
target_kind=context["target_kind"],
target_id=context["target_id"],
target_logical_id=context.get("target_logical_id"),
pick_position=context["pick_position"],
before_snapshot=result["snapshot"],
after_snapshot=result["after_snapshot"],
)
model_polydata = result.get("model_polydata")
edge_polydata = result.get("edge_polydata")
if model_polydata is None or edge_polydata is None:
2026-07-29 15:43:28 +08:00
deflection = float(context.get("edit_result_deflection", 1.6))
model_polydata = self.model.build_face_polydata(deflection=deflection)
edge_polydata = self.model.build_edge_polydata(
2026-07-29 15:43:28 +08:00
deflection=deflection,
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False))
)
self.clear_edit_preview(render=False)
self._populate_part_tree()
self._rebuild_scene_from_polydata(model_polydata, edge_polydata, reset_camera=False)
locator_note = self._locate_operation_record(record)
except Exception as exc:
rollback_message = self._restore_failed_edit_snapshot(result.get("snapshot") if isinstance(result, dict) else None)
self._end_edit_task(clear_preview=True)
QMessageBox.critical(self, "操作失败", f"编辑结果刷新失败:{exc}\n\n{rollback_message}")
self.statusBar().showMessage("编辑已完成,但刷新结果时失败")
return
self.undo_stack.append(result["snapshot"])
self.redo_stack.clear()
self.operation_history.append(record)
self.redo_history.clear()
self._clear_editable_candidates()
self._clear_cylinder_candidates()
self._refresh_history_list()
self._end_edit_task(clear_preview=False)
if result.get("quality_warnings"):
self.statusBar().showMessage("编辑完成,但有质量警告,请查看操作历史详情")
else:
selection_note = ";已保持当前选择" if self.selected_kind is not None else ""
self.statusBar().showMessage(f"{message}{selection_note}")
if self.selected_kind is None:
self.set_plain_info(f"{record.detail}\n\n{locator_note}")
@Slot(str)
def _fail_edit_action(self, message: str) -> None:
if hasattr(self, "_is_ui_thread") and not self._is_ui_thread():
self._invoke_on_ui_thread(lambda message=message: self._fail_edit_action(message))
return
self._end_edit_task(clear_preview=True)
QMessageBox.critical(self, "操作失败", message)
self._clear_editable_candidates()
self._clear_cylinder_candidates()
self.statusBar().showMessage("操作失败,模型已保持在编辑前状态")
def _restore_failed_edit_snapshot(self, snapshot: object) -> str:
if self.model is None or not isinstance(snapshot, dict):
return "未找到可用的编辑前快照,请重新加载 STEP 文件确认状态。"
try:
self.model.restore_snapshot(snapshot)
self._reset_selection()
self._populate_part_tree()
self._rebuild_scene(reset_camera=False)
self._clear_editable_candidates()
self._clear_cylinder_candidates()
except Exception as rollback_exc:
return f"尝试回滚到编辑前状态失败:{rollback_exc}。建议重新加载 STEP 文件。"
return "模型已回滚到编辑前状态。"
def _end_edit_task(self, clear_preview: bool = True) -> None:
self.operation_in_progress = False
2026-07-29 15:43:28 +08:00
self._stop_edit_busy_status()
self.pending_edit_context = None
if clear_preview:
self.clear_edit_preview(render=False)
self._update_action_states()
2026-07-29 15:43:28 +08:00
if self.selected_kind is not None and getattr(self, "current_info_values", None):
self._refresh_property_editor()
self._request_thread_quit(self.edit_thread)
QApplication.processEvents()
def _forget_edit_thread(self) -> None:
self.edit_thread = None
self.edit_worker = None
def _make_operation_record(
self,
operation_name: str,
target: str,
parameters: dict[str, object],
result_message: str,
before_stats,
after_stats,
before_geometry: dict[str, object],
after_geometry: dict[str, object],
before_part_stats=None,
after_part_stats=None,
quality_warnings: list[str] | None = None,
target_kind: str | None = None,
target_id: int | None = None,
target_logical_id: int | None = None,
pick_position: tuple[float, float, float] | None = None,
before_snapshot: dict[int, object] | None = None,
after_snapshot: dict[int, object] | None = None,
) -> OperationRecord:
target_summary = target
if target_kind in {"face", "feature"} and target_logical_id is not None:
target_summary = f"{target_kind} logical {target_logical_id}"
if target_id is not None and target_id != target_logical_id:
target_summary += f" (topology {target_id})"
summary_parts = [operation_name, target_summary]
if "distance" in parameters:
summary_parts.append(f"distance={_format_value(parameters['distance'])}")
if "semantic_distance" in parameters:
summary_parts.append(f"distance={_format_value(parameters['semantic_distance'])}")
2026-07-29 15:43:28 +08:00
if parameters.get("chamfer_distance1") is not None and parameters.get("chamfer_distance2") is not None:
summary_parts.append(
f"chamfer_d1_d2={_format_value(parameters['chamfer_distance1'])},"
f"{_format_value(parameters['chamfer_distance2'])}"
)
elif parameters.get("chamfer_distance") is not None:
summary_parts.append(f"chamfer={_format_value(parameters['chamfer_distance'])}")
if parameters.get("chamfer_angle_degrees") is not None:
summary_parts.append(f"chamfer_angle={_format_value(parameters['chamfer_angle_degrees'])}deg")
if parameters.get("target_edge_center") is not None:
summary_parts.append(f"edge_center={_format_value(parameters['target_edge_center'])}")
if parameters.get("target_endpoint_point") is not None:
summary_parts.append(f"edge_endpoint={_format_value(parameters['target_endpoint_point'])}")
if parameters.get("target_face_center") is not None:
summary_parts.append(f"face_center={_format_value(parameters['target_face_center'])}")
if parameters.get("target_axis_center") is not None:
summary_parts.append(f"axis_center={_format_value(parameters['target_axis_center'])}")
if parameters.get("target_plane_position") is not None:
summary_parts.append(f"plane_offset={_format_value(parameters['target_plane_position'])}")
if parameters.get("target_area") is not None:
summary_parts.append(f"area={_format_value(parameters['target_area'])}")
if "new_diameter" in parameters:
summary_parts.append(f"diameter={_format_value(parameters['new_diameter'])}")
if parameters.get("target_length") is not None:
summary_parts.append(f"target_length={_format_value(parameters['target_length'])}")
2026-07-29 15:43:28 +08:00
if parameters.get("new_slot_total_length") is not None:
summary_parts.append(f"slot_total_length={_format_value(parameters['new_slot_total_length'])}")
if parameters.get("new_slot_center_distance") is not None:
summary_parts.append(f"slot_center_distance={_format_value(parameters['new_slot_center_distance'])}")
if parameters.get("ellipse_target_radius") is not None:
label = parameters.get("ellipse_axis_kind") or "axis"
summary_parts.append(f"ellipse_{label}_radius={_format_value(parameters['ellipse_target_radius'])}")
2026-07-29 15:43:28 +08:00
if parameters.get("new_slot_angular_span_degrees") is not None:
summary_parts.append(f"slot_angle={_format_value(parameters['new_slot_angular_span_degrees'])}deg")
if parameters.get("target_bbox_diagonal") is not None:
summary_parts.append(f"bbox_diagonal={_format_value(parameters['target_bbox_diagonal'])}")
if parameters.get("target_axis_size") is not None:
axis = parameters.get("scale_axis") or "axis"
summary_parts.append(f"{axis}_size={_format_value(parameters['target_axis_size'])}")
if parameters.get("resize_strategy"):
summary_parts.append(f"strategy={parameters['resize_strategy']}")
actual_edge_length = _message_field(result_message, "nearest_length")
target_error = _message_field(result_message, "target_error")
if actual_edge_length is not None:
summary_parts.append(f"actual_length={actual_edge_length}")
if target_error is not None:
summary_parts.append(f"target_error={target_error}")
summary = " | ".join(summary_parts)
lines = [
f"operation: {operation_name}",
f"target: {target}",
f"target_kind: {target_kind or ''}",
f"target_id: {target_id if target_id is not None else ''}",
"parameters:",
]
if target_logical_id is not None:
lines.insert(4, f"target_logical_id: {target_logical_id}")
if target_id is not None and target_id != target_logical_id:
lines.insert(5, f"target_topological_id_at_operation: {target_id}")
if pick_position is not None:
insert_at = 6 if target_logical_id is not None and target_id is not None and target_id != target_logical_id else 5 if target_logical_id is not None else 4
lines.insert(insert_at, f"pick_position: {_format_value(pick_position)}")
for key, value in parameters.items():
lines.append(f" {key}: {_format_value(value)}")
if before_part_stats is not None and after_part_stats is not None:
lines.extend(
[
"target part topology before:",
f" solids: {before_part_stats.solids}",
f" faces: {before_part_stats.faces}",
f" edges: {before_part_stats.edges}",
"target part topology after:",
f" solids: {after_part_stats.solids} ({_signed_delta(after_part_stats.solids - before_part_stats.solids)})",
f" faces: {after_part_stats.faces} ({_signed_delta(after_part_stats.faces - before_part_stats.faces)})",
f" edges: {after_part_stats.edges} ({_signed_delta(after_part_stats.edges - before_part_stats.edges)})",
]
)
if quality_warnings:
lines.append("quality warnings:")
for warning in quality_warnings:
lines.append(f" {warning}")
lines.extend(
[
"topology before:",
f" solids: {before_stats.solids}",
f" faces: {before_stats.faces}",
f" edges: {before_stats.edges}",
"topology after:",
f" solids: {after_stats.solids} ({_signed_delta(after_stats.solids - before_stats.solids)})",
f" faces: {after_stats.faces} ({_signed_delta(after_stats.faces - before_stats.faces)})",
f" edges: {after_stats.edges} ({_signed_delta(after_stats.edges - before_stats.edges)})",
2026-07-29 15:43:28 +08:00
"result:",
f" {result_message}",
]
)
if before_geometry or after_geometry:
insert_at = max(len(lines) - 2, 0)
lines[insert_at:insert_at] = [
"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')}",
]
2026-07-29 15:43:28 +08:00
else:
lines.insert(max(len(lines) - 2, 0), "geometry: skipped during edit for faster UI feedback; use diff preview/report when needed.")
return OperationRecord(
summary=summary,
detail="\n".join(lines),
operation_name=operation_name,
target=target,
parameters=dict(parameters),
result_message=result_message,
target_kind=target_kind,
target_id=target_id,
target_logical_id=target_logical_id,
pick_position=pick_position,
before_snapshot=before_snapshot,
after_snapshot=after_snapshot,
)