8404 lines
404 KiB
Python
8404 lines
404 KiB
Python
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
import json
|
||
import math
|
||
from pathlib import Path
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
|
||
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 .geometry_utils import (
|
||
_tuple_add,
|
||
_tuple_dot,
|
||
_tuple_normalized,
|
||
_tuple_or_none,
|
||
_tuple_scale,
|
||
_tuple_sub,
|
||
_vector_length,
|
||
)
|
||
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)
|
||
|
||
|
||
def _compact_plan_value(value: object) -> str:
|
||
text = _format_value(value)
|
||
return text if len(text) <= 120 else text[:117] + "..."
|
||
|
||
|
||
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
|
||
plan = self._push_pull_plan_for_action(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 ""
|
||
isolation_line = (
|
||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if str(plan.get("risk")) == "high"
|
||
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"
|
||
f"{isolation_line}"
|
||
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消拉伸/切除平面")
|
||
return
|
||
isolation = self._isolation_for_plan(plan, "push_pull_face", [face_id, distance])
|
||
if isolation is None:
|
||
self._show_push_pull_preview(face_id, distance, plan=plan)
|
||
else:
|
||
self.clear_edit_preview(render=False)
|
||
|
||
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"),
|
||
"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"),
|
||
"push_pull_inward_material_depth": plan.get("push_pull_inward_material_depth"),
|
||
"push_pull_inward_cut_ratio": plan.get("push_pull_inward_cut_ratio"),
|
||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||
"selected_boundary_wires": plan.get("selected_boundary_wires"),
|
||
"selected_inner_boundary_wires": plan.get("selected_inner_boundary_wires"),
|
||
"selected_has_inner_boundaries": plan.get("selected_has_inner_boundaries"),
|
||
"same_domain_face_count": plan.get("same_domain_face_count"),
|
||
"first_level_boundary_edge_count": plan.get("first_level_boundary_edge_count"),
|
||
"first_level_boundary_vertex_count": plan.get("first_level_boundary_vertex_count"),
|
||
"first_level_adjacent_face_count": plan.get("first_level_adjacent_face_count"),
|
||
"topology_relation_status": plan.get("topology_relation_status"),
|
||
"topology_ignored_relation_note": plan.get("topology_ignored_relation_note"),
|
||
"first_level_topology_note": plan.get("first_level_topology_note"),
|
||
"planar_cap_extension_kind": plan.get("planar_cap_extension_kind"),
|
||
"planar_cap_extension_method": plan.get("planar_cap_extension_method"),
|
||
"planar_cap_boundary_edge_count": plan.get("planar_cap_boundary_edge_count"),
|
||
"planar_cap_inner_boundary_wires": plan.get("planar_cap_inner_boundary_wires"),
|
||
"planar_cap_adjacent_face_count": plan.get("planar_cap_adjacent_face_count"),
|
||
"cylindrical_cap_extension_kind": plan.get("cylindrical_cap_extension_kind"),
|
||
"cylindrical_cap_extension_method": plan.get("cylindrical_cap_extension_method"),
|
||
"cap_extra_adjacent_face_count": plan.get("cap_extra_adjacent_face_count"),
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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": "当前选中的面不是平面,不能移动这个面。",
|
||
"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("normal")
|
||
if not isinstance(outward, (tuple, list)) or len(outward) != 3:
|
||
return {
|
||
"status": "blocked",
|
||
"risk": "blocked",
|
||
"message": "当前平面缺少稳定移动方向,请重新选择这个面后再试。",
|
||
"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] = []
|
||
blockers: list[str] = []
|
||
if distance_abs <= 1e-9:
|
||
return {
|
||
"status": "blocked",
|
||
"risk": "blocked",
|
||
"message": "面移动距离为 0,不需要修改。",
|
||
"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("面移动距离相对当前面尺寸较大,布尔失败或大范围变形风险较高。")
|
||
elif distance_abs > bbox_diagonal * 0.08:
|
||
risk = "medium"
|
||
status = "caution"
|
||
warnings.append("面移动距离相对当前面尺寸偏大,请确认预览范围。")
|
||
|
||
outward_tuple = tuple(float(item) for item in outward)
|
||
inward_material_depth = _float_or_none(info.get("push_pull_inward_material_depth"))
|
||
inward_cut_ratio = None
|
||
if distance < 0:
|
||
if inward_material_depth is not None and inward_material_depth > 1e-9:
|
||
inward_cut_ratio = distance_abs / inward_material_depth
|
||
depth_tolerance = max(
|
||
inward_material_depth * 1e-5,
|
||
(bbox_diagonal or 0.0) * 1e-7,
|
||
1e-6,
|
||
)
|
||
if distance_abs >= inward_material_depth - depth_tolerance:
|
||
status = "blocked"
|
||
risk = "blocked"
|
||
blockers.append("向内切削距离达到或超过当前面背后的材料厚度;继续执行很可能把实体切空或生成无效几何。")
|
||
elif inward_cut_ratio >= 0.85:
|
||
risk = "high"
|
||
warnings.append("向内切削距离已经接近当前面背后的材料厚度,剩余壁厚很薄,请谨慎确认。")
|
||
elif inward_cut_ratio >= 0.6 and risk != "high":
|
||
risk = "medium"
|
||
warnings.append("向内切削距离超过当前面背后材料厚度的 60%,请确认不会切穿。")
|
||
else:
|
||
if risk == "low":
|
||
risk = "medium"
|
||
warnings.append("向内切除的材料厚度尚未缓存;完整切穿检查会放到后台计算。")
|
||
model_face_count = len(self.model.faces) if self.model is not None else 0
|
||
inner_wires = int(info.get("inner_boundary_wires", 0) or 0)
|
||
shallow_complex_cap_rebuild = (
|
||
inward_cut_ratio is not None
|
||
and inward_cut_ratio < 0.6
|
||
and inner_wires > 0
|
||
)
|
||
complex_multi_boundary_cap = status != "blocked" and model_face_count > 600 and inner_wires > 1
|
||
if complex_multi_boundary_cap and inward_cut_ratio is None:
|
||
risk = self._max_quick_risk(risk, "high")
|
||
warnings.append(
|
||
"当前是复杂大模型里的多内孔/多边界平面端盖,快速缓存里没有完整材料厚度;"
|
||
"将交给后台/隔离子进程重新计算一级边界侧壁重建计划。若发现接近切穿或需要二级关系传播,"
|
||
"模型层会快速阻止并保持原模型不变。"
|
||
)
|
||
elif complex_multi_boundary_cap and not shallow_complex_cap_rebuild:
|
||
status = "blocked"
|
||
risk = "blocked"
|
||
boundary_edges = _compact_plan_value(info.get("first_level_boundary_edge_count", "未知"))
|
||
adjacent_faces = _compact_plan_value(info.get("first_level_adjacent_face_count", "未知"))
|
||
blockers.append(
|
||
f"当前 Face {face_id} 是复杂大模型里的多内孔/多边界平面端盖:"
|
||
f"内边界 {inner_wires} 个,一级边界 Edge {boundary_edges} 条,"
|
||
f"共享边一级相邻 Face {adjacent_faces} 个。向内切除这类面需要判断孔壁、"
|
||
"槽底或台阶背后的二级关系是否一起变化;当前阶段只自动处理一级关系。"
|
||
"已在界面预检查阶段阻止,避免进入通用 OCCT 布尔后长时间卡住。"
|
||
)
|
||
elif complex_multi_boundary_cap and shallow_complex_cap_rebuild:
|
||
risk = self._max_quick_risk(risk, "medium")
|
||
warnings.append(
|
||
"当前是复杂大模型里的多内孔/多边界平面端盖,但本次向内切除没有接近切穿;"
|
||
"将交给后台/隔离子进程移动当前端面并重建一级边界侧壁,完成后校验目标位置和内边界。"
|
||
)
|
||
if status != "blocked" and risk in {"medium", "high"}:
|
||
status = "caution"
|
||
|
||
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 "可以尝试偏移该平面;完整几何检查会在后台执行。"
|
||
if blockers:
|
||
message = " ".join(blockers + warnings)
|
||
return {
|
||
"status": status,
|
||
"risk": risk,
|
||
"message": message,
|
||
"warnings": ";".join(warnings),
|
||
"blockers": ";".join(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,
|
||
"boundary_wires": info.get("boundary_wires"),
|
||
"inner_boundary_wires": info.get("inner_boundary_wires"),
|
||
"has_inner_boundaries": bool(info.get("has_inner_boundaries")),
|
||
"model_face_count": len(self.model.faces) if self.model is not None else None,
|
||
"push_pull_inward_material_depth": inward_material_depth,
|
||
"push_pull_inward_cut_ratio": inward_cut_ratio,
|
||
"outward_direction": outward_tuple,
|
||
"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 的法向移动平面区域;正向通常表示向外加料,负向表示向内切削。"
|
||
"相邻侧面会跟随重建,但这不是平移整个所属对象,也不是只改面积。"
|
||
),
|
||
"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": "使用当前显示/选中缓存生成快速预览;后台会重新计算真实拉伸/切除区域。",
|
||
"selected_boundary_wires": info.get("boundary_wires"),
|
||
"selected_inner_boundary_wires": info.get("inner_boundary_wires"),
|
||
"selected_has_inner_boundaries": bool(info.get("has_inner_boundaries")),
|
||
"same_domain_face_count": info.get("same_domain_face_count"),
|
||
"first_level_boundary_edge_count": info.get("first_level_boundary_edge_count"),
|
||
"first_level_boundary_vertex_count": info.get("first_level_boundary_vertex_count"),
|
||
"first_level_adjacent_face_count": info.get("first_level_adjacent_face_count"),
|
||
"topology_relation_status": info.get("topology_relation_status"),
|
||
"topology_ignored_relation_note": info.get("topology_ignored_relation_note"),
|
||
"first_level_topology_note": info.get("first_level_topology_note"),
|
||
"ui_quick_blocked_push_pull_plan": bool(
|
||
status == "blocked"
|
||
and distance < 0
|
||
and (len(self.model.faces) if self.model is not None else 0) > 600
|
||
and int(info.get("inner_boundary_wires", 0) or 0) > 1
|
||
),
|
||
}
|
||
|
||
def _should_defer_push_pull_model_plan(
|
||
self,
|
||
face_id: int,
|
||
distance: float,
|
||
quick_plan: dict[str, object],
|
||
) -> bool:
|
||
if self.model is None or quick_plan.get("status") == "blocked":
|
||
return False
|
||
if str(quick_plan.get("surface", "")) != "plane":
|
||
return False
|
||
try:
|
||
model_face_count = len(self.model.faces)
|
||
except Exception:
|
||
model_face_count = 0
|
||
if model_face_count < 600:
|
||
return False
|
||
|
||
inner_wires = int(quick_plan.get("inner_boundary_wires") or 0)
|
||
boundary_wires = int(quick_plan.get("boundary_wires") or 0)
|
||
if inner_wires <= 0 and boundary_wires <= 1 and not bool(quick_plan.get("has_inner_boundaries")):
|
||
return False
|
||
|
||
# Large STEP + holed planar caps are exactly where a full plan can spend
|
||
# seconds scanning topology before the actual isolated edit even starts.
|
||
return abs(float(distance)) > 1e-9
|
||
|
||
def _deferred_push_pull_model_plan(
|
||
self,
|
||
quick_plan: dict[str, object],
|
||
) -> dict[str, object]:
|
||
plan = dict(quick_plan)
|
||
warnings = [item for item in str(plan.get("warnings") or "").split(";") if item]
|
||
warnings.append(
|
||
"当前是复杂大模型里的多边界平面;完整几何计划将放到后台/隔离子进程里计算,避免主界面先卡住。"
|
||
)
|
||
plan["warnings"] = ";".join(warnings)
|
||
plan["risk"] = self._max_quick_risk(str(plan.get("risk") or "low"), "high")
|
||
if plan.get("status") != "blocked":
|
||
plan["status"] = "caution"
|
||
plan["ui_deferred_model_plan"] = True
|
||
plan["quick_plan_status"] = quick_plan.get("status")
|
||
plan["quick_plan_risk"] = quick_plan.get("risk")
|
||
plan["message"] = (
|
||
"为避免复杂 STEP 在界面线程生成完整拉伸/切除计划时卡顿,本次只做快速预检查;"
|
||
"真正的一级关系识别、风险判断、解析重建或快速阻止会在后台隔离进程中完成。"
|
||
)
|
||
plan.setdefault("edit_strategy_label", "后台计算完整拉伸/切除计划")
|
||
plan.setdefault(
|
||
"edit_semantics",
|
||
"界面先提交后台任务;子进程会按当前 Face 的一级拓扑关系决定是解析重建、局部重建还是阻止。",
|
||
)
|
||
return plan
|
||
|
||
def _push_pull_plan_for_action(self, face_id: int, distance: float) -> dict[str, object]:
|
||
quick_plan = self._quick_push_pull_plan(face_id, distance)
|
||
if quick_plan.get("status") == "blocked":
|
||
return quick_plan
|
||
if self.model is None:
|
||
return quick_plan
|
||
if self._should_defer_push_pull_model_plan(face_id, distance, quick_plan):
|
||
return self._deferred_push_pull_model_plan(quick_plan)
|
||
try:
|
||
model_plan = self.model.push_pull_plan(face_id, distance)
|
||
except Exception as exc:
|
||
blocked_plan = dict(quick_plan)
|
||
blocked_plan.update(
|
||
{
|
||
"status": "blocked",
|
||
"risk": "blocked",
|
||
"message": f"无法生成完整拉伸/切除计划:{exc}",
|
||
"model_plan_error": str(exc),
|
||
"quick_plan_status": quick_plan.get("status"),
|
||
"quick_plan_risk": quick_plan.get("risk"),
|
||
}
|
||
)
|
||
return blocked_plan
|
||
model_plan.setdefault("quick_plan_status", quick_plan.get("status"))
|
||
model_plan.setdefault("quick_plan_risk", quick_plan.get("risk"))
|
||
return model_plan
|
||
|
||
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._deferred_shell_thickness_plan_if_needed(face_id, target_thickness)
|
||
if plan is None:
|
||
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"当前 Face 拉伸/切除距离: {_format_value(plan.get('push_pull_distance', ''))}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
+ (
|
||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if str(plan.get("risk")) == "high"
|
||
else ""
|
||
)
|
||
+ "当前版本会移动当前平面区域,让它和相对平面的距离接近目标厚度;这不是 CAD 壳命令参数编辑。确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消壳体厚度调整")
|
||
return
|
||
|
||
isolation = self._isolation_for_plan(plan, "resize_shell_thickness", [face_id, target_thickness])
|
||
if isolation is not None:
|
||
self.clear_edit_preview(render=False)
|
||
else:
|
||
self._show_shell_thickness_preview(face_id, target_thickness, plan=plan)
|
||
|
||
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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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"重建模式: {_format_value(plan.get('owning_shell_thickness_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"相对平面 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"),
|
||
"owning_shell_thickness_rebuild_mode": plan.get("owning_shell_thickness_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"),
|
||
"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,
|
||
isolation=self._isolation_for_plan(plan, "resize_shell_thickness_owning_scale", [face_id, target_thickness]),
|
||
)
|
||
|
||
@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.quick_face_info(selected_face_id))
|
||
except Exception:
|
||
pass
|
||
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 _large_complex_local_face_blocker(self, face_id: int, info: dict[str, object]) -> str:
|
||
if self.model is None:
|
||
return ""
|
||
try:
|
||
model_face_count = len(self.model.faces)
|
||
except Exception:
|
||
model_face_count = 0
|
||
if model_face_count < 600:
|
||
return ""
|
||
if str(info.get("surface", "")) != "plane":
|
||
return ""
|
||
try:
|
||
inner_wires = int(info.get("inner_boundary_wires") or 0)
|
||
boundary_wires = int(info.get("boundary_wires") or 0)
|
||
except (TypeError, ValueError):
|
||
inner_wires = 0
|
||
boundary_wires = 0
|
||
local_ready = info.get("local_face_deform_ready")
|
||
if inner_wires <= 0 and boundary_wires <= 1 and local_ready is not False:
|
||
return ""
|
||
base = str(info.get("local_face_deform_blocker") or "").strip()
|
||
if not base:
|
||
base = "当前 Face 位于复杂大 STEP 中,并且有内孔、内边界或复杂边界;不适合做只改当前 Face 的局部变形。"
|
||
return (
|
||
f"{base} 为避免在界面线程生成完整局部重建计划时卡顿,已在轻量预检查阶段阻止;"
|
||
"请改用 `拉伸/切除`、孔/槽专门入口、`移动特征` 或 `缩放特征`。"
|
||
)
|
||
|
||
def _quick_blocked_local_face_plan(
|
||
self,
|
||
face_id: int,
|
||
blocker: str,
|
||
*,
|
||
resize_strategy: str,
|
||
edit_strategy_label: str,
|
||
edit_semantics: str,
|
||
info: dict[str, object] | None = None,
|
||
extra: dict[str, object] | None = None,
|
||
) -> dict[str, object]:
|
||
source = dict(info or self._selected_face_info_snapshot())
|
||
plan = {
|
||
"status": "blocked",
|
||
"risk": "blocked",
|
||
"message": blocker,
|
||
"warnings": "",
|
||
"blockers": blocker,
|
||
"face_id": face_id,
|
||
"part_id": source.get("part_id"),
|
||
"solid_id": source.get("solid_id"),
|
||
"surface": source.get("surface"),
|
||
"area": source.get("area"),
|
||
"area_center": source.get("area_center"),
|
||
"bbox_diagonal": source.get("bbox_diagonal"),
|
||
"boundary_wires": source.get("boundary_wires"),
|
||
"inner_boundary_wires": source.get("inner_boundary_wires"),
|
||
"has_inner_boundaries": bool(source.get("has_inner_boundaries")),
|
||
"local_face_deform_ready": source.get("local_face_deform_ready"),
|
||
"local_face_deform_blocker": source.get("local_face_deform_blocker"),
|
||
"local_face_deform_face_count": source.get("local_face_deform_face_count", 1),
|
||
"local_face_deform_moved_point_count": 0,
|
||
"local_face_deform_target_kind": "blocked-quick-preflight",
|
||
"resize_strategy": resize_strategy,
|
||
"edit_strategy_label": edit_strategy_label,
|
||
"edit_semantics": edit_semantics,
|
||
"quick_preflight": True,
|
||
"ui_quick_blocked_local_face_plan": True,
|
||
}
|
||
if extra:
|
||
plan.update(extra)
|
||
return plan
|
||
|
||
def _quick_blocked_face_area_local_plan(self, face_id: int, target_area: float) -> dict[str, object] | None:
|
||
info = self._selected_face_info_snapshot()
|
||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||
if not blocker:
|
||
return None
|
||
current_area = _float_or_none(info.get("area"))
|
||
area_delta = target_area - current_area if current_area is not None else None
|
||
area_delta_ratio = abs(area_delta) / current_area if current_area is not None and current_area > 1e-9 else None
|
||
area_scale = math.sqrt(target_area / current_area) if current_area is not None and current_area > 1e-9 and target_area > 0 else None
|
||
return self._quick_blocked_local_face_plan(
|
||
face_id,
|
||
blocker,
|
||
resize_strategy="local-face-area-only-deform",
|
||
edit_strategy_label="面积(局部重建)",
|
||
edit_semantics="移动当前 Face 的边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||
info=info,
|
||
extra={
|
||
"current_area": current_area,
|
||
"target_area": target_area,
|
||
"area_delta": area_delta,
|
||
"area_delta_ratio": area_delta_ratio,
|
||
"local_face_area_scale": area_scale,
|
||
},
|
||
)
|
||
|
||
def _quick_blocked_face_size_local_plan(
|
||
self,
|
||
face_id: int,
|
||
target_size: float,
|
||
axis_key: str,
|
||
) -> dict[str, object] | None:
|
||
info = self._selected_face_info_snapshot()
|
||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||
if not blocker:
|
||
return None
|
||
current_width = _float_or_none(info.get("local_face_width"))
|
||
current_height = _float_or_none(info.get("local_face_height"))
|
||
current_size = current_height if axis_key == "height" else current_width
|
||
delta = target_size - current_size if current_size is not None else None
|
||
ratio = abs(delta) / current_size if current_size is not None and current_size > 1e-9 else None
|
||
scale = target_size / current_size if current_size is not None and current_size > 1e-9 else None
|
||
axis_label = "V向尺寸" if axis_key == "height" else "U向尺寸"
|
||
return self._quick_blocked_local_face_plan(
|
||
face_id,
|
||
blocker,
|
||
resize_strategy=f"local-face-{axis_key}-only-deform",
|
||
edit_strategy_label=f"{axis_label}(局部重建)",
|
||
edit_semantics="只沿当前 Face 的一个面内方向移动边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||
info=info,
|
||
extra={
|
||
"face_size_axis": axis_key,
|
||
"face_size_label": axis_label,
|
||
"current_face_width": current_width,
|
||
"target_face_width": target_size if axis_key == "width" else current_width,
|
||
"current_face_height": current_height,
|
||
"target_face_height": target_size if axis_key == "height" else current_height,
|
||
"current_face_size": current_size,
|
||
"target_face_size": target_size,
|
||
"face_size_delta": delta,
|
||
"face_size_delta_ratio": ratio,
|
||
"face_size_scale": scale,
|
||
"face_size_center": info.get("area_center") or info.get("bbox_center"),
|
||
"face_size_axis_direction": info.get("face_height_direction" if axis_key == "height" else "face_width_direction"),
|
||
"face_width_direction": info.get("face_width_direction"),
|
||
"face_height_direction": info.get("face_height_direction"),
|
||
},
|
||
)
|
||
|
||
def _quick_blocked_face_center_local_plan(
|
||
self,
|
||
face_id: int,
|
||
target_center: tuple[float, float, float],
|
||
) -> dict[str, object] | None:
|
||
info = self._selected_face_info_snapshot()
|
||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||
if not blocker:
|
||
return None
|
||
current_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
|
||
move_vector = _tuple_sub(target_center, current_center) if current_center is not None else None
|
||
move_distance = _vector_length(move_vector) if move_vector is not None else None
|
||
bbox_diagonal = _float_or_none(info.get("bbox_diagonal"))
|
||
move_ratio = move_distance / bbox_diagonal if move_distance is not None and bbox_diagonal is not None and bbox_diagonal > 1e-9 else None
|
||
return self._quick_blocked_local_face_plan(
|
||
face_id,
|
||
blocker,
|
||
resize_strategy="local-face-only-deform",
|
||
edit_strategy_label="中心(局部重建)",
|
||
edit_semantics="移动当前 Face 的边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||
info=info,
|
||
extra={
|
||
"current_face_center": current_center,
|
||
"target_face_center": target_center,
|
||
"face_center_move_vector": move_vector,
|
||
"face_center_move_distance": move_distance,
|
||
"face_center_move_ratio": move_ratio,
|
||
},
|
||
)
|
||
|
||
def _quick_blocked_face_plane_offset_local_plan(self, face_id: int, distance: float) -> dict[str, object] | None:
|
||
info = self._selected_face_info_snapshot()
|
||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||
if not blocker:
|
||
return None
|
||
plane_origin = _tuple_or_none(info.get("plane_origin"))
|
||
plane_direction = (
|
||
_tuple_normalized(_tuple_or_none(info.get("push_pull_outward_direction")))
|
||
or _tuple_normalized(_tuple_or_none(info.get("oriented_normal")))
|
||
or _tuple_normalized(_tuple_or_none(info.get("normal")))
|
||
)
|
||
current_position = _tuple_dot(plane_origin, plane_direction) if plane_origin is not None and plane_direction is not None else None
|
||
target_position = current_position + distance if current_position is not None else None
|
||
current_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
|
||
move_vector = _tuple_scale(plane_direction, distance) if plane_direction is not None else None
|
||
target_center = _tuple_add(current_center, move_vector) if current_center is not None and move_vector is not None else None
|
||
move_distance = abs(float(distance))
|
||
bbox_diagonal = _float_or_none(info.get("bbox_diagonal"))
|
||
move_ratio = move_distance / bbox_diagonal if bbox_diagonal is not None and bbox_diagonal > 1e-9 else None
|
||
return self._quick_blocked_local_face_plan(
|
||
face_id,
|
||
blocker,
|
||
resize_strategy="local-face-plane-offset-deform",
|
||
edit_strategy_label="偏移曲面(局部重建)",
|
||
edit_semantics="按当前面垂直方向移动 Face 边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||
info=info,
|
||
extra={
|
||
"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": current_center,
|
||
"target_face_center": target_center,
|
||
"face_center_move_vector": move_vector,
|
||
"face_center_move_distance": move_distance,
|
||
"face_center_move_ratio": move_ratio,
|
||
},
|
||
)
|
||
|
||
def _deferred_shell_thickness_plan_if_needed(
|
||
self,
|
||
face_id: int,
|
||
target_thickness: float,
|
||
) -> dict[str, object] | None:
|
||
info = self._selected_face_info_snapshot()
|
||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||
if not blocker:
|
||
return None
|
||
current = (
|
||
_float_or_none(info.get("shell_thickness_estimate"))
|
||
or _float_or_none(info.get("shell_current_thickness"))
|
||
or _float_or_none(info.get("shell_signed_thickness"))
|
||
)
|
||
if target_thickness <= 0:
|
||
return self._quick_blocked_local_face_plan(
|
||
face_id,
|
||
"目标壳体厚度必须大于 0。",
|
||
resize_strategy="push-pull-shell-source-plane-to-target-thickness",
|
||
edit_strategy_label="壳体厚度(拉伸/切除)",
|
||
edit_semantics="移动当前平面区域以接近目标壳体厚度。",
|
||
info=info,
|
||
extra={
|
||
"shell_current_thickness": current,
|
||
"shell_target_thickness": target_thickness,
|
||
"shell_delta_thickness": None,
|
||
"shell_delta_ratio": None,
|
||
},
|
||
)
|
||
delta = target_thickness - current if current is not None else None
|
||
ratio = abs(delta) / current if current is not None and current > 1e-9 else None
|
||
warnings = [
|
||
"复杂大 STEP 的壳体相对面识别会放到后台隔离进程里执行,避免主界面先卡住。",
|
||
"执行前请确认这是想修改的局部壳体区域,而不是孔/槽或台阶端面。",
|
||
]
|
||
return {
|
||
"status": "caution",
|
||
"risk": "high",
|
||
"message": (
|
||
"为避免在界面线程扫描复杂模型里的相对面,本次只做快速预检查;"
|
||
"真正的壳体相对面识别、拉伸/切除距离计算和结果校验会在后台隔离进程中完成。"
|
||
),
|
||
"warnings": ";".join(warnings),
|
||
"blockers": "",
|
||
"face_id": face_id,
|
||
"part_id": info.get("part_id"),
|
||
"solid_id": info.get("solid_id"),
|
||
"surface": info.get("surface"),
|
||
"shell_current_thickness": current,
|
||
"shell_target_thickness": target_thickness,
|
||
"shell_delta_thickness": delta,
|
||
"shell_delta_ratio": ratio,
|
||
"shell_signed_thickness": info.get("shell_signed_thickness"),
|
||
"shell_opposite_face_id": info.get("shell_opposite_face_id"),
|
||
"shell_overlap_ratio_estimate": info.get("shell_overlap_ratio_estimate"),
|
||
"shell_confidence": info.get("shell_confidence", "deferred"),
|
||
"shell_source_face_ids": info.get("shell_source_face_ids", (face_id,)),
|
||
"shell_region_kind": info.get("shell_region_kind", "deferred-complex-face"),
|
||
"push_pull_distance": None,
|
||
"outward_direction": info.get("push_pull_outward_direction") or info.get("oriented_normal") or info.get("normal"),
|
||
"push_pull_scope_face_ids": info.get("push_pull_scope_face_ids") or (face_id,),
|
||
"push_pull_scope_face_count": len(_int_values(info.get("push_pull_scope_face_ids")) or [face_id]),
|
||
"push_pull_scope_note": "复杂壳体计划延后到后台隔离进程重新计算。",
|
||
"resize_strategy": "push-pull-shell-source-plane-to-target-thickness",
|
||
"edit_strategy_label": "壳体厚度(拉伸/切除)",
|
||
"edit_semantics": "后台识别相对平面后,把当前平面区域拉伸/切除到目标壳体厚度;失败会保持原模型不变。",
|
||
"quick_preflight": True,
|
||
"ui_deferred_model_plan": True,
|
||
"ui_deferred_shell_thickness_plan": True,
|
||
}
|
||
|
||
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 (
|
||
not self._quick_edit_title_supports_isolation(title)
|
||
and 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"
|
||
+ (
|
||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if str(plan.get("risk")) == "high" and self._quick_edit_title_supports_isolation(title)
|
||
else ""
|
||
)
|
||
+ "为避免复杂 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 _quick_edit_title_supports_isolation(self, title: str) -> bool:
|
||
return title in {
|
||
"壳体厚度(缩放特征)缩放所属对象",
|
||
"面积(局部重建)",
|
||
"面面积(缩放特征)",
|
||
"U向尺寸(局部重建)",
|
||
"V向尺寸(局部重建)",
|
||
"U向尺寸(缩放特征)",
|
||
"V向尺寸(缩放特征)",
|
||
"偏移曲面(局部重建)",
|
||
"中心(局部重建)",
|
||
"圆柱高度调整",
|
||
"高度(缩放特征)缩放所属对象",
|
||
}
|
||
|
||
def _isolation_for_plan(
|
||
self,
|
||
plan: dict[str, object],
|
||
operation: str,
|
||
args: list[object],
|
||
*,
|
||
timeout_seconds: float = 180.0,
|
||
) -> dict[str, object] | None:
|
||
risk = str(plan.get("risk", ""))
|
||
isolated_geometry_operations = {
|
||
"push_pull_face",
|
||
"move_face_plane_offset_local",
|
||
"resize_face_area_local",
|
||
"resize_face_area",
|
||
"resize_face_size_local",
|
||
"resize_face_size_owning_scale",
|
||
"move_face_center_local",
|
||
"resize_shell_thickness",
|
||
"resize_shell_thickness_owning_scale",
|
||
"resize_cylindrical_height",
|
||
"resize_cylindrical_boss_height",
|
||
"resize_cylindrical_height_owning_scale",
|
||
"resize_cone_reference_radius",
|
||
"resize_cone_semi_angle",
|
||
"resize_sphere_radius",
|
||
"resize_torus_radius",
|
||
"resize_cylindrical_hole",
|
||
"resize_cylindrical_owning_scale",
|
||
"move_cylindrical_hole_axis",
|
||
"suppress_cylindrical_hole",
|
||
"resize_cylindrical_depth",
|
||
"resize_cylindrical_depth_owning_scale",
|
||
"move_cylindrical_slot_axis",
|
||
"resize_cylindrical_slot_width",
|
||
"resize_cylindrical_slot_depth",
|
||
"resize_cylindrical_slot_arc_length",
|
||
"resize_cylindrical_slot_angular_span",
|
||
"resize_cylindrical_slot_total_length",
|
||
"resize_cylindrical_slot_center_distance",
|
||
"resize_general_edge_length",
|
||
"move_edge_endpoint",
|
||
"move_edge_center",
|
||
"move_circular_edge_axis_center",
|
||
"resize_ellipse_edge_axis_radius",
|
||
"resize_existing_fillet",
|
||
"fillet_edge",
|
||
"chamfer_edge",
|
||
"chamfer_edge_asymmetric",
|
||
"chamfer_edge_distance_angle",
|
||
}
|
||
if operation not in isolated_geometry_operations:
|
||
return None
|
||
if risk not in {"low", "medium", "high"}:
|
||
return None
|
||
return {
|
||
"operation": operation,
|
||
"args": args,
|
||
"timeout_seconds": timeout_seconds,
|
||
"reason": f"{risk}-risk-isolated-occ-edit",
|
||
}
|
||
|
||
def _edit_failure_diagnostics(self, context: dict[str, object]) -> str:
|
||
parameters = context.get("parameters")
|
||
if not isinstance(parameters, dict):
|
||
parameters = {}
|
||
lines: list[str] = []
|
||
operation_name = str(context.get("operation_name") or "").strip()
|
||
target = str(context.get("target") or "").strip()
|
||
if operation_name or target:
|
||
lines.append(f"操作: {operation_name or '未知'} / {target or '未知对象'}")
|
||
|
||
strategy = str(parameters.get("edit_strategy_label") or "").strip()
|
||
if strategy:
|
||
lines.append(f"编辑策略: {strategy}")
|
||
|
||
surface = parameters.get("surface")
|
||
distance = parameters.get("semantic_distance")
|
||
current_position = parameters.get("current_plane_position")
|
||
target_position = parameters.get("target_plane_position")
|
||
surface_chunks: list[str] = []
|
||
if surface not in {None, ""}:
|
||
surface_chunks.append(f"面类型={_compact_plan_value(surface)}")
|
||
if distance not in {None, ""}:
|
||
surface_chunks.append(f"修改量={_compact_plan_value(distance)}")
|
||
if current_position not in {None, ""}:
|
||
surface_chunks.append(f"当前位置={_compact_plan_value(current_position)}")
|
||
if target_position not in {None, ""}:
|
||
surface_chunks.append(f"目标位置={_compact_plan_value(target_position)}")
|
||
if surface_chunks:
|
||
lines.append("当前对象: " + ",".join(surface_chunks))
|
||
|
||
boundary_chunks: list[str] = []
|
||
boundary_keys = (
|
||
("线圈", "selected_boundary_wires"),
|
||
("内孔/内边界", "selected_inner_boundary_wires"),
|
||
("同域Face", "same_domain_face_count"),
|
||
("一级Edge", "first_level_boundary_edge_count"),
|
||
("一级Vertex", "first_level_boundary_vertex_count"),
|
||
("一级相邻Face", "first_level_adjacent_face_count"),
|
||
)
|
||
for label, key in boundary_keys:
|
||
value = parameters.get(key)
|
||
if value not in {None, ""}:
|
||
boundary_chunks.append(f"{label}={_compact_plan_value(value)}")
|
||
if boundary_chunks:
|
||
lines.append("一级关系证据: " + ",".join(boundary_chunks))
|
||
|
||
topology_note = str(parameters.get("first_level_topology_note") or "").strip()
|
||
ignored_note = str(parameters.get("topology_ignored_relation_note") or "").strip()
|
||
if topology_note:
|
||
lines.append(f"一级关系说明: {topology_note}")
|
||
if ignored_note:
|
||
lines.append(f"暂不处理范围: {ignored_note}")
|
||
|
||
planar_method = str(parameters.get("planar_cap_extension_method") or "").strip()
|
||
cylindrical_method = str(parameters.get("cylindrical_cap_extension_method") or "").strip()
|
||
if planar_method or cylindrical_method:
|
||
method_parts: list[str] = []
|
||
if planar_method:
|
||
method_parts.append(
|
||
f"平面端盖={_compact_plan_value(parameters.get('planar_cap_extension_kind'))}"
|
||
f"/{_compact_plan_value(planar_method)}"
|
||
)
|
||
if cylindrical_method:
|
||
method_parts.append(
|
||
f"圆柱端盖={_compact_plan_value(parameters.get('cylindrical_cap_extension_kind'))}"
|
||
f"/{_compact_plan_value(cylindrical_method)}"
|
||
)
|
||
lines.append("已识别的专用路径: " + ",".join(method_parts))
|
||
|
||
risk_message = str(parameters.get("push_pull_message") or "").strip()
|
||
if risk_message:
|
||
lines.append(f"计划阶段判断: {risk_message}")
|
||
|
||
isolation = context.get("isolation")
|
||
if isinstance(isolation, dict) and isolation:
|
||
timeout = isolation.get("timeout_seconds")
|
||
reason = str(isolation.get("reason") or "").strip()
|
||
if timeout not in {None, ""}:
|
||
lines.append(f"隔离保护: 子进程超时上限 {_compact_plan_value(timeout)}s")
|
||
if reason:
|
||
lines.append(f"隔离原因: {reason}")
|
||
|
||
has_inner_boundaries = bool(parameters.get("selected_has_inner_boundaries"))
|
||
try:
|
||
inner_wires = int(parameters.get("selected_inner_boundary_wires", 0) or 0)
|
||
except (TypeError, ValueError):
|
||
inner_wires = 0
|
||
if has_inner_boundaries or inner_wires > 0:
|
||
lines.append(
|
||
"可能原因: 当前面带孔/内边界。若修改方向会越过孔壁、槽底、台阶终点,"
|
||
"就可能涉及一级相邻面背后的二级或更深拓扑关系;当前阶段只自动传播一级关系。"
|
||
)
|
||
elif str(parameters.get("push_pull_risk") or "") == "high":
|
||
lines.append(
|
||
"可能原因: 当前操作风险较高,底层 OCCT 布尔或局部重建可能返回无效 B-Rep;"
|
||
"这通常不是界面卡住,而是几何内核没有稳定给出可用结果。"
|
||
)
|
||
|
||
if not lines:
|
||
return ""
|
||
return "\n\n诊断信息:\n" + "\n".join(f"- {line}" for line in lines)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(plan, "resize_cylindrical_hole", [face_id, diameter])
|
||
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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(plan, "resize_cylindrical_owning_scale", [face_id, diameter])
|
||
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"),
|
||
"embedded_cone_recut_mode": plan.get("embedded_cone_recut_mode"),
|
||
"embedded_cone_current_small_radius": plan.get("embedded_cone_current_small_radius"),
|
||
"embedded_cone_target_small_radius": plan.get("embedded_cone_target_small_radius"),
|
||
"embedded_cone_current_large_radius": plan.get("embedded_cone_current_large_radius"),
|
||
"embedded_cone_target_large_radius": plan.get("embedded_cone_target_large_radius"),
|
||
"embedded_cone_height": plan.get("embedded_cone_height"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"move_cylindrical_hole_axis",
|
||
[face_id, list(target_center)],
|
||
)
|
||
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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"move_cylindrical_slot_axis",
|
||
[face_id, list(target_center)],
|
||
)
|
||
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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(plan, "resize_cylindrical_owning_scale", [face_id, target_diameter])
|
||
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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_slot_width",
|
||
[face_id, target_width, pair_face_id],
|
||
)
|
||
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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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:
|
||
target_depth = float(self.slot_depth_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "目标槽深无效", "请输入数字形式的目标槽/半孔深度。")
|
||
return
|
||
if target_depth <= 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
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_slot_depth",
|
||
[face_id, target_depth, pair_face_id],
|
||
)
|
||
self.clear_edit_preview(render=False)
|
||
|
||
def action():
|
||
return self.model.resize_cylindrical_slot_depth(face_id, target_depth, 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_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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_slot_arc_length(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_arc_length_input"):
|
||
return
|
||
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
|
||
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
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_slot_arc_length",
|
||
[face_id, target_arc_length, pair_face_id],
|
||
)
|
||
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"),
|
||
"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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_slot_angular_span(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_angular_span_input"):
|
||
return
|
||
try:
|
||
target_angular_span = float(self.slot_angular_span_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "目标圆弧角度无效", "请输入数字形式的目标槽/半孔圆弧角度。")
|
||
return
|
||
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()
|
||
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}",
|
||
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(
|
||
"槽/半孔圆弧角度调整",
|
||
plan,
|
||
lines,
|
||
"槽/半孔圆弧角度调整已阻止",
|
||
"已取消槽/半孔圆弧角度调整",
|
||
):
|
||
return
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_slot_angular_span",
|
||
[face_id, target_angular_span],
|
||
)
|
||
self.clear_edit_preview(render=False)
|
||
|
||
def action():
|
||
return self.model.resize_cylindrical_slot_angular_span(face_id, target_angular_span)
|
||
|
||
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_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"),
|
||
"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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_slot_total_length(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_total_length_input"):
|
||
return
|
||
try:
|
||
target_total_length = float(self.slot_total_length_input.text())
|
||
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))
|
||
return
|
||
plan = self.model.cylindrical_slot_total_length_plan(
|
||
face_id,
|
||
target_total_length,
|
||
pair_face_id=pair_face_id,
|
||
)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能调整槽孔总长度", str(plan["message"]))
|
||
self.statusBar().showMessage("槽孔总长度调整已阻止")
|
||
return
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_slot_total_length",
|
||
[face_id, target_total_length, pair_face_id],
|
||
)
|
||
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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_slot_center_distance(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_center_distance_input"):
|
||
return
|
||
try:
|
||
target_center_distance = float(self.slot_center_distance_input.text())
|
||
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))
|
||
return
|
||
plan = self.model.cylindrical_slot_center_distance_plan(
|
||
face_id,
|
||
target_center_distance,
|
||
pair_face_id=pair_face_id,
|
||
)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能调整槽孔中心距", str(plan["message"]))
|
||
self.statusBar().showMessage("槽孔中心距调整已阻止")
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_slot_center_distance",
|
||
[face_id, target_center_distance, pair_face_id],
|
||
)
|
||
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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_boss(self) -> None:
|
||
if self.model is None:
|
||
return
|
||
if self._edit_busy("请等待当前编辑完成后再调整凸台直径。"):
|
||
return
|
||
if self.selected_face_id is None:
|
||
QMessageBox.information(self, "未选择圆柱凸台", "请先选择一个圆柱凸台候选 Face。")
|
||
return
|
||
try:
|
||
diameter = float(self.boss_diameter_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "直径无效", "请输入数字形式的目标凸台直径。")
|
||
return
|
||
info = self.model.face_info(self.selected_face_id)
|
||
if "diameter" not in info:
|
||
QMessageBox.information(self, "不是圆柱面", "当前选中的 Face 不是圆柱面,不能调整圆柱凸台直径。")
|
||
return
|
||
plan = self.model.cylindrical_boss_resize_plan(self.selected_face_id, diameter)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能调整凸台直径", str(plan["message"]))
|
||
self.statusBar().showMessage("圆柱凸台直径调整已阻止")
|
||
return
|
||
if plan["risk"] != "low":
|
||
target_to_height_ratio = plan.get("target_to_height_ratio", "")
|
||
target_to_height_line = (
|
||
""
|
||
if target_to_height_ratio is None or target_to_height_ratio == ""
|
||
else f"目标直径/估算高度: {_format_value(target_to_height_ratio)}\n"
|
||
)
|
||
warnings = str(plan.get("warnings", ""))
|
||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认圆柱凸台直径调整",
|
||
(
|
||
f"face: {plan['face_id']}\n"
|
||
f"当前凸台直径: {_format_value(plan['current_diameter'])}\n"
|
||
f"目标凸台直径: {_format_value(plan['target_diameter'])}\n"
|
||
f"直径变化量: {_format_value(plan['delta_diameter'])}\n"
|
||
f"直径变化比例: {_format_percent(plan['diameter_delta_ratio'])}\n"
|
||
f"{target_to_height_line}"
|
||
f"调整模式: {plan['resize_mode']}\n"
|
||
f"候选判断: {plan['feature_guess']}\n"
|
||
f"置信度: {plan['confidence']}\n"
|
||
f"材料投票: {plan['material_vote_summary']}\n"
|
||
f"工具策略: {plan['boss_tool_strategy']}\n"
|
||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||
f"工具高度: {_format_value(plan['boss_tool_height'])}\n"
|
||
f"轴向重叠余量: {_format_value(plan['boss_tool_axial_margin'])}\n"
|
||
f"风险: {plan['risk']}\n\n"
|
||
f"{warnings_line}"
|
||
f"{plan['message']}\n\n"
|
||
"继续操作会对当前零件的局部几何执行受限B-Rep布尔修改。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No,
|
||
)
|
||
if result != QMessageBox.StandardButton.Yes:
|
||
self.statusBar().showMessage("已取消圆柱凸台直径调整")
|
||
return
|
||
|
||
face_id = self.selected_face_id
|
||
self._show_cylinder_boss_resize_preview(face_id, diameter)
|
||
|
||
def action():
|
||
return self.model.resize_cylindrical_boss(face_id, diameter)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="调整圆柱凸台直径",
|
||
target=f"Face {face_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"new_diameter": diameter,
|
||
"old_diameter": info.get("diameter"),
|
||
"delta_diameter": plan.get("delta_diameter"),
|
||
"diameter_delta_ratio": plan.get("diameter_delta_ratio"),
|
||
"target_to_height_ratio": plan.get("target_to_height_ratio"),
|
||
"resize_mode": plan.get("resize_mode"),
|
||
"feature_type": plan.get("feature_type"),
|
||
"feature_guess": plan.get("feature_guess"),
|
||
"confidence": plan.get("confidence"),
|
||
"boss_resize_status": plan.get("status"),
|
||
"boss_resize_risk": plan.get("risk"),
|
||
"boss_resize_message": plan.get("message"),
|
||
"boss_resize_warnings": plan.get("warnings"),
|
||
"boss_resize_blockers": plan.get("blockers"),
|
||
"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"),
|
||
"boss_tool_strategy": plan.get("boss_tool_strategy"),
|
||
"boss_tool_height": plan.get("boss_tool_height"),
|
||
"boss_tool_axial_margin": plan.get("boss_tool_axial_margin"),
|
||
"boss_tool_radius": plan.get("boss_tool_radius"),
|
||
"boss_tool_outer_radius": plan.get("boss_tool_outer_radius"),
|
||
"boss_tool_inner_radius": plan.get("boss_tool_inner_radius"),
|
||
"boss_tool_radial_overlap": plan.get("boss_tool_radial_overlap"),
|
||
"boss_tool_note": plan.get("boss_tool_note"),
|
||
"feature_adjacent_face_ids": plan.get("feature_adjacent_face_ids"),
|
||
"feature_boundary_edge_ids": plan.get("feature_boundary_edge_ids"),
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
)
|
||
|
||
def 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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_boss_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_boss_height_plan(face_id, target_height)
|
||
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"端盖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"
|
||
"继续操作会拉伸/切除识别到的凸台端盖,真实修改当前 B-Rep 几何。\n\n"
|
||
"确定继续吗?"
|
||
),
|
||
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_cylindrical_boss_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"),
|
||
"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"),
|
||
"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,
|
||
isolation=self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_boss_height",
|
||
[face_id, target_height],
|
||
),
|
||
)
|
||
|
||
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,
|
||
isolation=self._isolation_for_plan(plan, "resize_cylindrical_height", [face_id, target_height]),
|
||
)
|
||
|
||
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"),
|
||
"embedded_cone_recut_mode": plan.get("embedded_cone_recut_mode"),
|
||
"embedded_cone_fixed_small_radius": plan.get("embedded_cone_fixed_small_radius"),
|
||
"embedded_cone_current_small_radius": plan.get("embedded_cone_current_small_radius"),
|
||
"embedded_cone_target_small_radius": plan.get("embedded_cone_target_small_radius"),
|
||
"embedded_cone_current_large_radius": plan.get("embedded_cone_current_large_radius"),
|
||
"embedded_cone_target_large_radius": plan.get("embedded_cone_target_large_radius"),
|
||
"embedded_cone_height": plan.get("embedded_cone_height"),
|
||
"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,
|
||
isolation=self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_height_owning_scale",
|
||
[face_id, target_height],
|
||
),
|
||
)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(plan, "suppress_cylindrical_hole", [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": (
|
||
"沿识别到的开口到底面方向调整深度:加深时切削,变浅时从新底面到旧底面补料。"
|
||
),
|
||
"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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_depth",
|
||
[face_id, target_depth, bottom_face_id],
|
||
)
|
||
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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_cylindrical_depth_owning_scale",
|
||
[face_id, target_depth, bottom_face_id],
|
||
)
|
||
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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(plan, "resize_existing_fillet", [face_id, target_radius])
|
||
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"
|
||
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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(plan, "fillet_edge", [edge_id, 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,
|
||
"确认给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"
|
||
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"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(plan, "chamfer_edge", [edge_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,
|
||
"确认给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"
|
||
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"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"chamfer_edge_asymmetric",
|
||
[edge_id, distance1, distance2, reference_face_id],
|
||
)
|
||
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"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"chamfer_edge_distance_angle",
|
||
[edge_id, distance, angle_degrees, reference_face_id],
|
||
)
|
||
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"),
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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,
|
||
)
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_general_edge_length",
|
||
[edge_id, target_length, anchor_mode, strategy_mode],
|
||
)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
|
||
self.statusBar().showMessage("Edge长度修改已阻止")
|
||
return
|
||
|
||
if plan["risk"] != "low":
|
||
warnings = str(plan.get("warnings", ""))
|
||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认修改Edge长度",
|
||
(
|
||
f"Edge: {plan['edge_id']}\n"
|
||
f"当前Edge长度: {_format_value(plan['current_length'])}\n"
|
||
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
|
||
f"变化量: {_format_value(plan['delta_length'])}\n"
|
||
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
||
f"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"
|
||
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)
|
||
|
||
def action():
|
||
return self.model.resize_general_edge_length(
|
||
edge_id,
|
||
target_length,
|
||
anchor_mode=anchor_mode,
|
||
strategy_mode=strategy_mode,
|
||
)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修改Edge长度",
|
||
target=f"Edge {edge_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"current_length": plan.get("current_length"),
|
||
"target_length": plan.get("target_length"),
|
||
"delta_length": plan.get("delta_length"),
|
||
"length_change_ratio": plan.get("length_change_ratio"),
|
||
"edge_length_anchor_mode": plan.get("edge_length_anchor_mode"),
|
||
"edge_length_anchor_label": plan.get("edge_length_anchor_label"),
|
||
"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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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,
|
||
)
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_general_edge_length",
|
||
[edge_id, target_length, anchor_mode, strategy_mode],
|
||
)
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
|
||
self.statusBar().showMessage("Edge长度修改已阻止")
|
||
return
|
||
|
||
warnings = str(plan.get("warnings", ""))
|
||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||
strategy = str(plan.get("resize_strategy", ""))
|
||
if plan["risk"] != "low":
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认修改Edge长度",
|
||
(
|
||
f"Edge: {plan['edge_id']}\n"
|
||
f"曲线类型: {_format_value(plan.get('curve', ''))}\n"
|
||
f"当前Edge长度: {_format_value(plan['current_length'])}\n"
|
||
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
|
||
f"变化量: {_format_value(plan['delta_length'])}\n"
|
||
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
||
f"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"
|
||
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)
|
||
|
||
def action():
|
||
return self.model.resize_general_edge_length(
|
||
edge_id,
|
||
target_length,
|
||
anchor_mode=anchor_mode,
|
||
strategy_mode=strategy_mode,
|
||
)
|
||
|
||
self._run_edit_action(
|
||
action,
|
||
operation_name="修改Edge长度",
|
||
target=f"Edge {edge_id}",
|
||
parameters={
|
||
"part_id": plan.get("part_id"),
|
||
"solid_id": plan.get("solid_id"),
|
||
"curve": plan.get("curve"),
|
||
"current_length": plan.get("current_length"),
|
||
"target_length": plan.get("target_length"),
|
||
"delta_length": plan.get("delta_length"),
|
||
"length_change_ratio": plan.get("length_change_ratio"),
|
||
"edge_length_anchor_mode": plan.get("edge_length_anchor_mode"),
|
||
"edge_length_anchor_label": plan.get("edge_length_anchor_label"),
|
||
"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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_ellipse_edge_major_radius(self) -> None:
|
||
self._resize_ellipse_edge_axis_radius("major")
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(
|
||
plan,
|
||
"resize_ellipse_edge_axis_radius",
|
||
[edge_id, target_radius, 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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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()),
|
||
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_circular_edge_axis_center(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_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, "圆心坐标无效", "请输入 X, Y, Z 三个数字形式的目标圆心坐标。")
|
||
return
|
||
|
||
edge_id = self.selected_edge_id
|
||
plan = self.model.circular_edge_axis_move_plan(edge_id, target_center)
|
||
isolation = self._isolation_for_plan(plan, "move_circular_edge_axis_center", [edge_id, target_center])
|
||
lines = [
|
||
f"Edge: {edge_id}",
|
||
f"当前圆心: {_format_value(plan.get('current_edge_center'))}",
|
||
f"目标圆心: {_format_value(plan.get('target_edge_center'))}",
|
||
f"圆心移动: {_format_value(plan.get('circular_edge_center_move_vector'))}",
|
||
f"相邻Face: {_format_value(plan.get('circular_edge_cylinder_face_id'))}",
|
||
f"执行路径: {_format_value(plan.get('circular_edge_cylinder_mode_label'))}",
|
||
f"当前轴心: {_format_value(plan.get('current_axis_center'))}",
|
||
f"目标轴心: {_format_value(plan.get('target_axis_center'))}",
|
||
f"圆边半径: {_format_value(plan.get('circular_edge_current_radius'))}",
|
||
"当前版本会按圆Edge圆心的移动量,移动相邻孔/槽/凸台的圆柱轴心。",
|
||
]
|
||
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_circular_edge_axis_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"),
|
||
"circular_edge_center_move_vector": plan.get("circular_edge_center_move_vector"),
|
||
"circular_edge_center_move_distance": plan.get("circular_edge_center_move_distance"),
|
||
"circular_edge_current_radius": plan.get("circular_edge_current_radius"),
|
||
"circular_edge_current_diameter": plan.get("circular_edge_current_diameter"),
|
||
"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"),
|
||
"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"),
|
||
"target_diameter": plan.get("target_diameter"),
|
||
"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"),
|
||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||
},
|
||
target_kind="edge",
|
||
target_id=edge_id,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(plan, "move_edge_center", [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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(plan, "move_edge_endpoint", [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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_cone_reference_radius(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, "cone_reference_radius_input"):
|
||
return
|
||
try:
|
||
target_radius = float(self.cone_reference_radius_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "圆锥参考半径无效", "请输入数字形式的目标圆锥参考半径。")
|
||
return
|
||
|
||
face_id = self.selected_face_id
|
||
plan = self.model.conical_reference_radius_plan(face_id, target_radius)
|
||
isolation = self._isolation_for_plan(plan, "resize_cone_reference_radius", [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 ""
|
||
isolation_line = (
|
||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if isolation is not None
|
||
else ""
|
||
)
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认修改圆锥面参数",
|
||
(
|
||
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"
|
||
f"{isolation_line}"
|
||
"继续操作会修改当前 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_conical_reference_radius(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"),
|
||
"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"),
|
||
"semi_angle_degrees": plan.get("semi_angle_degrees"),
|
||
"target_semi_angle": plan.get("target_semi_angle"),
|
||
"target_semi_angle_degrees": plan.get("target_semi_angle_degrees"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_cone_semi_angle(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, "cone_reference_radius_input"):
|
||
return
|
||
try:
|
||
target_angle_degrees = float(self.cone_reference_radius_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "圆锥半角无效", "请输入数字形式的目标半角,单位是度。")
|
||
return
|
||
|
||
face_id = self.selected_face_id
|
||
plan = self.model.conical_semi_angle_plan(face_id, target_angle_degrees)
|
||
isolation = self._isolation_for_plan(plan, "resize_cone_semi_angle", [face_id, target_angle_degrees])
|
||
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 ""
|
||
isolation_line = (
|
||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if isolation is not None
|
||
else ""
|
||
)
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认修改圆锥半角",
|
||
(
|
||
f"Face: {face_id}\n"
|
||
f"当前半角: {_format_value(plan.get('semi_angle_degrees'))} 度\n"
|
||
f"目标半角: {_format_value(plan.get('target_semi_angle_degrees'))} 度\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('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"
|
||
f"{isolation_line}"
|
||
"继续操作会修改当前 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_conical_semi_angle(face_id, target_angle_degrees)
|
||
|
||
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": "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"),
|
||
"semi_angle_degrees": plan.get("semi_angle_degrees"),
|
||
"target_semi_angle": plan.get("target_semi_angle"),
|
||
"target_semi_angle_degrees": plan.get("target_semi_angle_degrees"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
def resize_sphere_radius(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, "sphere_radius_input"):
|
||
return
|
||
try:
|
||
target_radius = float(self.sphere_radius_input.text())
|
||
except ValueError:
|
||
QMessageBox.critical(self, "球面半径无效", "请输入数字形式的目标球面半径。")
|
||
return
|
||
|
||
face_id = self.selected_face_id
|
||
plan = self.model.spherical_radius_plan(face_id, target_radius)
|
||
isolation = self._isolation_for_plan(plan, "resize_sphere_radius", [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 ""
|
||
isolation_line = (
|
||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if isolation is not None
|
||
else ""
|
||
)
|
||
result = QMessageBox.question(
|
||
self,
|
||
"确认修改球面(缩放特征)",
|
||
(
|
||
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"
|
||
f"{isolation_line}"
|
||
"继续操作会修改当前 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_spherical_radius(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"),
|
||
"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"),
|
||
"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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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)
|
||
isolation = self._isolation_for_plan(plan, "resize_torus_radius", [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 ""
|
||
isolation_line = (
|
||
"本次会在隔离子进程里执行高风险 OCCT 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if isolation is not None
|
||
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"
|
||
f"{isolation_line}"
|
||
"继续操作会修改当前 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,
|
||
isolation=isolation,
|
||
)
|
||
|
||
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"
|
||
+ (
|
||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||
if str(plan.get("risk")) == "high"
|
||
else ""
|
||
)
|
||
+ "这不是只改变一个面的 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"),
|
||
},
|
||
target_kind="face",
|
||
target_id=face_id,
|
||
isolation=self._isolation_for_plan(plan, "resize_face_area", [face_id, target_area]),
|
||
)
|
||
|
||
def resize_face_area_local(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._quick_blocked_face_area_local_plan(face_id, target_area)
|
||
if plan is None:
|
||
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(
|
||
"面积(局部重建)",
|
||
plan,
|
||
lines,
|
||
"面积(局部重建)已阻止",
|
||
"已取消面积(局部重建)",
|
||
):
|
||
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="面积(局部重建)",
|
||
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,
|
||
isolation=self._isolation_for_plan(plan, "resize_face_area_local", [face_id, target_area]),
|
||
)
|
||
|
||
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 = "V向尺寸" if axis_key == "height" else "U向尺寸"
|
||
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._quick_blocked_face_size_local_plan(face_id, target_size, axis_key)
|
||
if plan is None:
|
||
plan = self.model.face_size_local_resize_plan(face_id, target_size, axis_key)
|
||
lines = [
|
||
f"Face: {face_id}",
|
||
f"修改对象: {axis_label}(局部重建)",
|
||
f"当前U向尺寸: {_format_value(plan.get('current_face_width'))}",
|
||
f"目标U向尺寸: {_format_value(plan.get('target_face_width'))}",
|
||
f"当前V向尺寸: {_format_value(plan.get('current_face_height'))}",
|
||
f"目标V向尺寸: {_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,
|
||
isolation=self._isolation_for_plan(plan, "resize_face_size_local", [face_id, target_size, axis_key]),
|
||
)
|
||
|
||
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 = "V向尺寸" if axis_key == "height" else "U向尺寸"
|
||
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"当前U向尺寸: {_format_value(plan.get('current_face_width'))}",
|
||
f"目标U向尺寸: {_format_value(plan.get('target_face_width'))}",
|
||
f"当前V向尺寸: {_format_value(plan.get('current_face_height'))}",
|
||
f"目标V向尺寸: {_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,
|
||
isolation=self._isolation_for_plan(plan, "resize_face_size_owning_scale", [face_id, target_size, axis_key]),
|
||
)
|
||
|
||
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
|
||
plan = self.model.face_plane_offset_owning_translation_plan(face_id, distance)
|
||
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能按偏移曲面平移", str(plan["message"]))
|
||
self.statusBar().showMessage("偏移曲面整体平移已阻止")
|
||
return
|
||
current_position = float(plan.get("current_plane_position", 0.0) or 0.0)
|
||
target_position = float(plan.get("target_plane_position", current_position) or current_position)
|
||
plane_direction = tuple(plan.get("plane_direction") or (0.0, 0.0, 0.0))
|
||
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)
|
||
|
||
def action():
|
||
return self.model.translate_face_plane_offset_owning(face_id, distance)
|
||
|
||
moved_kind = str(plan.get("target_kind", ""))
|
||
moved_id = plan.get("solid_id") if moved_kind == "solid" else plan.get("part_id")
|
||
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": plan.get("plane_origin"),
|
||
"plane_direction": plane_direction,
|
||
"translation_vector": plan.get("translation_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("请等待当前编辑完成后再执行偏移曲面(局部重建)。"):
|
||
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
|
||
plan = self._quick_blocked_face_plane_offset_local_plan(face_id, distance)
|
||
if plan is None:
|
||
plan = self.model.face_plane_offset_local_plan(face_id, distance)
|
||
lines = [
|
||
f"Face: {face_id}",
|
||
f"当前偏移曲面: {_format_value(plan.get('current_plane_position'))}",
|
||
f"目标偏移曲面: {_format_value(plan.get('target_plane_position'))}",
|
||
f"平面方向: {_format_value(plan.get('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_plane_offset_local(face_id, distance)
|
||
|
||
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": plan.get("current_plane_position"),
|
||
"target_plane_position": plan.get("target_plane_position"),
|
||
"plane_origin": plan.get("plane_origin"),
|
||
"plane_direction": plan.get("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": "偏移曲面(局部重建)",
|
||
"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,
|
||
isolation=self._isolation_for_plan(plan, "move_face_plane_offset_local", [face_id, distance]),
|
||
)
|
||
|
||
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.face_center_owning_translation_plan(face_id, target_center)
|
||
|
||
def action():
|
||
return self.model.move_face_center_owning(face_id, target_center)
|
||
|
||
elif self.selected_part_id is not None:
|
||
moved_kind = "part"
|
||
moved_id = self.selected_part_id
|
||
plan = self.model.face_center_owning_translation_plan(face_id, target_center)
|
||
|
||
def action():
|
||
return self.model.move_face_center_owning(face_id, target_center)
|
||
|
||
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("请等待当前编辑完成后再执行中心(局部重建)。"):
|
||
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)
|
||
if current_center is None:
|
||
QMessageBox.information(self, "不能执行中心(局部重建)", "当前 Face 缺少稳定中心坐标。")
|
||
return
|
||
target_center = (
|
||
current_center[0] + vector[0],
|
||
current_center[1] + vector[1],
|
||
current_center[2] + vector[2],
|
||
)
|
||
plan = self._quick_blocked_face_center_local_plan(face_id, target_center)
|
||
if plan is None:
|
||
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(
|
||
"中心(局部重建)",
|
||
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_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": 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,
|
||
isolation=self._isolation_for_plan(plan, "move_face_center_local", [face_id, list(target_center)]),
|
||
)
|
||
|
||
def move_selected_axis_center_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。")
|
||
return
|
||
try:
|
||
vector = self._translation_vector_from_inputs()
|
||
except ValueError as exc:
|
||
QMessageBox.critical(self, "轴心坐标无效", str(exc))
|
||
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
|
||
|
||
if plan["status"] == "blocked":
|
||
QMessageBox.information(self, "不能按轴心平移", str(plan["message"]))
|
||
self.statusBar().showMessage("按轴心平移所属对象已阻止")
|
||
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
|
||
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_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"
|
||
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 _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,
|
||
)
|
||
|
||
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,
|
||
isolation: dict[str, object] | 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)
|
||
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(),
|
||
"edit_result_deflection": result_deflection,
|
||
"isolation": dict(isolation or {}),
|
||
}
|
||
|
||
self._begin_edit_task(operation_name)
|
||
self.pending_edit_context = context
|
||
thread = QThread(self)
|
||
worker = EditWorker(self._make_edit_job(action, context))
|
||
worker.moveToThread(thread)
|
||
thread.started.connect(worker.run)
|
||
worker.finished.connect(self._finish_edit_action, Qt.ConnectionType.QueuedConnection)
|
||
worker.failed.connect(self._fail_edit_action, Qt.ConnectionType.QueuedConnection)
|
||
thread.finished.connect(worker.deleteLater)
|
||
thread.finished.connect(thread.deleteLater)
|
||
thread.finished.connect(self._forget_edit_thread)
|
||
self.edit_thread = thread
|
||
self.edit_worker = worker
|
||
thread.start()
|
||
|
||
def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None:
|
||
if self.model is None or target_id is None or target_kind not in {"face", "feature"}:
|
||
return None
|
||
try:
|
||
numeric_id = int(target_id)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
if 0 <= numeric_id < len(self.model.faces):
|
||
try:
|
||
return self.model.face_region_logical_id(numeric_id)
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
def _make_edit_job(self, action, context: dict[str, object]):
|
||
def job():
|
||
if self.model is None:
|
||
raise RuntimeError("Model is not loaded.")
|
||
snapshot = self.model.snapshot()
|
||
target_part_id = self._edit_context_part_id(context)
|
||
before_stats = self.model.stats()
|
||
before_part_stats = self._part_stats_or_none(target_part_id)
|
||
before_quality = self._edit_quality_info_or_none(self.model, context, target_part_id)
|
||
before_geometry = {}
|
||
isolation = context.get("isolation")
|
||
if isinstance(isolation, dict) and isolation:
|
||
return self._run_isolated_edit_job(
|
||
context=context,
|
||
isolation=isolation,
|
||
snapshot=snapshot,
|
||
before_stats=before_stats,
|
||
before_part_stats=before_part_stats,
|
||
before_quality=before_quality,
|
||
before_geometry=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)
|
||
after_quality = self._edit_quality_info_or_none(self.model, context, target_part_id)
|
||
quality_warnings = self._verified_edit_quality_warnings(
|
||
context,
|
||
before_stats,
|
||
after_stats,
|
||
before_part_stats,
|
||
after_part_stats,
|
||
before_quality,
|
||
after_quality,
|
||
after_model=self.model,
|
||
)
|
||
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}"
|
||
f"{self._edit_failure_diagnostics(context)}"
|
||
) from exc
|
||
model_polydata = None
|
||
edge_polydata = None
|
||
try:
|
||
deflection = float(context.get("edit_result_deflection", 1.6))
|
||
face_polydata = self.model.build_face_polydata(deflection=deflection)
|
||
model_polydata = _smooth_surface_polydata(face_polydata)
|
||
edge_polydata = self.model.build_edge_polydata(
|
||
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": quality_warnings,
|
||
"after_geometry": after_geometry,
|
||
"model_polydata": model_polydata,
|
||
"edge_polydata": edge_polydata,
|
||
}
|
||
|
||
return job
|
||
|
||
def _run_isolated_edit_job(
|
||
self,
|
||
*,
|
||
context: dict[str, object],
|
||
isolation: dict[str, object],
|
||
snapshot: dict[object, object],
|
||
before_stats,
|
||
before_part_stats,
|
||
before_quality: dict[str, object] | None,
|
||
before_geometry: dict[str, object],
|
||
) -> dict[str, object]:
|
||
if self.model is None:
|
||
raise RuntimeError("Model is not loaded.")
|
||
target_part_id = self._edit_context_part_id(context)
|
||
timeout_seconds = float(isolation.get("timeout_seconds") or 180.0)
|
||
operation = str(isolation.get("operation") or "").strip()
|
||
args = list(isolation.get("args") or [])
|
||
if not operation:
|
||
raise RuntimeError("隔离执行缺少操作名称。")
|
||
|
||
project_root = Path(__file__).resolve().parent.parent
|
||
with tempfile.TemporaryDirectory(prefix="geom_param_isolated_edit_") as temp_dir:
|
||
temp_root = Path(temp_dir)
|
||
input_path = temp_root / "input.step"
|
||
output_path = temp_root / "output.step"
|
||
request_path = temp_root / "request.json"
|
||
self.model.export_all(input_path)
|
||
request_path.write_text(
|
||
json.dumps(
|
||
{
|
||
"input_path": str(input_path),
|
||
"output_path": str(output_path),
|
||
"operation": operation,
|
||
"args": args,
|
||
},
|
||
ensure_ascii=False,
|
||
indent=2,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
command = self._isolated_edit_command(request_path)
|
||
self.isolated_edit_cancel_requested = False
|
||
process: subprocess.Popen[str] | None = None
|
||
try:
|
||
process = subprocess.Popen(
|
||
command,
|
||
cwd=project_root,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
)
|
||
self.active_isolated_edit_process = process
|
||
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
||
completed = subprocess.CompletedProcess(command, process.returncode, stdout, stderr)
|
||
except subprocess.TimeoutExpired as exc:
|
||
self._terminate_isolated_edit_process(process)
|
||
try:
|
||
stdout, stderr = process.communicate(timeout=5.0) if process is not None else ("", "")
|
||
except Exception:
|
||
stdout, stderr = "", ""
|
||
raise RuntimeError(
|
||
f"隔离子进程执行超时,已终止危险计算;主程序和原模型保持不变。"
|
||
f" 超时时间: {timeout_seconds:g}s"
|
||
f"{self._edit_failure_diagnostics(context)}"
|
||
) from exc
|
||
finally:
|
||
if getattr(self, "active_isolated_edit_process", None) is process:
|
||
self.active_isolated_edit_process = None
|
||
|
||
if getattr(self, "isolated_edit_cancel_requested", False):
|
||
raise RuntimeError("隔离子进程已取消;主程序和原模型保持不变。")
|
||
|
||
response_path = request_path.with_suffix(".response.json")
|
||
response: dict[str, object] = {}
|
||
if response_path.exists():
|
||
try:
|
||
response = json.loads(response_path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
response = {}
|
||
if completed.returncode != 0 or not bool(response.get("ok", False)):
|
||
error = str(response.get("error") or completed.stderr or completed.stdout or "未知错误").strip()
|
||
raise RuntimeError(
|
||
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。"
|
||
f" 子进程返回码: {completed.returncode}. 错误: {error}"
|
||
f"{self._edit_failure_diagnostics(context)}"
|
||
)
|
||
if not output_path.exists():
|
||
raise RuntimeError("隔离子进程报告成功,但没有生成结果 STEP;原模型保持不变。")
|
||
|
||
new_model = StepModel.load(output_path)
|
||
try:
|
||
new_model.filename = self.step_path
|
||
except Exception:
|
||
pass
|
||
child_message = str(response.get("message") or "隔离子进程编辑完成。")
|
||
self._preserve_isolated_face_logical_id(new_model, context, child_message)
|
||
after_snapshot = new_model.snapshot()
|
||
after_stats = new_model.stats()
|
||
after_part_stats = self._part_stats_or_none_for_model(new_model, target_part_id)
|
||
after_quality = self._edit_quality_info_or_none(new_model, context, target_part_id)
|
||
quality_warnings = self._verified_edit_quality_warnings(
|
||
context,
|
||
before_stats,
|
||
after_stats,
|
||
before_part_stats,
|
||
after_part_stats,
|
||
before_quality,
|
||
after_quality,
|
||
after_model=new_model,
|
||
)
|
||
after_geometry: dict[str, object] = {}
|
||
model_polydata = None
|
||
edge_polydata = None
|
||
try:
|
||
deflection = float(context.get("edit_result_deflection", 1.6))
|
||
face_polydata = new_model.build_face_polydata(deflection=deflection)
|
||
model_polydata = _smooth_surface_polydata(face_polydata)
|
||
edge_polydata = new_model.build_edge_polydata(
|
||
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": f"{child_message} 已通过隔离子进程完成;如果 OCC 崩溃,主程序不会被带崩。",
|
||
"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,
|
||
"after_model": new_model,
|
||
"quality_warnings": quality_warnings,
|
||
"after_geometry": after_geometry,
|
||
"model_polydata": model_polydata,
|
||
"edge_polydata": edge_polydata,
|
||
}
|
||
|
||
def _isolated_edit_command(self, request_path: Path) -> list[str]:
|
||
if getattr(sys, "frozen", False):
|
||
return [sys.executable, "--isolated-edit-worker", str(request_path)]
|
||
return [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)]
|
||
|
||
def _terminate_isolated_edit_process(self, process: subprocess.Popen | None = None) -> bool:
|
||
target = process or getattr(self, "active_isolated_edit_process", None)
|
||
if target is None:
|
||
return False
|
||
try:
|
||
if target.poll() is not None:
|
||
return False
|
||
except Exception:
|
||
return False
|
||
self.isolated_edit_cancel_requested = True
|
||
try:
|
||
target.terminate()
|
||
return True
|
||
except Exception:
|
||
try:
|
||
target.kill()
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def _cancel_active_edit_for_close(self) -> bool:
|
||
if not (self.operation_in_progress or (self.edit_thread is not None and self.edit_thread.isRunning())):
|
||
return False
|
||
if not self._terminate_isolated_edit_process():
|
||
return False
|
||
self.close_after_edit_cancel = True
|
||
self.statusBar().showMessage("正在取消后台几何计算,子进程已请求终止...")
|
||
return True
|
||
|
||
def _preserve_isolated_face_logical_id(
|
||
self,
|
||
model: StepModel,
|
||
context: dict[str, object],
|
||
result_message: str,
|
||
) -> None:
|
||
if not self._context_should_preserve_face_logical_id(context):
|
||
return
|
||
target_logical_id = context.get("target_logical_id")
|
||
if target_logical_id is None:
|
||
return
|
||
located_text = _message_field(result_message, "nearest_face") or _message_field(result_message, "verified_face")
|
||
if located_text is None:
|
||
return
|
||
try:
|
||
face_id = int(float(located_text))
|
||
logical_id = int(target_logical_id)
|
||
except (TypeError, ValueError):
|
||
return
|
||
candidate_ids: list[int] = []
|
||
if 0 <= face_id < len(model.faces):
|
||
candidate_ids.append(face_id)
|
||
parameters = context.get("parameters")
|
||
parameters = parameters if isinstance(parameters, dict) else {}
|
||
target_position = _float_or_none(parameters.get("target_plane_position"))
|
||
plane_direction = (
|
||
_unit_triple_or_none(parameters.get("plane_direction"))
|
||
or _unit_triple_or_none(parameters.get("outward_direction"))
|
||
)
|
||
if target_position is not None and plane_direction is not None:
|
||
candidate_ids.extend(
|
||
self._face_ids_at_plane_position(
|
||
model,
|
||
target_position,
|
||
plane_direction,
|
||
_float_or_none(parameters.get("bbox_diagonal")),
|
||
self._edit_integrity_int_or_none(parameters.get("part_id")),
|
||
self._edit_integrity_int_or_none(parameters.get("solid_id")),
|
||
)
|
||
)
|
||
for candidate_id in candidate_ids:
|
||
if not (0 <= int(candidate_id) < len(model.faces)):
|
||
continue
|
||
try:
|
||
face_ids = model.face_region_ids(int(candidate_id)) or [int(candidate_id)]
|
||
except Exception:
|
||
face_ids = [int(candidate_id)]
|
||
try:
|
||
model.assign_logical_face_region_exclusive(logical_id, face_ids)
|
||
return
|
||
except Exception:
|
||
continue
|
||
|
||
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):
|
||
return self._part_stats_or_none_for_model(self.model, part_id)
|
||
|
||
def _part_stats_or_none_for_model(self, model: StepModel | None, part_id: int | None):
|
||
if model is None or part_id is None:
|
||
return None
|
||
try:
|
||
return model.part_topology_stats(part_id)
|
||
except Exception:
|
||
return None
|
||
|
||
def _edit_quality_info_or_none(
|
||
self,
|
||
model: StepModel | None,
|
||
context: dict[str, object],
|
||
target_part_id: int | None,
|
||
) -> dict[str, object] | None:
|
||
if model is None:
|
||
return None
|
||
try:
|
||
if target_part_id is not None:
|
||
return model.export_quality_info("part", target_part_id)
|
||
return model.export_quality_info("all", None)
|
||
except Exception:
|
||
try:
|
||
return model.export_quality_info("all", None)
|
||
except Exception:
|
||
return None
|
||
|
||
def _verified_edit_quality_warnings(
|
||
self,
|
||
context: dict[str, object],
|
||
before_stats,
|
||
after_stats,
|
||
before_part_stats,
|
||
after_part_stats,
|
||
before_quality: dict[str, object] | None,
|
||
after_quality: dict[str, object] | None,
|
||
*,
|
||
after_model: StepModel | None = None,
|
||
) -> list[str]:
|
||
warnings = _edit_quality_warnings(before_part_stats, after_part_stats)
|
||
blockers: list[str] = []
|
||
|
||
if getattr(after_stats, "solids", 0) <= 0:
|
||
blockers.append("编辑结果里没有检测到 Solid,模型已保持在修改前状态。")
|
||
if getattr(after_stats, "faces", 0) <= 0:
|
||
blockers.append("编辑结果里没有检测到 Face,模型已保持在修改前状态。")
|
||
if getattr(after_stats, "edges", 0) <= 0:
|
||
blockers.append("编辑结果里没有检测到 Edge,模型已保持在修改前状态。")
|
||
|
||
if after_quality is not None:
|
||
quality_status = str(after_quality.get("quality_status", ""))
|
||
after_brep_valid = after_quality.get("brep_valid")
|
||
before_brep_valid = before_quality.get("brep_valid") if before_quality is not None else None
|
||
quality_warnings = str(after_quality.get("quality_warnings", "")).strip()
|
||
if quality_status == "blocked":
|
||
blockers.append(quality_warnings or "编辑结果质量检查被判定为不可用。")
|
||
elif after_brep_valid is False and before_brep_valid is not False:
|
||
blockers.append(quality_warnings or "编辑结果没有通过 B-Rep 校验。")
|
||
elif quality_warnings:
|
||
warnings.append(quality_warnings)
|
||
|
||
if self._context_is_face_parameter_edit(context):
|
||
if (
|
||
before_part_stats is not None
|
||
and after_part_stats is not None
|
||
and getattr(before_part_stats, "solids", 0) > 0
|
||
and getattr(after_part_stats, "solids", 0) != getattr(before_part_stats, "solids", 0)
|
||
):
|
||
blockers.append(
|
||
"Face 参数化编辑后目标特征的 Solid 数量发生变化,已阻止接受该结果。"
|
||
f" Solid: {before_part_stats.solids} -> {after_part_stats.solids}。"
|
||
)
|
||
|
||
target_blocker = self._face_target_integrity_blocker(after_model, context)
|
||
if target_blocker:
|
||
blockers.append(target_blocker)
|
||
|
||
if blockers:
|
||
raise RuntimeError(" ".join(blockers))
|
||
return warnings
|
||
|
||
def _face_target_integrity_blocker(
|
||
self,
|
||
model: StepModel | None,
|
||
context: dict[str, object],
|
||
) -> str:
|
||
if model is None:
|
||
return ""
|
||
parameters = context.get("parameters")
|
||
if not isinstance(parameters, dict):
|
||
return ""
|
||
surface = str(parameters.get("surface") or "")
|
||
face_ids = self._face_target_candidate_ids(model, context)
|
||
if not face_ids:
|
||
if surface in {"cone", "sphere", "torus"} and any(
|
||
parameters.get(key) not in {"", None}
|
||
for key in (
|
||
"target_reference_radius",
|
||
"target_semi_angle_degrees",
|
||
"target_radius",
|
||
"target_major_radius",
|
||
"target_minor_radius",
|
||
)
|
||
):
|
||
return f"Face 编辑结果里没有找到目标 {surface} 面,模型已恢复到修改前状态。"
|
||
return ""
|
||
|
||
checks: list[tuple[str, bool]] = []
|
||
target_area = _float_or_none(parameters.get("target_area"))
|
||
if target_area is not None and target_area > 0:
|
||
checks.append(("面积", self._face_target_area_matches(model, face_ids, target_area)))
|
||
|
||
target_width = _float_or_none(parameters.get("target_face_width"))
|
||
target_height = _float_or_none(parameters.get("target_face_height"))
|
||
if target_width is not None or target_height is not None:
|
||
checks.append(("U向尺寸/V向尺寸", self._face_target_size_matches(model, face_ids, target_width, target_height)))
|
||
|
||
target_center = _triple_or_none(parameters.get("target_face_center"))
|
||
if target_center is not None:
|
||
checks.append(("中心", self._face_target_center_matches(model, face_ids, target_center)))
|
||
|
||
target_position = _float_or_none(parameters.get("target_plane_position"))
|
||
plane_direction = (
|
||
_unit_triple_or_none(parameters.get("plane_direction"))
|
||
or _unit_triple_or_none(parameters.get("outward_direction"))
|
||
)
|
||
if target_position is not None and plane_direction is not None:
|
||
checks.append(
|
||
(
|
||
"偏移曲面",
|
||
self._face_target_plane_position_matches(
|
||
model,
|
||
face_ids,
|
||
target_position,
|
||
plane_direction,
|
||
_float_or_none(parameters.get("bbox_diagonal")),
|
||
),
|
||
)
|
||
)
|
||
|
||
target_thickness = _float_or_none(parameters.get("shell_target_thickness"))
|
||
if target_thickness is not None and target_thickness > 0:
|
||
checks.append(
|
||
(
|
||
"壳体厚度",
|
||
self._face_target_shell_thickness_matches(model, face_ids, target_thickness),
|
||
)
|
||
)
|
||
|
||
if surface == "sphere":
|
||
target_radius = _float_or_none(parameters.get("target_radius"))
|
||
if target_radius is not None and target_radius > 0:
|
||
checks.append(("半径", self._face_target_numeric_info_matches(model, face_ids, "radius", target_radius)))
|
||
elif surface == "cone":
|
||
resize_strategy = str(parameters.get("resize_strategy") or "")
|
||
embedded_target_small_radius = _float_or_none(parameters.get("embedded_cone_target_small_radius"))
|
||
embedded_target_large_radius = _float_or_none(parameters.get("embedded_cone_target_large_radius"))
|
||
if (
|
||
embedded_target_small_radius is not None
|
||
and embedded_target_small_radius > 0
|
||
and embedded_target_large_radius is not None
|
||
and embedded_target_large_radius > embedded_target_small_radius
|
||
):
|
||
checks.append(
|
||
(
|
||
"锥孔边界半径",
|
||
self._face_target_cone_boundary_radii_match(
|
||
model,
|
||
face_ids,
|
||
embedded_target_small_radius,
|
||
embedded_target_large_radius,
|
||
),
|
||
)
|
||
)
|
||
target_reference_radius = _float_or_none(parameters.get("target_reference_radius"))
|
||
if (
|
||
target_reference_radius is not None
|
||
and target_reference_radius > 0
|
||
and "semi-angle" not in resize_strategy
|
||
and "bounded-cone-recut" not in resize_strategy
|
||
):
|
||
checks.append(
|
||
(
|
||
"参考半径",
|
||
self._face_target_numeric_info_matches(
|
||
model,
|
||
face_ids,
|
||
"reference_radius",
|
||
target_reference_radius,
|
||
),
|
||
)
|
||
)
|
||
target_semi_angle_degrees = _float_or_none(parameters.get("target_semi_angle_degrees"))
|
||
if target_semi_angle_degrees is not None and target_semi_angle_degrees > 0:
|
||
checks.append(
|
||
(
|
||
"半角",
|
||
self._face_target_angle_degrees_matches(model, face_ids, target_semi_angle_degrees),
|
||
)
|
||
)
|
||
elif surface == "torus":
|
||
target_major_radius = _float_or_none(parameters.get("target_major_radius"))
|
||
if target_major_radius is not None and target_major_radius > 0:
|
||
checks.append(
|
||
(
|
||
"主半径",
|
||
self._face_target_numeric_info_matches(
|
||
model,
|
||
face_ids,
|
||
"major_radius",
|
||
target_major_radius,
|
||
),
|
||
)
|
||
)
|
||
target_minor_radius = _float_or_none(parameters.get("target_minor_radius"))
|
||
if target_minor_radius is not None and target_minor_radius > 0:
|
||
checks.append(
|
||
(
|
||
"小半径",
|
||
self._face_target_numeric_info_matches(
|
||
model,
|
||
face_ids,
|
||
"minor_radius",
|
||
target_minor_radius,
|
||
),
|
||
)
|
||
)
|
||
|
||
failed = [name for name, ok in checks if not ok]
|
||
if not failed:
|
||
return ""
|
||
return (
|
||
"Face 编辑结果没有达到目标值;"
|
||
f"未通过的检查: {', '.join(failed)}。"
|
||
"模型已恢复到修改前状态。"
|
||
)
|
||
|
||
def _face_target_candidate_ids(self, model: StepModel, context: dict[str, object]) -> list[int]:
|
||
parameters = context.get("parameters")
|
||
parameters = parameters if isinstance(parameters, dict) else {}
|
||
candidates: list[int] = []
|
||
logical_id = context.get("target_logical_id")
|
||
if logical_id is not None:
|
||
try:
|
||
candidates.extend(model.face_ids_for_logical_id(int(logical_id)))
|
||
except Exception:
|
||
pass
|
||
target_id = context.get("target_id")
|
||
try:
|
||
numeric_target_id = int(target_id) if target_id is not None else -1
|
||
except (TypeError, ValueError):
|
||
numeric_target_id = -1
|
||
if 0 <= numeric_target_id < len(model.faces):
|
||
candidates.append(numeric_target_id)
|
||
if not candidates:
|
||
candidates = list(range(len(model.faces)))
|
||
|
||
part_id = self._edit_integrity_int_or_none(parameters.get("part_id"))
|
||
solid_id = self._edit_integrity_int_or_none(parameters.get("solid_id"))
|
||
surface = str(parameters.get("surface") or "")
|
||
target_keys = (
|
||
"target_area",
|
||
"target_face_width",
|
||
"target_face_height",
|
||
"target_face_center",
|
||
"target_plane_position",
|
||
"shell_target_thickness",
|
||
"target_reference_radius",
|
||
"target_semi_angle_degrees",
|
||
"embedded_cone_target_small_radius",
|
||
"embedded_cone_target_large_radius",
|
||
"target_radius",
|
||
"target_major_radius",
|
||
"target_minor_radius",
|
||
)
|
||
has_target_value = any(parameters.get(key) not in {"", None} for key in target_keys)
|
||
|
||
def filtered(source_ids, *, require_solid: bool = True) -> list[int]:
|
||
result: list[int] = []
|
||
for face_id in source_ids:
|
||
if not (0 <= int(face_id) < len(model.faces)):
|
||
continue
|
||
if part_id is not None and int(model.face_part_ids[int(face_id)]) != part_id:
|
||
continue
|
||
if (
|
||
require_solid
|
||
and solid_id is not None
|
||
and solid_id >= 0
|
||
and int(model.face_solid_ids[int(face_id)]) != solid_id
|
||
):
|
||
continue
|
||
if surface in {"plane", "cylinder", "cone", "sphere", "torus"}:
|
||
try:
|
||
if model.face_surface_kind(int(face_id)) != surface:
|
||
continue
|
||
except Exception:
|
||
continue
|
||
if int(face_id) not in result:
|
||
result.append(int(face_id))
|
||
return result
|
||
|
||
result = filtered(candidates)
|
||
if result:
|
||
if surface == "plane" and parameters.get("target_plane_position") not in {"", None}:
|
||
for face_id in filtered(range(len(model.faces))):
|
||
if face_id not in result:
|
||
result.append(face_id)
|
||
return result
|
||
if surface in {"plane", "cylinder", "cone", "sphere", "torus"} and has_target_value:
|
||
result = filtered(range(len(model.faces)))
|
||
if result:
|
||
return result
|
||
if solid_id is not None and solid_id >= 0:
|
||
result = filtered(range(len(model.faces)), require_solid=False)
|
||
return result
|
||
|
||
def _edit_integrity_int_or_none(self, value: object) -> int | None:
|
||
if value in {"", None}:
|
||
return None
|
||
try:
|
||
return int(value)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
def _face_target_area_matches(self, model: StepModel, face_ids: list[int], target_area: float) -> bool:
|
||
tolerance = max(abs(target_area) * 0.02, 1e-4)
|
||
for face_id in face_ids:
|
||
try:
|
||
area = _float_or_none(model.face_info(face_id).get("area"))
|
||
except Exception:
|
||
area = None
|
||
if area is not None and abs(area - target_area) <= tolerance:
|
||
return True
|
||
return False
|
||
|
||
def _face_target_size_matches(
|
||
self,
|
||
model: StepModel,
|
||
face_ids: list[int],
|
||
target_width: float | None,
|
||
target_height: float | None,
|
||
) -> bool:
|
||
for face_id in face_ids:
|
||
try:
|
||
info = model.face_info(face_id)
|
||
except Exception:
|
||
continue
|
||
width = _float_or_none(info.get("local_face_width"))
|
||
height = _float_or_none(info.get("local_face_height"))
|
||
if target_width is not None:
|
||
if width is None or abs(width - target_width) > max(abs(target_width) * 0.02, 1e-4):
|
||
continue
|
||
if target_height is not None:
|
||
if height is None or abs(height - target_height) > max(abs(target_height) * 0.02, 1e-4):
|
||
continue
|
||
return True
|
||
return False
|
||
|
||
def _face_target_center_matches(
|
||
self,
|
||
model: StepModel,
|
||
face_ids: list[int],
|
||
target_center: tuple[float, float, float],
|
||
) -> bool:
|
||
reference = max(max(abs(item) for item in target_center), 1.0)
|
||
tolerance = max(reference * 1e-5, 1e-4)
|
||
for face_id in face_ids:
|
||
try:
|
||
info = model.face_info(face_id)
|
||
except Exception:
|
||
continue
|
||
center = _triple_or_none(info.get("area_center")) or _triple_or_none(info.get("bbox_center"))
|
||
if center is None:
|
||
continue
|
||
distance = math.sqrt(sum((center[index] - target_center[index]) ** 2 for index in range(3)))
|
||
if distance <= tolerance:
|
||
return True
|
||
return False
|
||
|
||
def _face_target_plane_position_matches(
|
||
self,
|
||
model: StepModel,
|
||
face_ids: list[int],
|
||
target_position: float,
|
||
direction: tuple[float, float, float],
|
||
bbox_diagonal: float | None,
|
||
) -> bool:
|
||
tolerance = max((bbox_diagonal or 1.0) * 1e-4, abs(target_position) * 1e-5, 1e-4)
|
||
for face_id in face_ids:
|
||
try:
|
||
info = model.quick_face_info(face_id)
|
||
except Exception:
|
||
continue
|
||
if str(info.get("surface") or "") != "plane":
|
||
continue
|
||
origin = _triple_or_none(info.get("plane_origin"))
|
||
if origin is None:
|
||
continue
|
||
position = sum(origin[index] * direction[index] for index in range(3))
|
||
if abs(position - target_position) <= tolerance:
|
||
return True
|
||
return False
|
||
|
||
def _face_ids_at_plane_position(
|
||
self,
|
||
model: StepModel,
|
||
target_position: float,
|
||
direction: tuple[float, float, float],
|
||
bbox_diagonal: float | None,
|
||
part_id: int | None,
|
||
solid_id: int | None,
|
||
) -> list[int]:
|
||
tolerance = max((bbox_diagonal or 1.0) * 1e-4, abs(target_position) * 1e-5, 1e-4)
|
||
matches: list[int] = []
|
||
for face_id in range(len(model.faces)):
|
||
if part_id is not None and int(model.face_part_ids[face_id]) != part_id:
|
||
continue
|
||
if solid_id is not None and solid_id >= 0 and int(model.face_solid_ids[face_id]) != solid_id:
|
||
continue
|
||
try:
|
||
info = model.quick_face_info(face_id)
|
||
except Exception:
|
||
continue
|
||
if str(info.get("surface") or "") != "plane":
|
||
continue
|
||
origin = _triple_or_none(info.get("plane_origin"))
|
||
if origin is None:
|
||
continue
|
||
position = sum(origin[index] * direction[index] for index in range(3))
|
||
if abs(position - target_position) <= tolerance:
|
||
matches.append(face_id)
|
||
return matches
|
||
|
||
def _face_target_shell_thickness_matches(
|
||
self,
|
||
model: StepModel,
|
||
face_ids: list[int],
|
||
target_thickness: float,
|
||
) -> bool:
|
||
tolerance = max(abs(target_thickness) * 0.03, 1e-4)
|
||
for face_id in face_ids:
|
||
try:
|
||
info = model.feature_info(face_id)
|
||
except Exception:
|
||
continue
|
||
current = _float_or_none(info.get("shell_thickness_estimate"))
|
||
if current is not None and abs(current - target_thickness) <= tolerance:
|
||
return True
|
||
return False
|
||
|
||
def _face_target_numeric_info_matches(
|
||
self,
|
||
model: StepModel,
|
||
face_ids: list[int],
|
||
info_key: str,
|
||
target_value: float,
|
||
) -> bool:
|
||
tolerance = max(abs(target_value) * 0.01, 1e-4)
|
||
for face_id in face_ids:
|
||
try:
|
||
value = _float_or_none(model.face_info(face_id).get(info_key))
|
||
except Exception:
|
||
value = None
|
||
if value is not None and abs(value - target_value) <= tolerance:
|
||
return True
|
||
return False
|
||
|
||
def _face_target_cone_boundary_radii_match(
|
||
self,
|
||
model: StepModel,
|
||
face_ids: list[int],
|
||
target_small_radius: float,
|
||
target_large_radius: float,
|
||
) -> bool:
|
||
tolerance = max(abs(target_large_radius) * 0.01, abs(target_small_radius) * 0.01, 1e-4)
|
||
for face_id in face_ids:
|
||
try:
|
||
info = model.face_info(face_id)
|
||
axis_point = _triple_or_none(info.get("axis_point"))
|
||
axis_direction = _unit_triple_or_none(info.get("axis"))
|
||
if axis_point is None or axis_direction is None:
|
||
continue
|
||
circles = model._conical_face_circle_boundaries(face_id, axis_point, axis_direction)
|
||
except Exception:
|
||
continue
|
||
radii = sorted(float(circle["radius"]) for circle in circles)
|
||
if len(radii) != 2:
|
||
continue
|
||
if abs(radii[0] - target_small_radius) <= tolerance and abs(radii[1] - target_large_radius) <= tolerance:
|
||
return True
|
||
return False
|
||
|
||
def _face_target_angle_degrees_matches(
|
||
self,
|
||
model: StepModel,
|
||
face_ids: list[int],
|
||
target_degrees: float,
|
||
) -> bool:
|
||
tolerance = max(abs(target_degrees) * 0.01, 0.05)
|
||
for face_id in face_ids:
|
||
try:
|
||
angle = _float_or_none(model.face_info(face_id).get("semi_angle"))
|
||
except Exception:
|
||
angle = None
|
||
if angle is None:
|
||
continue
|
||
degrees = abs(math.degrees(angle))
|
||
if abs(degrees - target_degrees) <= tolerance:
|
||
return True
|
||
return False
|
||
|
||
def _context_is_face_parameter_edit(self, context: dict[str, object]) -> bool:
|
||
parameters = context.get("parameters")
|
||
if not isinstance(parameters, dict):
|
||
return False
|
||
strategy = str(parameters.get("resize_strategy") or parameters.get("translate_strategy") or "")
|
||
if any(
|
||
token in strategy
|
||
for token in (
|
||
"face",
|
||
"plane-offset",
|
||
"push-pull-planar",
|
||
"push-pull-shell",
|
||
"shell-thickness",
|
||
"cone",
|
||
"sphere",
|
||
"torus",
|
||
"radial-affine-scale",
|
||
)
|
||
):
|
||
return True
|
||
operation = str(context.get("operation_name") or "")
|
||
target_kind = str(context.get("target_kind") or "")
|
||
face_target_keys = (
|
||
"target_area",
|
||
"target_face_width",
|
||
"target_face_height",
|
||
"target_face_center",
|
||
"target_plane_position",
|
||
"shell_target_thickness",
|
||
"target_reference_radius",
|
||
"target_semi_angle_degrees",
|
||
"embedded_cone_target_small_radius",
|
||
"embedded_cone_target_large_radius",
|
||
"target_radius",
|
||
"target_major_radius",
|
||
"target_minor_radius",
|
||
)
|
||
if target_kind in {"face", "feature"} and any(parameters.get(key) not in {"", None} for key in face_target_keys):
|
||
return True
|
||
return target_kind in {"face", "feature"} and any(
|
||
token in operation
|
||
for token in (
|
||
"Face",
|
||
"面",
|
||
"面积",
|
||
"中心",
|
||
"U向尺寸",
|
||
"V向尺寸",
|
||
"偏移曲面",
|
||
"壳体",
|
||
"拉伸/切除",
|
||
"圆锥",
|
||
"球面",
|
||
"环面",
|
||
)
|
||
)
|
||
|
||
def _context_should_preserve_face_logical_id(self, context: dict[str, object]) -> bool:
|
||
if self._context_is_face_parameter_edit(context):
|
||
return True
|
||
target_kind = str(context.get("target_kind") or "")
|
||
if target_kind not in {"face", "feature"}:
|
||
return False
|
||
return context.get("target_id") not in {"", None} and context.get("target_logical_id") not in {"", 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} 正在后台计算;槽宽会在后台换算并重建孔/槽几何。"
|
||
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 "面积(局部重建)" in operation_name:
|
||
progress_text = f"{operation_name} 正在后台计算;当前会在面内移动所选 Face 顶点并重建相邻平面。"
|
||
elif "U向尺寸(缩放特征)" in operation_name or "V向尺寸(缩放特征)" in operation_name:
|
||
progress_text = f"{operation_name} 正在后台计算;当前会沿选中 Face 的平面内方向缩放所属特征或 Solid,其它几何会跟随变化。"
|
||
elif "U向尺寸(局部重建)" in operation_name or "V向尺寸(局部重建)" 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 "中心(局部重建)" 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} 正在后台计算。"
|
||
self._start_edit_busy_status(progress_text)
|
||
if self.edit_preview_timer is not None:
|
||
self.edit_preview_timer.stop()
|
||
QApplication.processEvents()
|
||
|
||
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
|
||
after_model = result.get("after_model")
|
||
if after_model is not None:
|
||
self.model = after_model
|
||
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:
|
||
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(
|
||
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
|
||
close_after_cancel = bool(getattr(self, "close_after_edit_cancel", False))
|
||
if close_after_cancel:
|
||
self.close_after_edit_cancel = False
|
||
self._end_edit_task(clear_preview=True)
|
||
if not close_after_cancel:
|
||
QMessageBox.critical(self, "操作失败", message)
|
||
self._clear_editable_candidates()
|
||
self._clear_cylinder_candidates()
|
||
self.statusBar().showMessage("后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "操作失败,模型已保持在编辑前状态")
|
||
if close_after_cancel:
|
||
if self.edit_thread is not None and self.edit_thread.isRunning():
|
||
self.edit_thread.quit()
|
||
self.edit_thread.wait(1500)
|
||
self.close()
|
||
|
||
def _restore_failed_edit_snapshot(self, snapshot: object) -> str:
|
||
if self.model is None or not isinstance(snapshot, dict):
|
||
return "未找到可用的编辑前快照,请重新加载 STEP 文件确认状态。"
|
||
try:
|
||
self.model.restore_snapshot(snapshot)
|
||
self._reset_selection()
|
||
self._populate_part_tree()
|
||
self._rebuild_scene(reset_camera=False)
|
||
self._clear_editable_candidates()
|
||
self._clear_cylinder_candidates()
|
||
except Exception as rollback_exc:
|
||
return f"尝试回滚到编辑前状态失败:{rollback_exc}。建议重新加载 STEP 文件。"
|
||
return "模型已回滚到编辑前状态。"
|
||
|
||
def _end_edit_task(self, clear_preview: bool = True) -> None:
|
||
self.operation_in_progress = False
|
||
self._stop_edit_busy_status()
|
||
self.pending_edit_context = None
|
||
if clear_preview:
|
||
self.clear_edit_preview(render=False)
|
||
self._update_action_states()
|
||
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'])}")
|
||
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'])}")
|
||
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'])}")
|
||
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)})",
|
||
"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')}",
|
||
]
|
||
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,
|
||
)
|
||
|