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

8404 lines
404 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"建模意图: {_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"建模意图: {_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,
)