from __future__ import annotations from datetime import datetime import math from pathlib import Path import vtk from PySide6.QtCore import Qt, QThread, QTimer, Slot from PySide6.QtWidgets import ( QApplication, QFileDialog, QMessageBox, QTableWidgetItem, QTreeWidgetItem, ) from .model import StepModel from .records import OperationRecord from .ui_helpers import * # noqa: F403 from .workers import EditWorker, LoadWorker, ScanWorker def _message_field(message: str, key: str) -> str | None: marker = f"{key}=" start = message.find(marker) if start < 0: return None start += len(marker) end = message.find(",", start) if end < 0: end = len(message) value = message[start:end].strip().rstrip(".") return value or None class WindowActionMixin: def export_all(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再导出。"): return if not self._confirm_export_quality("all"): return target, _ = QFileDialog.getSaveFileName( self, "导出当前完整 STEP", str(self.step_path.parent / f"{self.step_path.stem}_edited.step"), "STEP 文件 (*.step *.stp);;所有文件 (*.*)", ) if not target: return self._run_action(lambda: self.model.export_all(target), f"已导出 {Path(target).name}") def export_selected_part(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再导出。"): return if self.selected_part_id is None: QMessageBox.information(self, "未选择零件", "请先选择一个零件。") return if not self._confirm_export_quality("part", self.selected_part_id): return part = self.model.part_by_id(self.selected_part_id) default_name = f"{part.name if part else 'part'}_export.step".replace(" ", "_") target, _ = QFileDialog.getSaveFileName( self, "导出选中零件", str(self.step_path.parent / default_name), "STEP 文件 (*.step *.stp);;所有文件 (*.*)", ) if not target: return self._run_action( lambda: self.model.export_part(self.selected_part_id, target), f"已导出选中零件到 {Path(target).name}", ) def export_selected_solid(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再导出。"): return if self.selected_solid_id is None: QMessageBox.information(self, "未选择 Solid", "请先选择一个 Solid,或选择一个属于 Solid 的 Face。") return if not self._confirm_export_quality("solid", self.selected_solid_id): return target, _ = QFileDialog.getSaveFileName( self, "导出选中 Solid", str(self.step_path.parent / f"solid_{self.selected_solid_id}_export.step"), "STEP 文件 (*.step *.stp);;所有文件 (*.*)", ) if not target: return self._run_action( lambda: self.model.export_solid(self.selected_solid_id, target), f"已导出选中 Solid 到 {Path(target).name}", ) def export_selected_face(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再导出。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择 Face", "请先切换到 Face 或特征模式并选择一个 Face。") return if not self._confirm_export_quality("face", self.selected_face_id): return target, _ = QFileDialog.getSaveFileName( self, "导出选中面区域", str(self.step_path.parent / f"face_region_{self.selected_face_id}_export.step"), "STEP 文件 (*.step *.stp);;所有文件 (*.*)", ) if not target: return self._run_action( lambda: self.model.export_face(self.selected_face_id, target), f"已导出选中面区域到 {Path(target).name}", ) def export_selected_feature(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再导出。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择特征", "请先切换到特征模式并选择一个特征。") return if not self._confirm_export_quality("feature", self.selected_face_id): return target, _ = QFileDialog.getSaveFileName( self, "导出选中特征区域", str(self.step_path.parent / f"feature_face_{self.selected_face_id}_export.step"), "STEP 文件 (*.step *.stp);;所有文件 (*.*)", ) if not target: return self._run_action( lambda: self.model.export_feature(self.selected_face_id, target), f"已导出选中特征区域到 {Path(target).name}", ) def export_selected_edge(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再导出。"): return if self.selected_edge_id is None: QMessageBox.information(self, "未选择Edge", "请先切换到Edge模式并选择一个Edge。") return if not self._confirm_export_quality("edge", self.selected_edge_id): return target, _ = QFileDialog.getSaveFileName( self, "导出选中 Edge", str(self.step_path.parent / f"edge_{self.selected_edge_id}_export.step"), "STEP 文件 (*.step *.stp);;所有文件 (*.*)", ) if not target: return self._run_action( lambda: self.model.export_edge(self.selected_edge_id, target), f"已导出选中 Edge 到 {Path(target).name}", ) def check_export_quality(self) -> None: if self.model is None: return if self._edit_busy("编辑计算中,暂时不能检查导出质量。"): return scope, target_id = self._current_export_quality_target() try: info = self.model.export_quality_info(scope, target_id) except Exception as exc: QMessageBox.critical(self, "质量检查失败", str(exc)) self.statusBar().showMessage("导出质量检查失败") return report = _export_quality_text(info) self.set_plain_info(report) if info.get("quality_status") == "ok": self.statusBar().showMessage("导出质量检查通过") else: self.statusBar().showMessage("导出质量检查发现警告") def _current_export_quality_target(self) -> tuple[str, int | None]: if self.selected_kind == "feature" and self.selected_face_id is not None: return "feature", self.selected_face_id if self.selected_kind == "face" and self.selected_face_id is not None: return "face", self.selected_face_id if self.selected_kind == "edge" and self.selected_edge_id is not None: return "edge", self.selected_edge_id if self.selected_kind == "edge": if self.selected_solid_id is not None: return "solid", self.selected_solid_id if self.selected_part_id is not None: return "part", self.selected_part_id if self.selected_kind == "solid" and self.selected_solid_id is not None: return "solid", self.selected_solid_id if self.selected_kind == "part" and self.selected_part_id is not None: return "part", self.selected_part_id return "all", None def _confirm_export_quality(self, scope: str, target_id: int | None = None) -> bool: if self.model is None: return False try: info = self.model.export_quality_info(scope, target_id) except Exception as exc: QMessageBox.critical(self, "导出质量检查失败", str(exc)) self.statusBar().showMessage("导出质量检查失败") return False report = _export_quality_text(info) self.set_plain_info(report) if info.get("quality_status") == "ok": return True result = QMessageBox.question( self, "导出质量警告", f"{report}\n\n仍然继续导出吗?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) return result == QMessageBox.StandardButton.Yes def repair_model(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再修复模型。"): return def action(): return self.model.repair_model() self._run_edit_action( action, operation_name="修复当前模型", target="当前完整模型", parameters={"scope": "all", "repair_strategy": "ShapeFix + UnifySameDomain"}, target_kind=None, target_id=None, ) def repair_selected_shape(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再修复选中对象。"): return if self.selected_solid_id is not None: solid_id = self.selected_solid_id def action(): return self.model.repair_solid(solid_id) self._run_edit_action( action, operation_name="修复选中Solid", target=f"Solid {solid_id}", parameters={ "scope": "solid", "solid_id": solid_id, "part_id": self.selected_part_id, "repair_strategy": "ShapeFix + UnifySameDomain", }, target_kind="solid", target_id=solid_id, ) return if self.selected_part_id is not None: part_id = self.selected_part_id def action(): return self.model.repair_part(part_id) self._run_edit_action( action, operation_name="修复选中零件", target=f"零件 {part_id}", parameters={ "scope": "part", "part_id": part_id, "repair_strategy": "ShapeFix + UnifySameDomain", }, target_kind="part", target_id=part_id, ) return QMessageBox.information(self, "未选择对象", "请先选择零件、Solid、Face 或 Edge。") self.statusBar().showMessage("未选择可修复对象") def push_pull_face(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再执行新的推拉。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择 Face", "请先选择一个平面 Face。") return try: distance = float(self.offset_input.text()) except ValueError: QMessageBox.critical(self, "距离无效", "请输入数字形式的Face偏移距离。") return face_id = self.selected_face_id plan = self._quick_push_pull_plan(face_id, distance) if plan["status"] == "blocked": QMessageBox.information(self, "不能推拉平面", str(plan["message"])) self.statusBar().showMessage("推拉平面已阻止") return self._show_push_pull_preview(face_id, distance, plan=plan) def action(): return self.model.push_pull_face(face_id, distance) self._run_edit_action( action, operation_name="推拉平面", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "semantic_distance": distance, "distance_rule": "positive=outward fuse, negative=inward cut", "surface": plan.get("surface"), "outward_direction": plan.get("outward_direction"), "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"), "push_pull_risk": plan.get("risk"), "push_pull_status": plan.get("status"), "push_pull_message": plan.get("message"), "push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"), "push_pull_scope_face_count": plan.get("push_pull_scope_face_count"), "push_pull_scope_note": plan.get("push_pull_scope_note"), "bbox_diagonal": plan.get("bbox_diagonal"), }, target_kind="face", target_id=face_id, ) def _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 不是平面,不能执行 Face 偏移。", "face_id": face_id, "distance": distance, "surface": surface, } outward = info.get("push_pull_outward_direction") if not isinstance(outward, (tuple, list)) or len(outward) != 3: outward = info.get("oriented_normal") or info.get("normal") if not isinstance(outward, (tuple, list)) or len(outward) != 3: return { "status": "blocked", "risk": "blocked", "message": "当前平面缺少稳定推拉方向,请重新选择该 Face 后再试。", "face_id": face_id, "distance": distance, "surface": surface, } bbox_diagonal = _float_or_none(info.get("bbox_diagonal")) distance_abs = abs(float(distance)) risk = "low" status = "ready" warnings: list[str] = [] if distance_abs <= 1e-9: return { "status": "blocked", "risk": "blocked", "message": "Face 偏移距离为 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("Face 偏移距离相对当前 Face 尺寸较大,布尔失败或大范围变形风险较高。") elif distance_abs > bbox_diagonal * 0.08: risk = "medium" status = "caution" warnings.append("Face 偏移距离相对当前 Face 尺寸偏大,请确认预览范围。") scope_face_ids = ( _int_values(info.get("push_pull_scope_face_ids")) or _int_values(info.get("feature_highlight_face_ids")) or _int_values(info.get("same_domain_face_ids")) or [face_id] ) plane_origin = info.get("plane_origin") current_plane_position = None if isinstance(plane_origin, (tuple, list)) and len(plane_origin) == 3: try: current_plane_position = sum(float(plane_origin[index]) * float(outward[index]) for index in range(3)) except (TypeError, ValueError): current_plane_position = None target_plane_position = ( current_plane_position + float(distance) if current_plane_position is not None else None ) part_id = info.get("part_id") solid_id = info.get("solid_id") if part_id is None and self.model is not None and 0 <= face_id < len(self.model.face_part_ids): part_id = self.model.face_part_ids[face_id] if solid_id is None and self.model is not None and 0 <= face_id < len(self.model.face_solid_ids): solid_id = self.model.face_solid_ids[face_id] message = " ".join(warnings) if warnings else "可以尝试偏移该平面;完整几何检查会在后台执行。" return { "status": status, "risk": risk, "message": message, "warnings": ";".join(warnings), "blockers": "", "face_id": face_id, "part_id": part_id, "solid_id": solid_id, "distance": distance, "surface": surface, "area": info.get("area"), "bbox_diagonal": bbox_diagonal, "outward_direction": tuple(float(item) for item in outward), "current_plane_position": current_plane_position, "target_plane_position": target_plane_position, "direction_confidence": info.get("push_pull_confidence", "cached"), "direction_note": info.get("push_pull_note", "使用当前选中 Face 的缓存方向,完整检查在后台执行。"), "push_pull_scope_face_ids": tuple(scope_face_ids), "push_pull_scope_face_count": len(scope_face_ids), "push_pull_scope_note": "使用当前显示/选中缓存生成快速预览;后台会重新计算真实推拉区域。", } def resize_shell_thickness(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再调整薄壁厚度。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择平面", "请先选择一个已识别到相对面的薄壁/壳体平面候选。") return if not hasattr(self, "shell_thickness_input"): return try: target_thickness = float(self.shell_thickness_input.text()) except ValueError: QMessageBox.critical(self, "目标厚度无效", "请输入数字形式的目标薄壁厚度。") return face_id = self.selected_face_id plan = self.model.shell_thickness_plan(face_id, target_thickness) if plan["status"] == "blocked": QMessageBox.information(self, "不能调整薄壁厚度", str(plan["message"])) self.statusBar().showMessage("薄壁厚度调整已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认调整薄壁厚度", ( f"face: {plan['face_id']}\n" f"当前厚度估算: {_format_value(plan['shell_current_thickness'])}\n" f"目标厚度: {_format_value(plan['shell_target_thickness'])}\n" f"厚度变化量: {_format_value(plan['shell_delta_thickness'])}\n" f"厚度变化比例: {_format_percent(plan['shell_delta_ratio'])}\n" f"相对平面 Face: {_format_value(plan.get('shell_opposite_face_id', ''))}\n" f"相对平面重叠率: {_format_percent(plan.get('shell_overlap_ratio_estimate'))}\n" f"置信度: {_format_value(plan.get('shell_confidence', ''))}\n" f"当前面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "当前版本会移动当前平面区域,让它和相对平面的距离接近目标厚度;这不是 CAD 壳命令参数编辑。确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消薄壁厚度调整") return self._show_shell_thickness_preview(face_id, target_thickness) def action(): return self.model.resize_shell_thickness(face_id, target_thickness) self._run_edit_action( action, operation_name="调整薄壁厚度", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "shell_region_kind": plan.get("shell_region_kind"), "shell_confidence": plan.get("shell_confidence"), "shell_source_face_ids": plan.get("shell_source_face_ids"), "shell_opposite_face_id": plan.get("shell_opposite_face_id"), "shell_current_thickness": plan.get("shell_current_thickness"), "shell_target_thickness": plan.get("shell_target_thickness"), "shell_delta_thickness": plan.get("shell_delta_thickness"), "shell_delta_ratio": plan.get("shell_delta_ratio"), "shell_signed_thickness": plan.get("shell_signed_thickness"), "shell_overlap_ratio_estimate": plan.get("shell_overlap_ratio_estimate"), "shell_opposite_normal_dot": plan.get("shell_opposite_normal_dot"), "shell_desired_movement_vector": plan.get("shell_desired_movement_vector"), "shell_movement_alignment": plan.get("shell_movement_alignment"), "push_pull_distance": plan.get("push_pull_distance"), "outward_direction": plan.get("outward_direction"), "push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"), "push_pull_scope_face_count": plan.get("push_pull_scope_face_count"), "push_pull_scope_note": plan.get("push_pull_scope_note"), "resize_strategy": plan.get("resize_strategy"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="feature" if self.selected_kind == "feature" else "face", target_id=face_id, ) @staticmethod def _max_quick_risk(current: str, candidate: str) -> str: order = {"low": 0, "medium": 1, "high": 2, "blocked": 3} return current if order.get(current, 0) >= order.get(candidate, 0) else candidate def _selected_face_info_snapshot(self) -> dict[str, object]: info = dict(getattr(self, "current_info_values", {}) or {}) if self.model is None or self.selected_face_id is None: return info selected_face_id = int(self.selected_face_id) possible_ids = ( info.get("face_id"), info.get("topological_face_id"), info.get("feature_source_face_id"), info.get("face_region_source_face_id"), ) has_selected_id = False for value in possible_ids: try: if value is not None and int(value) == selected_face_id: has_selected_id = True break except (TypeError, ValueError): pass has_any_face_id = any(value is not None for value in possible_ids) if info and has_any_face_id and not has_selected_id: info = {} if info and str(info.get("kind", "")) in {"edge", "part", "solid"} and not has_selected_id: info = {} if "surface" not in info and "diameter" not in info: try: info = dict(self.model.face_info(selected_face_id)) except Exception: pass return info def _quick_cylinder_resize_plan( self, face_id: int, target_diameter: float, info: dict[str, object] | None = None, ) -> dict[str, object]: info = dict(info or self._selected_face_info_snapshot()) warnings: list[str] = [] blockers: list[str] = [] risk = "low" status = "ready" surface = str(info.get("surface", "")) current_diameter = _float_or_none(info.get("diameter")) height_estimate = _float_or_none(info.get("height_estimate")) if height_estimate is None: height_estimate = _float_or_none(info.get("same_domain_height_estimate")) if height_estimate is None: height_estimate = _float_or_none(info.get("hole_depth_estimate")) guess = str(info.get("feature_guess", "cylindrical face")) confidence = str(info.get("confidence", "low")) angular_span = _float_or_none(info.get("angular_span")) if surface != "cylinder" or current_diameter is None or current_diameter <= 1e-9: blockers.append("当前选中对象不是可识别的圆柱面,不能调整孔/槽直径。") if target_diameter <= 0: blockers.append("目标直径必须大于 0。") if guess == "round/fillet candidate": risk = self._max_quick_risk(risk, "high") warnings.append("当前圆柱面更像圆角/倒圆,调整孔径可能误切圆角。") elif guess == "boss/outer-round candidate": risk = self._max_quick_risk(risk, "high") warnings.append("当前圆柱面更像凸台或外圆,调整孔径可能切掉外部结构。") elif guess != "hole/groove candidate": risk = self._max_quick_risk(risk, "high") warnings.append("当前圆柱面还没有被稳定识别为孔/槽候选。") if guess == "hole/groove candidate" and confidence == "low": risk = self._max_quick_risk(risk, "medium") warnings.append("孔/槽判断置信度较低。") if ( guess == "hole/groove candidate" and angular_span is not None and angular_span < math.tau * 0.92 ): risk = self._max_quick_risk(risk, "medium") warnings.append("这是局部圆柱面,更像槽或半孔,不是完整圆孔。") delta_diameter = None delta_ratio = None target_to_height_ratio = None resize_mode = "unknown" if current_diameter is not None and current_diameter > 1e-9: delta_diameter = target_diameter - current_diameter delta_ratio = abs(delta_diameter) / current_diameter resize_mode = "expand" if delta_diameter > 0 else "shrink" if abs(delta_diameter) <= max(current_diameter * 1e-5, 1e-6): blockers.append("目标直径与当前直径几乎相同,不需要修改。") elif target_diameter > current_diameter * 2.0: blockers.append( "目标直径超过当前直径的 2 倍;当前版本会阻止这种极端放大,避免布尔计算长时间卡死。" ) elif delta_ratio > 1.0: risk = self._max_quick_risk(risk, "high") warnings.append("目标直径变化超过当前直径的 100%,很可能导致大范围误切或布尔失败。") elif delta_ratio > 0.35: risk = self._max_quick_risk(risk, "medium") warnings.append("目标直径变化超过当前直径的 35%。") if target_diameter < current_diameter: if guess != "hole/groove candidate": blockers.append("缩小孔径当前版本只支持明确的孔/槽候选。") else: risk = self._max_quick_risk(risk, "high") warnings.append("缩小孔径需要先补料再重切,属于高风险实验路径。") if height_estimate is not None and height_estimate > 1e-9: target_to_height_ratio = target_diameter / height_estimate if target_diameter > height_estimate * 2.0: blockers.append("目标直径超过圆柱面估算高度的 2 倍,当前版本暂不放行。") elif target_diameter > height_estimate: risk = self._max_quick_risk(risk, "medium") warnings.append("目标直径大于圆柱面估算高度,请确认单位和目标。") if blockers: status = "blocked" risk = "blocked" elif risk in {"medium", "high"}: status = "caution" message = ";".join(blockers + warnings) if blockers or warnings else "可以尝试调整孔/槽直径。" return { "status": status, "risk": risk, "message": message, "warnings": ";".join(warnings), "blockers": ";".join(blockers), "face_id": face_id, "part_id": info.get("part_id"), "solid_id": info.get("solid_id"), "current_diameter": current_diameter, "target_diameter": target_diameter, "delta_diameter": delta_diameter, "diameter_delta_ratio": delta_ratio, "target_to_height_ratio": target_to_height_ratio, "resize_mode": resize_mode, "feature_type": info.get("feature_type"), "feature_guess": guess, "confidence": confidence, "angular_span": angular_span, "height_estimate": height_estimate, "material_vote_summary": info.get("material_vote_summary"), "cylinder_end_type": info.get("cylinder_end_type"), "hole_depth_estimate": info.get("hole_depth_estimate"), "feature_bottom_face_ids": info.get("feature_bottom_face_ids"), "feature_opening_face_ids": info.get("feature_opening_face_ids"), "quick_preflight": True, } def _confirm_quick_edit_plan( self, title: str, plan: dict[str, object], lines: list[str], blocked_status: str, cancelled_status: str, ) -> bool: if plan.get("status") == "blocked": QMessageBox.information(self, f"不能{title}", str(plan.get("message", ""))) self.statusBar().showMessage(blocked_status) return False if self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan): return False if plan.get("risk") == "low": return True warnings = str(plan.get("warnings", "")) warnings_line = f"\n警告: {warnings}\n" if warnings else "" result = QMessageBox.question( self, f"确认{title}", ( "\n".join(lines) + f"\n风险: {plan.get('risk')}\n" + warnings_line + f"\n{plan.get('message', '')}\n\n" "为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;" "详细几何方案会进入后台计算,完成后自动刷新模型。\n\n" "确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage(cancelled_status) return False return True def resize_hole(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再调整孔径。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择圆柱面", "请先选择一个圆柱面。") return try: diameter = float(self.hole_diameter_input.text()) except ValueError: QMessageBox.critical(self, "直径无效", "请输入数字形式的目标直径。") return face_id = self.selected_face_id info = self._selected_face_info_snapshot() plan = self._quick_cylinder_resize_plan(face_id, diameter, info) lines = [ f"Face: {face_id}", f"当前直径: {_format_value(plan.get('current_diameter'))}", f"目标直径: {_format_value(plan.get('target_diameter'))}", f"直径变化量: {_format_value(plan.get('delta_diameter'))}", f"直径变化比例: {_format_percent(plan.get('diameter_delta_ratio'))}", f"候选判断: {plan.get('feature_guess')}", f"置信度: {plan.get('confidence')}", ] if plan.get("target_to_height_ratio") not in {None, ""}: lines.append(f"目标直径/估算高度: {_format_value(plan.get('target_to_height_ratio'))}") if not self._confirm_quick_edit_plan( "圆柱孔径调整", plan, lines, "圆柱孔径调整已阻止", "已取消圆柱孔径调整", ): return self.clear_edit_preview(render=False) def action(): return self.model.resize_cylindrical_hole(face_id, diameter) self._run_edit_action( action, operation_name="调整圆柱孔径", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "new_diameter": diameter, "old_diameter": plan.get("current_diameter"), "delta_diameter": plan.get("delta_diameter"), "diameter_delta_ratio": plan.get("diameter_delta_ratio"), "target_to_height_ratio": plan.get("target_to_height_ratio"), "resize_mode": plan.get("resize_mode"), "feature_type": plan.get("feature_type"), "feature_bottom_face_ids": plan.get("feature_bottom_face_ids"), "feature_opening_face_ids": plan.get("feature_opening_face_ids"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), "material_vote_summary": plan.get("material_vote_summary"), "cylinder_end_type": plan.get("cylinder_end_type"), "hole_depth_estimate": plan.get("hole_depth_estimate"), "quick_preflight": True, "ui_preview": "skipped-to-avoid-ui-freeze", }, target_kind="face", target_id=face_id, ) def move_cylindrical_hole_axis(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再移动孔轴心。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择圆柱孔", "请先选择一个完整圆柱孔 Face。") return try: target_center = ( float(self.hole_center_x_input.text()), float(self.hole_center_y_input.text()), float(self.hole_center_z_input.text()), ) except (AttributeError, ValueError): QMessageBox.critical(self, "孔轴心坐标无效", "请输入 X, Y, Z 三个数字形式的目标坐标。") return face_id = self.selected_face_id plan = self.model.cylindrical_axis_move_plan(face_id, target_center) lines = [ f"Face: {face_id}", f"当前轴心: {_format_value(plan.get('current_axis_center'))}", f"目标轴心: {_format_value(plan.get('target_axis_center'))}", f"移动向量: {_format_value(plan.get('axis_move_vector'))}", f"移动距离: {_format_value(plan.get('axis_move_distance'))}", f"孔直径: {_format_value(plan.get('target_diameter'))}", f"候选判断: {plan.get('feature_guess')}", f"置信度: {plan.get('confidence')}", ] if not self._confirm_quick_edit_plan( "圆柱孔轴心坐标修改", plan, lines, "圆柱孔轴心坐标修改已阻止", "已取消圆柱孔轴心坐标修改", ): return self.clear_edit_preview(render=False) def action(): return self.model.move_cylindrical_hole_axis(face_id, target_center) self._run_edit_action( action, operation_name="修改圆柱孔轴心坐标", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "surface": plan.get("surface", "cylinder"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "current_diameter": plan.get("current_diameter"), "target_diameter": plan.get("target_diameter"), "current_axis_center": plan.get("current_axis_center"), "target_axis_center": plan.get("target_axis_center"), "axis_move_vector": plan.get("axis_move_vector"), "axis_move_distance": plan.get("axis_move_distance"), "axis_move_radial_distance": plan.get("axis_move_radial_distance"), "axis_move_axial_delta": plan.get("axis_move_axial_delta"), "resize_strategy": plan.get("resize_strategy"), "move_axis_status": plan.get("status"), "move_axis_risk": plan.get("risk"), "move_axis_message": plan.get("message"), "move_axis_warnings": plan.get("warnings"), "move_axis_blockers": plan.get("blockers"), "fill_strategy": plan.get("fill_strategy"), "fill_height": plan.get("fill_height"), "fill_radius": plan.get("fill_radius"), "cutter_strategy": plan.get("cutter_strategy"), "cutter_height": plan.get("cutter_height"), "target_cutter_axis_point": plan.get("target_cutter_axis_point"), "target_cutter_start_point": plan.get("target_cutter_start_point"), "quick_preflight": True, "ui_preview": "skipped-to-avoid-ui-freeze", }, target_kind="face", target_id=face_id, ) def resize_slot_width(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再调整槽/半孔宽度。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择槽/半孔", "请先选择一个槽/半孔候选 Face。") return if not hasattr(self, "slot_width_input"): return try: target_width = float(self.slot_width_input.text()) except ValueError: QMessageBox.critical(self, "目标槽宽无效", "请输入数字形式的目标槽/半孔宽度。") return if target_width <= 0: QMessageBox.critical(self, "目标槽宽无效", "目标槽/半孔宽度必须大于 0。") return face_id = self.selected_face_id info = self._selected_face_info_snapshot() if info.get("surface") != "cylinder" or info.get("slot_kind") != "partial-cylindrical-groove": QMessageBox.information(self, "不是槽/半孔候选", "当前选中Face没有被识别为槽/半孔候选。") return angular_span = _float_or_none(info.get("slot_angular_span")) if angular_span is None: angular_span = _float_or_none(info.get("angular_span")) if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92: QMessageBox.information(self, "不能调整槽宽", "当前候选没有稳定的部分圆柱角度,不能换算槽宽。") return sin_half_span = math.sin(min(max(angular_span, 1e-6), math.tau - 1e-6) / 2.0) if abs(sin_half_span) <= 1e-6: QMessageBox.information(self, "不能调整槽宽", "当前槽圆弧角度过小,不能可靠换算为圆柱直径。") return target_diameter = target_width / sin_half_span current_width = _float_or_none(info.get("slot_chord_width_estimate")) try: pair_face_id = self._slot_pair_face_override(face_id) except ValueError as exc: QMessageBox.critical(self, "槽孔配对端无效", str(exc)) return plan = self.model.cylindrical_slot_resize_plan(face_id, target_width, "width", pair_face_id=pair_face_id) target_diameter = float(plan.get("slot_target_diameter") or plan.get("target_diameter") or target_diameter) lines = [ f"Face: {face_id}", f"当前槽宽估算: {_format_value(current_width)}", f"目标槽宽: {_format_value(target_width)}", f"槽圆弧角度: {_format_value(angular_span)}", f"换算目标圆柱直径: {_format_value(target_diameter)}", "当前版本会把槽宽换算成圆柱直径后执行孔/槽重建,不是完整 CAD 槽参数编辑。", ] if not self._confirm_quick_edit_plan( "槽/半孔宽度调整", plan, lines, "槽/半孔宽度调整已阻止", "已取消槽/半孔宽度调整", ): return self.clear_edit_preview(render=False) def action(): return self.model.resize_cylindrical_slot_width(face_id, target_width, pair_face_id=pair_face_id) self._run_edit_action( action, operation_name="调整槽/半孔宽度", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "new_slot_width": target_width, "old_slot_width": current_width, "slot_width_delta": None if current_width is None else target_width - current_width, "slot_angular_span": angular_span, "slot_resize_mode": "width", "slot_resize_strategy": plan.get("slot_resize_strategy"), "resize_strategy": plan.get("slot_resize_strategy"), "surface": plan.get("surface", "cylinder"), "slot_pair_face_id": plan.get("slot_pair_face_id"), "slot_pair_manual_face_id": pair_face_id, "slot_pair_manual": plan.get("slot_pair_manual"), "slot_pair_axis_distance": plan.get("slot_pair_axis_distance"), "slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"), "slot_target_diameter": target_diameter, "derived_new_diameter": target_diameter, "old_diameter": plan.get("current_diameter"), "delta_diameter": plan.get("delta_diameter"), "diameter_delta_ratio": plan.get("diameter_delta_ratio"), "resize_mode": plan.get("resize_mode"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "slot_status": info.get("slot_status"), "slot_kind": info.get("slot_kind"), "feature_slot_boundary_face_ids": info.get("feature_slot_boundary_face_ids"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), "material_vote_summary": plan.get("material_vote_summary"), "cylinder_end_type": plan.get("cylinder_end_type"), "hole_depth_estimate": plan.get("hole_depth_estimate"), "feature_bottom_face_ids": plan.get("feature_bottom_face_ids"), "feature_opening_face_ids": plan.get("feature_opening_face_ids"), "quick_preflight": True, "ui_preview": "skipped-to-avoid-ui-freeze", }, target_kind="face", target_id=face_id, ) def resize_slot_depth(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再调整槽/半孔深度。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择槽/半孔", "请先选择一个槽/半孔候选 Face。") return if not hasattr(self, "slot_depth_input"): return try: 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 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"), "surface": plan.get("surface", "cylinder"), "slot_pair_face_id": plan.get("slot_pair_face_id"), "slot_pair_manual_face_id": pair_face_id, "slot_pair_manual": plan.get("slot_pair_manual"), "slot_pair_axis_distance": plan.get("slot_pair_axis_distance"), "slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"), "slot_target_diameter": target_diameter, "derived_new_diameter": target_diameter, "old_diameter": plan.get("current_diameter"), "delta_diameter": plan.get("delta_diameter"), "diameter_delta_ratio": plan.get("diameter_delta_ratio"), "resize_mode": plan.get("resize_mode"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "slot_status": info.get("slot_status"), "slot_kind": info.get("slot_kind"), "feature_slot_boundary_face_ids": info.get("feature_slot_boundary_face_ids"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), "material_vote_summary": plan.get("material_vote_summary"), "cylinder_end_type": plan.get("cylinder_end_type"), "hole_depth_estimate": plan.get("hole_depth_estimate"), "feature_bottom_face_ids": plan.get("feature_bottom_face_ids"), "feature_opening_face_ids": plan.get("feature_opening_face_ids"), "quick_preflight": True, "ui_preview": "skipped-to-avoid-ui-freeze", }, target_kind="face", target_id=face_id, ) def resize_slot_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 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"), "surface": plan.get("surface", "cylinder"), "slot_pair_face_id": plan.get("slot_pair_face_id"), "slot_pair_manual_face_id": pair_face_id, "slot_pair_manual": plan.get("slot_pair_manual"), "slot_pair_axis_distance": plan.get("slot_pair_axis_distance"), "slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"), "slot_target_diameter": target_diameter, "derived_new_diameter": target_diameter, "old_diameter": plan.get("current_diameter"), "delta_diameter": plan.get("delta_diameter"), "diameter_delta_ratio": plan.get("diameter_delta_ratio"), "resize_mode": plan.get("resize_mode"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "slot_status": info.get("slot_status"), "slot_kind": info.get("slot_kind"), "feature_slot_boundary_face_ids": info.get("feature_slot_boundary_face_ids"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), "material_vote_summary": plan.get("material_vote_summary"), "cylinder_end_type": plan.get("cylinder_end_type"), "hole_depth_estimate": plan.get("hole_depth_estimate"), "feature_bottom_face_ids": plan.get("feature_bottom_face_ids"), "feature_opening_face_ids": plan.get("feature_opening_face_ids"), "quick_preflight": True, "ui_preview": "skipped-to-avoid-ui-freeze", }, target_kind="face", target_id=face_id, ) def resize_slot_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 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"), "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, ) 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 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"), "slot_resize_mode": plan.get("slot_resize_mode"), "slot_pair_face_id": plan.get("slot_pair_face_id"), "slot_pair_manual_face_id": pair_face_id, "slot_pair_manual": plan.get("slot_pair_manual"), "slot_pair_axis_distance": plan.get("slot_pair_axis_distance"), "slot_pair_boundary_overlap_count": plan.get("slot_pair_boundary_overlap_count"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="face", target_id=face_id, ) def resize_boss(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再调整凸台直径。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择圆柱凸台", "请先选择一个圆柱凸台候选 Face。") return try: diameter = float(self.boss_diameter_input.text()) except ValueError: QMessageBox.critical(self, "直径无效", "请输入数字形式的目标凸台直径。") return info = self.model.face_info(self.selected_face_id) if "diameter" not in info: QMessageBox.information(self, "不是圆柱面", "当前选中的 Face 不是圆柱面,不能调整圆柱凸台直径。") return plan = self.model.cylindrical_boss_resize_plan(self.selected_face_id, diameter) if plan["status"] == "blocked": QMessageBox.information(self, "不能调整凸台直径", str(plan["message"])) self.statusBar().showMessage("圆柱凸台直径调整已阻止") return if plan["risk"] != "low": target_to_height_ratio = plan.get("target_to_height_ratio", "") target_to_height_line = ( "" if target_to_height_ratio is None or target_to_height_ratio == "" else f"目标直径/估算高度: {_format_value(target_to_height_ratio)}\n" ) warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认圆柱凸台直径调整", ( f"face: {plan['face_id']}\n" f"当前凸台直径: {_format_value(plan['current_diameter'])}\n" f"目标凸台直径: {_format_value(plan['target_diameter'])}\n" f"直径变化量: {_format_value(plan['delta_diameter'])}\n" f"直径变化比例: {_format_percent(plan['diameter_delta_ratio'])}\n" f"{target_to_height_line}" f"调整模式: {plan['resize_mode']}\n" f"候选判断: {plan['feature_guess']}\n" f"置信度: {plan['confidence']}\n" f"材料投票: {plan['material_vote_summary']}\n" f"工具策略: {plan['boss_tool_strategy']}\n" f"工具高度: {_format_value(plan['boss_tool_height'])}\n" f"轴向重叠余量: {_format_value(plan['boss_tool_axial_margin'])}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会对当前零件的局部几何执行受限B-Rep布尔修改。\n\n" "确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消圆柱凸台直径调整") return face_id = self.selected_face_id self._show_cylinder_boss_resize_preview(face_id, diameter) def action(): return self.model.resize_cylindrical_boss(face_id, diameter) self._run_edit_action( action, operation_name="调整圆柱凸台直径", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "new_diameter": diameter, "old_diameter": info.get("diameter"), "delta_diameter": plan.get("delta_diameter"), "diameter_delta_ratio": plan.get("diameter_delta_ratio"), "target_to_height_ratio": plan.get("target_to_height_ratio"), "resize_mode": plan.get("resize_mode"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "boss_resize_status": plan.get("status"), "boss_resize_risk": plan.get("risk"), "boss_resize_message": plan.get("message"), "boss_resize_warnings": plan.get("warnings"), "boss_resize_blockers": plan.get("blockers"), "material_vote_summary": plan.get("material_vote_summary"), "boss_tool_strategy": plan.get("boss_tool_strategy"), "boss_tool_height": plan.get("boss_tool_height"), "boss_tool_axial_margin": plan.get("boss_tool_axial_margin"), "boss_tool_radius": plan.get("boss_tool_radius"), "boss_tool_outer_radius": plan.get("boss_tool_outer_radius"), "boss_tool_inner_radius": plan.get("boss_tool_inner_radius"), "boss_tool_radial_overlap": plan.get("boss_tool_radial_overlap"), "boss_tool_note": plan.get("boss_tool_note"), "feature_adjacent_face_ids": plan.get("feature_adjacent_face_ids"), "feature_boundary_edge_ids": plan.get("feature_boundary_edge_ids"), }, target_kind="face", target_id=face_id, ) def 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"), "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, ) 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"风险: {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._show_cylinder_boss_height_preview(face_id, target_height) 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"), "same_domain_face_ids": plan.get("same_domain_face_ids"), "feature_start_end_face_ids": plan.get("feature_start_end_face_ids"), "feature_end_end_face_ids": plan.get("feature_end_end_face_ids"), }, target_kind="face", target_id=face_id, ) def resize_cylinder_height(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再调整圆柱高度。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择圆柱面", "请先选择一个有端盖的完整圆柱 Face。") return if not hasattr(self, "boss_height_input"): return try: target_height = float(self.boss_height_input.text()) except ValueError: QMessageBox.critical(self, "高度无效", "请输入数字形式的目标圆柱高度。") return face_id = self.selected_face_id plan = self.model.cylindrical_height_plan(face_id, target_height) lines = [ f"Face: {face_id}", f"端盖Face: {_format_value(plan.get('boss_height_cap_face_id'))}", f"端盖位置: {_format_value(plan.get('boss_height_endpoint_label'))}", f"当前圆柱高度: {_format_value(plan.get('current_height'))}", f"目标圆柱高度: {_format_value(plan.get('target_height'))}", f"高度变化量: {_format_value(plan.get('delta_height'))}", f"端盖推拉距离: {_format_value(plan.get('push_pull_distance'))}", f"候选判断: {plan.get('feature_guess')}", "当前版本会推拉识别到的圆柱端盖,不依赖 CAD 原始建模历史。", ] if not self._confirm_quick_edit_plan( "圆柱高度调整", plan, lines, "圆柱高度调整已阻止", "已取消圆柱高度调整", ): return self.clear_edit_preview(render=False) def action(): return self.model.resize_cylindrical_height(face_id, target_height) self._run_edit_action( action, operation_name="调整圆柱高度", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "surface": plan.get("surface", "cylinder"), "new_height": target_height, "old_height": plan.get("current_height"), "delta_height": plan.get("delta_height"), "height_delta_ratio": plan.get("height_delta_ratio"), "boss_height_cap_face_id": plan.get("boss_height_cap_face_id"), "boss_height_endpoint_role": plan.get("boss_height_endpoint_role"), "boss_height_endpoint_label": plan.get("boss_height_endpoint_label"), "boss_height_open_end": plan.get("boss_height_open_end"), "push_pull_distance": plan.get("push_pull_distance"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), "resize_strategy": plan.get("resize_strategy"), "same_domain_face_ids": plan.get("same_domain_face_ids"), "feature_start_end_face_ids": plan.get("feature_start_end_face_ids"), "feature_end_end_face_ids": plan.get("feature_end_end_face_ids"), }, target_kind="face", target_id=face_id, ) def suppress_hole(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再封堵圆柱孔。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择圆柱面", "请先选择一个圆柱孔 Face。") return info = self.model.face_info(self.selected_face_id) if info.get("surface") != "cylinder" or "diameter" not in info: QMessageBox.information(self, "不是圆柱孔", "当前选中的 Face 不是可封堵的圆柱孔。") return plan = self.model.cylindrical_suppress_plan(self.selected_face_id) if plan["status"] == "blocked": QMessageBox.information(self, "不能封堵圆柱孔", str(plan["message"])) self.statusBar().showMessage("封堵圆柱孔已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认封堵圆柱孔", ( f"face: {plan['face_id']}\n" f"直径: {_format_value(plan['diameter'])}\n" f"估算高度: {_format_value(plan['height_estimate'])}\n" f"候选判断: {plan['feature_guess']}\n" f"置信度: {plan['confidence']}\n" f"材料投票: {plan['material_vote_summary']}\n" f"端部类型: {plan['cylinder_end_type']}\n" f"疑似底面 Face: {_format_value(plan.get('feature_bottom_face_ids', ''))}\n" f"风险: {plan['risk']}\n\n" f"补料策略: {plan['fill_strategy']}\n" f"补料半径: {_format_value(plan['fill_radius'])}\n" f"补料高度: {_format_value(plan['fill_height'])}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会用补料体封堵当前孔,并对当前零件执行受限OCCT Fuse。\n\n" "确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消封堵圆柱孔") return face_id = self.selected_face_id self._show_cylinder_suppress_preview(face_id) def action(): return self.model.suppress_cylindrical_hole(face_id) self._run_edit_action( action, operation_name="封堵圆柱孔", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "diameter": plan.get("diameter"), "height_estimate": plan.get("height_estimate"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "suppress_status": plan.get("status"), "suppress_risk": plan.get("risk"), "suppress_message": plan.get("message"), "suppress_warnings": plan.get("warnings"), "suppress_blockers": plan.get("blockers"), "cylinder_end_type": plan.get("cylinder_end_type"), "feature_bottom_face_ids": plan.get("feature_bottom_face_ids"), "feature_opening_face_ids": plan.get("feature_opening_face_ids"), "feature_bottom_note": plan.get("feature_bottom_note"), "fill_strategy": plan.get("fill_strategy"), "fill_height": plan.get("fill_height"), "fill_radius": plan.get("fill_radius"), "fill_radius_overlap": plan.get("fill_radius_overlap"), "fill_note": plan.get("fill_note"), }, target_kind="face", target_id=face_id, ) def _slot_pair_face_override(self, source_face_id: int | None = None) -> int | None: if not hasattr(self, "slot_pair_face_input"): return None text = self.slot_pair_face_input.text().strip() if not text: return None try: pair_face_id = int(text) except ValueError as exc: raise ValueError("槽孔配对端 Face ID 必须是整数。") from exc if source_face_id is not None and pair_face_id == int(source_face_id): raise ValueError("槽孔配对端不能和当前槽端使用同一个 Face ID。") if self.model is not None and (pair_face_id < 0 or pair_face_id >= len(self.model.faces)): raise ValueError(f"槽孔配对端 Face ID {pair_face_id} 不存在。") return pair_face_id def _hole_bottom_face_override(self) -> int | None: if not hasattr(self, "hole_bottom_face_input"): return None text = self.hole_bottom_face_input.text().strip() if not text: return None try: bottom_face_id = int(text) except ValueError as exc: raise ValueError("底面 Face ID 必须是整数。") from exc if self.model is not None and (bottom_face_id < 0 or bottom_face_id >= len(self.model.faces)): raise ValueError(f"底面 Face ID {bottom_face_id} 不存在。") return bottom_face_id def _quick_cylinder_depth_plan( self, face_id: int, target_depth: float, bottom_face_id: int | None = None, info: dict[str, object] | None = None, ) -> dict[str, object]: info = dict(info or self._selected_face_info_snapshot()) warnings: list[str] = [] blockers: list[str] = [] risk = "low" status = "ready" surface = str(info.get("surface", "")) guess = str(info.get("feature_guess", "cylindrical face")) confidence = str(info.get("confidence", "low")) angular_span = _float_or_none(info.get("angular_span")) end_type = str(info.get("cylinder_end_type", "unknown")) current_depth = _float_or_none(info.get("hole_depth_estimate")) if current_depth is None: current_depth = _float_or_none(info.get("same_domain_height_estimate")) manual_bottom = bottom_face_id is not None if surface != "cylinder": blockers.append("当前选中对象不是圆柱孔/槽,不能调整孔深。") if guess != "hole/groove candidate": blockers.append("孔深调整当前版本只支持明确的孔/槽候选。") if end_type != "blind" and not manual_bottom: blockers.append("孔深调整需要识别到盲孔/盲槽底面,或手动填写底面 Face ID。") elif end_type != "blind" and manual_bottom: risk = self._max_quick_risk(risk, "medium") warnings.append("端部类型不是明确 blind,当前会按手动底面 Face ID 尝试推断孔深方向。") if current_depth is None or current_depth <= 1e-9: blockers.append("当前圆柱面没有可靠的深度估算。") if target_depth <= 0: blockers.append("目标深度必须大于 0。") if guess == "hole/groove candidate" and confidence == "low": risk = self._max_quick_risk(risk, "medium") warnings.append("孔/槽判断置信度较低。") if ( guess == "hole/groove candidate" and angular_span is not None and angular_span < math.tau * 0.92 ): risk = self._max_quick_risk(risk, "medium") warnings.append("这是局部圆柱面,更像槽或半孔,孔深调整会按局部槽处理。") delta_depth = None delta_ratio = None depth_mode = "unknown" if current_depth is not None and current_depth > 1e-9: delta_depth = target_depth - current_depth delta_ratio = abs(delta_depth) / current_depth depth_mode = "deepen" if delta_depth > 0 else "shallow" if abs(delta_depth) <= max(current_depth * 1e-5, 1e-6): blockers.append("目标深度与当前深度几乎相同,不需要修改。") elif delta_ratio > 1.0: risk = self._max_quick_risk(risk, "high") warnings.append("目标深度变化超过当前深度的 100%,很可能贯穿、误切或布尔失败。") elif delta_ratio > 0.35: risk = self._max_quick_risk(risk, "medium") warnings.append("目标深度变化超过当前深度的 35%。") if target_depth < current_depth * 0.08: risk = self._max_quick_risk(risk, "high") warnings.append("目标深度非常浅,补料后可能生成很薄的局部面。") if blockers: status = "blocked" risk = "blocked" elif risk in {"medium", "high"}: status = "caution" message = ";".join(blockers + warnings) if blockers or warnings else "可以尝试调整盲孔/盲槽深度。" return { "status": status, "risk": risk, "message": message, "warnings": ";".join(warnings), "blockers": ";".join(blockers), "face_id": face_id, "part_id": info.get("part_id"), "solid_id": info.get("solid_id"), "current_depth": current_depth, "target_depth": target_depth, "delta_depth": delta_depth, "depth_delta_ratio": delta_ratio, "depth_mode": depth_mode, "diameter": info.get("diameter"), "radius": info.get("radius"), "feature_type": info.get("feature_type"), "feature_guess": guess, "confidence": confidence, "angular_span": angular_span, "cylinder_end_type": end_type, "feature_bottom_face_ids": info.get("feature_bottom_face_ids"), "manual_bottom_face_id": "" if bottom_face_id is None else bottom_face_id, "manual_bottom_face_used": manual_bottom, "feature_opening_face_ids": info.get("feature_opening_face_ids"), "feature_bottom_confidence": info.get("feature_bottom_confidence"), "feature_bottom_detection": info.get("feature_bottom_detection"), "feature_bottom_note": info.get("feature_bottom_note"), "quick_preflight": True, } def resize_hole_depth(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再调整孔深。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择圆柱面", "请先选择一个盲孔或盲槽的圆柱面。") return try: target_depth = float(self.hole_depth_input.text()) except ValueError: QMessageBox.critical(self, "深度无效", "请输入数字形式的目标孔深。") return try: bottom_face_id = self._hole_bottom_face_override() except ValueError as exc: QMessageBox.critical(self, "底面 Face ID 无效", str(exc)) return face_id = self.selected_face_id info = self._selected_face_info_snapshot() plan = self._quick_cylinder_depth_plan(face_id, target_depth, bottom_face_id=bottom_face_id, info=info) lines = [ f"Face: {face_id}", f"当前深度: {_format_value(plan.get('current_depth'))}", f"目标深度: {_format_value(plan.get('target_depth'))}", f"深度变化量: {_format_value(plan.get('delta_depth'))}", f"深度变化比例: {_format_percent(plan.get('depth_delta_ratio'))}", f"候选判断: {plan.get('feature_guess')}", f"置信度: {plan.get('confidence')}", f"端部类型: {plan.get('cylinder_end_type')}", ] if bottom_face_id is not None: lines.append(f"手动底面 Face: {bottom_face_id}") if not self._confirm_quick_edit_plan( "盲孔/盲槽深度调整", plan, lines, "盲孔/盲槽深度调整已阻止", "已取消盲孔/盲槽深度调整", ): return self.clear_edit_preview(render=False) def action(): return self.model.resize_cylindrical_depth( face_id, target_depth, bottom_face_id=bottom_face_id, ) self._run_edit_action( action, operation_name="调整盲孔/盲槽深度", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "old_depth": plan.get("current_depth"), "new_depth": target_depth, "delta_depth": plan.get("delta_depth"), "depth_delta_ratio": plan.get("depth_delta_ratio"), "depth_mode": plan.get("depth_mode"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "depth_status": plan.get("status"), "depth_risk": plan.get("risk"), "depth_message": plan.get("message"), "depth_warnings": plan.get("warnings"), "depth_blockers": plan.get("blockers"), "cylinder_end_type": plan.get("cylinder_end_type"), "feature_bottom_face_ids": plan.get("feature_bottom_face_ids"), "manual_bottom_face_id": plan.get("manual_bottom_face_id"), "manual_bottom_face_used": plan.get("manual_bottom_face_used"), "manual_bottom_face_note": plan.get("manual_bottom_face_note"), "feature_opening_face_ids": plan.get("feature_opening_face_ids"), "feature_bottom_confidence": plan.get("feature_bottom_confidence"), "feature_bottom_detection": plan.get("feature_bottom_detection"), "feature_bottom_note": plan.get("feature_bottom_note"), "quick_preflight": True, "ui_preview": "skipped-to-avoid-ui-freeze", }, target_kind="face", target_id=face_id, ) def resize_existing_fillet(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再修改已有圆角。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择已有圆角", "请先选择一个已有圆角/倒圆候选 Face。") return try: target_radius = float(self.edge_fillet_radius_input.text()) except ValueError: QMessageBox.critical(self, "半径无效", "请输入数字形式的目标圆角半径。") return face_id = self.selected_face_id plan = self.model.existing_fillet_resize_plan(face_id, target_radius) if self._block_unisolated_high_risk_operation("已阻止高风险已有圆角修改", plan): return if plan["status"] == "blocked": QMessageBox.information(self, "不能修改已有圆角", str(plan["message"])) self.statusBar().showMessage("已有圆角半径修改已阻止") return warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认修改已有圆角半径", ( f"face: {plan['face_id']}\n" f"当前估算半径: {_format_value(plan['current_radius'])}\n" f"目标半径: {_format_value(plan['target_radius'])}\n" f"半径变化量: {_format_value(plan['delta_radius'])}\n" f"半径变化比例: {_format_percent(plan['radius_delta_ratio'])}\n" f"圆弧跨度: {_format_value(plan.get('angular_span', ''))}\n" f"长度估算: {_format_value(plan.get('height_estimate', ''))}\n" f"支撑 Face: {_format_value(plan.get('feature_existing_fillet_support_face_ids', ''))}\n" f"边界 Edge: {_format_value(plan.get('feature_boundary_edge_ids', ''))}\n" f"策略: {plan['resize_strategy']}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会先尝试移除当前圆角面,再对恢复出的锐边重新倒圆。\n" "这不是 CAD 历史特征参数编辑,复杂圆角可能失败。\n\n" "确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消修改已有圆角半径") return self._show_existing_fillet_resize_preview(face_id, target_radius) def action(): return self.model.resize_existing_fillet(face_id, target_radius) self._run_edit_action( action, operation_name="修改已有圆角半径", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "old_radius": plan.get("current_radius"), "new_radius": target_radius, "delta_radius": plan.get("delta_radius"), "radius_delta_ratio": plan.get("radius_delta_ratio"), "feature_type": plan.get("feature_type"), "feature_guess": plan.get("feature_guess"), "confidence": plan.get("confidence"), "existing_fillet_status": plan.get("status"), "existing_fillet_risk": plan.get("risk"), "existing_fillet_message": plan.get("message"), "existing_fillet_warnings": plan.get("warnings"), "existing_fillet_blockers": plan.get("blockers"), "feature_existing_fillet_support_face_ids": plan.get("feature_existing_fillet_support_face_ids"), "feature_boundary_edge_ids": plan.get("feature_boundary_edge_ids"), "resize_strategy": plan.get("resize_strategy"), "resize_note": plan.get("resize_note"), "axis_point": plan.get("axis_point"), "axis": plan.get("axis"), "height_estimate": plan.get("height_estimate"), "angular_span": plan.get("angular_span"), }, target_kind="face", target_id=face_id, ) def fillet_edge(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再添加圆角。"): return if self.selected_edge_id is None: QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。") return try: radius = float(self.edge_fillet_radius_input.text()) except ValueError: QMessageBox.critical(self, "半径无效", "请输入数字形式的圆角半径。") return edge_id = self.selected_edge_id plan = self.model.edge_fillet_plan(edge_id, radius) if self._block_unisolated_high_risk_operation("已阻止高风险边圆角", plan): return if plan["status"] == "blocked": QMessageBox.information(self, "不能添加圆角", str(plan["message"])) self.statusBar().showMessage("添加圆角已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认给Edge添加圆角", ( f"Edge: {plan['edge_id']}\n" f"Edge长度: {_format_value(plan['edge_length'])}\n" f"目标圆角半径: {_format_value(plan['target_radius'])}\n" f"半径/Edge长度比例: {_format_percent(plan['radius_to_length_ratio'])}\n" f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n" f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会调用OCCT倒圆并真实修改当前零件的局部几何。\n\n" "确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消添加圆角") return self._show_edge_fillet_preview(edge_id, radius) def action(): return self.model.fillet_edge(edge_id, radius) self._run_edit_action( action, operation_name="给Edge添加圆角", target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "edge_length": plan.get("edge_length"), "fillet_radius": radius, "radius_to_length_ratio": plan.get("radius_to_length_ratio"), "curve": plan.get("curve"), "adjacent_face_ids": plan.get("adjacent_face_ids"), "adjacent_face_count": plan.get("adjacent_face_count"), "fillet_status": plan.get("status"), "fillet_risk": plan.get("risk"), "fillet_message": plan.get("message"), "fillet_warnings": plan.get("warnings"), "fillet_blockers": plan.get("blockers"), }, target_kind="edge", target_id=edge_id, ) def chamfer_edge(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再添加倒角。"): return if self.selected_edge_id is None: QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。") return try: distance = float(self.edge_chamfer_distance_input.text()) except ValueError: QMessageBox.critical(self, "距离无效", "请输入数字形式的倒角距离。") return edge_id = self.selected_edge_id plan = self.model.edge_chamfer_plan(edge_id, distance) if self._block_unisolated_high_risk_operation("已阻止高风险边倒角", plan): return if plan["status"] == "blocked": QMessageBox.information(self, "不能添加倒角", str(plan["message"])) self.statusBar().showMessage("添加倒角已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认给Edge添加倒角", ( f"Edge: {plan['edge_id']}\n" f"Edge长度: {_format_value(plan['edge_length'])}\n" f"目标倒角距离: {_format_value(plan['target_distance'])}\n" f"倒角距离/Edge长度比例: {_format_percent(plan['distance_to_length_ratio'])}\n" f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n" f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会调用OCCT倒角并真实修改当前零件的局部几何。\n\n" "确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消添加倒角") return self._show_edge_chamfer_preview(edge_id, distance) def action(): return self.model.chamfer_edge(edge_id, distance) self._run_edit_action( action, operation_name="给Edge添加倒角", target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "edge_length": plan.get("edge_length"), "chamfer_distance": distance, "distance_to_length_ratio": plan.get("distance_to_length_ratio"), "curve": plan.get("curve"), "adjacent_face_ids": plan.get("adjacent_face_ids"), "adjacent_face_count": plan.get("adjacent_face_count"), "chamfer_status": plan.get("status"), "chamfer_risk": plan.get("risk"), "chamfer_message": plan.get("message"), "chamfer_warnings": plan.get("warnings"), "chamfer_blockers": plan.get("blockers"), }, target_kind="edge", target_id=edge_id, ) def chamfer_edge_asymmetric(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再添加不等距倒角。"): return if self.selected_edge_id is None: QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。") return try: distance1 = float(self.edge_chamfer_distance1_input.text()) distance2 = float(self.edge_chamfer_distance2_input.text()) except (AttributeError, ValueError): QMessageBox.critical(self, "距离无效", "请输入数字形式的不等距倒角 D1, D2。") return reference_face_id = None if hasattr(self, "edge_chamfer_reference_face_input"): text = self.edge_chamfer_reference_face_input.text().strip() if text: try: reference_face_id = int(text) except ValueError: QMessageBox.critical(self, "参考Face无效", "倒角参考 Face ID 必须是整数。") return edge_id = self.selected_edge_id plan = self.model.edge_asymmetric_chamfer_plan(edge_id, distance1, distance2, reference_face_id) if self._block_unisolated_high_risk_operation("已阻止高风险不等距倒角", plan): return lines = [ f"Edge: {edge_id}", f"Edge长度: {_format_value(plan.get('edge_length'))}", f"D1: {_format_value(plan.get('target_distance1'))}", f"D2: {_format_value(plan.get('target_distance2'))}", f"参考Face: {_format_value(plan.get('reference_face_id'))}", f"可用相邻Face: {_format_value(plan.get('reference_face_candidates'))}", f"D1/Edge长度比例: {_format_percent(plan.get('distance1_to_length_ratio'))}", f"D2/Edge长度比例: {_format_percent(plan.get('distance2_to_length_ratio'))}", ] if not self._confirm_quick_edit_plan( "给Edge添加不等距倒角", plan, lines, blocked_status="不等距倒角已阻止", cancelled_status="已取消不等距倒角", ): return self._show_edge_chamfer_preview(edge_id, max(distance1, distance2)) def action(): return self.model.chamfer_edge_asymmetric(edge_id, distance1, distance2, reference_face_id) self._run_edit_action( action, operation_name="给Edge添加不等距倒角", target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "edge_length": plan.get("edge_length"), "chamfer_mode": plan.get("chamfer_mode"), "chamfer_distance1": distance1, "chamfer_distance2": distance2, "distance1_to_length_ratio": plan.get("distance1_to_length_ratio"), "distance2_to_length_ratio": plan.get("distance2_to_length_ratio"), "reference_face_id": plan.get("reference_face_id"), "reference_face_candidates": plan.get("reference_face_candidates"), "curve": plan.get("curve"), "adjacent_face_ids": plan.get("adjacent_face_ids"), "adjacent_face_count": plan.get("adjacent_face_count"), "chamfer_status": plan.get("status"), "chamfer_risk": plan.get("risk"), "chamfer_message": plan.get("message"), "chamfer_warnings": plan.get("warnings"), "chamfer_blockers": plan.get("blockers"), }, target_kind="edge", target_id=edge_id, ) def chamfer_edge_distance_angle(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再添加距离+角度倒角。"): return if self.selected_edge_id is None: QMessageBox.information(self, "未选择Edge", "请先选择一条直线Edge。") return try: distance = float(self.edge_chamfer_angle_distance_input.text()) angle_degrees = float(self.edge_chamfer_angle_degrees_input.text()) except (AttributeError, ValueError): QMessageBox.critical(self, "目标值无效", "请输入数字形式的距离和角度,例如 1, 45。") return reference_face_id = None if hasattr(self, "edge_chamfer_reference_face_input"): text = self.edge_chamfer_reference_face_input.text().strip() if text: try: reference_face_id = int(text) except ValueError: QMessageBox.critical(self, "参考Face无效", "倒角参考 Face ID 必须是整数。") return edge_id = self.selected_edge_id plan = self.model.edge_distance_angle_chamfer_plan(edge_id, distance, angle_degrees, reference_face_id) if self._block_unisolated_high_risk_operation("已阻止高风险距离+角度倒角", plan): return lines = [ f"Edge: {edge_id}", f"Edge长度: {_format_value(plan.get('edge_length'))}", f"距离D: {_format_value(plan.get('target_distance'))}", f"角度: {_format_value(plan.get('target_angle_degrees'))} 度", f"参考Face: {_format_value(plan.get('reference_face_id'))}", f"可用相邻Face: {_format_value(plan.get('reference_face_candidates'))}", f"D/Edge长度比例: {_format_percent(plan.get('distance_to_length_ratio'))}", ] if not self._confirm_quick_edit_plan( "给Edge添加距离+角度倒角", plan, lines, blocked_status="距离+角度倒角已阻止", cancelled_status="已取消距离+角度倒角", ): return self._show_edge_chamfer_preview(edge_id, distance) def action(): return self.model.chamfer_edge_distance_angle(edge_id, distance, angle_degrees, reference_face_id) self._run_edit_action( action, operation_name="给Edge添加距离+角度倒角", target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "edge_length": plan.get("edge_length"), "chamfer_mode": plan.get("chamfer_mode"), "chamfer_distance": distance, "chamfer_angle_degrees": angle_degrees, "chamfer_angle_radians": plan.get("target_angle_radians"), "distance_to_length_ratio": plan.get("distance_to_length_ratio"), "reference_face_id": plan.get("reference_face_id"), "reference_face_candidates": plan.get("reference_face_candidates"), "curve": plan.get("curve"), "adjacent_face_ids": plan.get("adjacent_face_ids"), "adjacent_face_count": plan.get("adjacent_face_count"), "chamfer_status": plan.get("status"), "chamfer_risk": plan.get("risk"), "chamfer_message": plan.get("message"), "chamfer_warnings": plan.get("warnings"), "chamfer_blockers": plan.get("blockers"), }, target_kind="edge", target_id=edge_id, ) def resize_edge_length(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再修改Edge长度。"): return if self.selected_edge_id is None: QMessageBox.information(self, "未选择Edge", "请先选择一条Edge。") return try: target_length = float(self.edge_target_length_input.text()) except ValueError: QMessageBox.critical(self, "目标长度无效", "请输入数字形式的Edge目标长度。") return edge_id = self.selected_edge_id anchor_mode = self._edge_length_anchor_mode_from_ui() plan = self.model.general_edge_length_plan(edge_id, target_length, anchor_mode=anchor_mode) if plan["status"] == "blocked": QMessageBox.information(self, "不能修改Edge长度", str(plan["message"])) self.statusBar().showMessage("Edge长度修改已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认修改Edge长度", ( f"Edge: {plan['edge_id']}\n" f"当前Edge长度: {_format_value(plan['current_length'])}\n" f"目标Edge长度: {_format_value(plan['target_length'])}\n" f"变化量: {_format_value(plan['delta_length'])}\n" f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n" f"策略: {_format_value(plan.get('resize_strategy', ''))}\n" f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n" f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n" f"重建面数: {_format_value(plan.get('local_edge_deform_face_count', ''))}\n" f"端面Face: {_format_value(plan.get('end_face_id', ''))}\n" f"端面位置: {_format_value(plan.get('end_face_label', ''))}\n" f"端面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n" f"圆柱Face: {_format_value(plan.get('cylinder_resize_face_id', ''))}\n" f"圆形Edge模式: {_format_value(plan.get('circular_edge_cylinder_mode_label', ''))}\n" f"换算目标直径: {_format_value(plan.get('cylinder_resize_target_diameter', ''))}\n" f"缩放类型: {_format_value(plan.get('affine_transform_label') or plan.get('affine_transform_kind', ''))}\n" f"缩放说明: {_format_value(plan.get('affine_transform_note', ''))}\n" f"缩放目标: {_format_value(plan.get('affine_target_kind', ''))}\n" f"预估Edge长度: {_format_value(plan.get('affine_predicted_edge_length', ''))}\n" f"缩放校正: {_format_value(plan.get('affine_scale_correction', ''))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "当前会优先只移动被选直线Edge的端点并重建周边平面;没有明确局部路径时会尝试端面推拉、相邻圆柱直径编辑或几何缩放 fallback。" "这不是通用参数化Edge长度编辑。\n\n" "确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消Edge长度修改") return self._show_edge_length_preview(edge_id, target_length, anchor_mode=anchor_mode) def action(): return self.model.resize_general_edge_length(edge_id, target_length, anchor_mode=anchor_mode) self._run_edit_action( action, operation_name="修改Edge长度", target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "current_length": plan.get("current_length"), "target_length": plan.get("target_length"), "delta_length": plan.get("delta_length"), "length_change_ratio": plan.get("length_change_ratio"), "edge_length_anchor_mode": plan.get("edge_length_anchor_mode"), "edge_length_anchor_label": plan.get("edge_length_anchor_label"), "local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"), "local_edge_deform_anchor": plan.get("local_edge_deform_anchor"), "local_edge_deform_face_count": plan.get("local_edge_deform_face_count"), "local_edge_deform_start_move": plan.get("local_edge_deform_start_move"), "local_edge_deform_end_move": plan.get("local_edge_deform_end_move"), "local_edge_deform_moved_endpoint_count": plan.get("local_edge_deform_moved_endpoint_count"), "local_edge_deform_risk": plan.get("local_edge_deform_risk"), "local_edge_deform_note": plan.get("local_edge_deform_note"), "end_face_id": plan.get("end_face_id"), "end_face_endpoint_role": plan.get("end_face_endpoint_role"), "end_face_label": plan.get("end_face_label"), "push_pull_distance": plan.get("push_pull_distance"), "resize_strategy": plan.get("resize_strategy"), "affine_scale": plan.get("affine_scale"), "affine_initial_scale": plan.get("affine_initial_scale"), "affine_scale_correction": plan.get("affine_scale_correction"), "affine_predicted_edge_length": plan.get("affine_predicted_edge_length"), "affine_scale_refine_iterations": plan.get("affine_scale_refine_iterations"), "affine_scale_refine_note": plan.get("affine_scale_refine_note"), "affine_transform_kind": plan.get("affine_transform_kind"), "affine_transform_label": plan.get("affine_transform_label"), "affine_transform_note": plan.get("affine_transform_note"), "affine_transform_warning": plan.get("affine_transform_warning"), "affine_transform_risk": plan.get("affine_transform_risk"), "affine_axis_point": plan.get("affine_axis_point"), "affine_axis_direction": plan.get("affine_axis_direction"), "affine_axis_source": plan.get("affine_axis_source"), "affine_anchor_source": plan.get("affine_anchor_source"), "affine_target_kind": plan.get("affine_target_kind"), "planar_edge_scale_sample_count": plan.get("planar_edge_scale_sample_count"), "planar_edge_scale_plane_deviation": plan.get("planar_edge_scale_plane_deviation"), "circular_edge_current_radius": plan.get("circular_edge_current_radius"), "circular_edge_target_radius": plan.get("circular_edge_target_radius"), "circular_edge_length_scale": plan.get("circular_edge_length_scale"), "circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"), "circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"), "circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"), "cylinder_resize_face_id": plan.get("cylinder_resize_face_id"), "cylinder_resize_operation": plan.get("cylinder_resize_operation"), "cylinder_resize_current_diameter": plan.get("cylinder_resize_current_diameter"), "cylinder_resize_target_diameter": plan.get("cylinder_resize_target_diameter"), "cylinder_resize_delta_diameter": plan.get("cylinder_resize_delta_diameter"), "cylinder_resize_delta_ratio": plan.get("cylinder_resize_delta_ratio"), "cylinder_resize_status": plan.get("cylinder_resize_status"), "cylinder_resize_risk": plan.get("cylinder_resize_risk"), "cylinder_resize_feature_guess": plan.get("cylinder_resize_feature_guess"), "cylinder_resize_confidence": plan.get("cylinder_resize_confidence"), "cylinder_resize_same_domain_face_ids": plan.get("cylinder_resize_same_domain_face_ids"), "cylinder_resize_same_domain_face_count": plan.get("cylinder_resize_same_domain_face_count"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="edge", target_id=edge_id, ) def resize_any_edge_length(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再修改Edge长度。"): return if self.selected_edge_id is None: QMessageBox.information(self, "未选择Edge", "请先选择一条Edge。") return try: target_length = float(self.edge_target_length_input.text()) except ValueError: QMessageBox.critical(self, "目标长度无效", "请输入数字形式的Edge目标长度。") return edge_id = self.selected_edge_id anchor_mode = self._edge_length_anchor_mode_from_ui() plan = self.model.general_edge_length_plan(edge_id, target_length, anchor_mode=anchor_mode) if plan["status"] == "blocked": QMessageBox.information(self, "不能修改Edge长度", str(plan["message"])) self.statusBar().showMessage("Edge长度修改已阻止") return warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" strategy = str(plan.get("resize_strategy", "")) if plan["risk"] != "low": result = QMessageBox.question( self, "确认修改Edge长度", ( f"Edge: {plan['edge_id']}\n" f"曲线类型: {_format_value(plan.get('curve', ''))}\n" f"当前Edge长度: {_format_value(plan['current_length'])}\n" f"目标Edge长度: {_format_value(plan['target_length'])}\n" f"变化量: {_format_value(plan['delta_length'])}\n" f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n" f"策略: {_format_value(strategy)}\n" f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n" f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n" f"重建面数: {_format_value(plan.get('local_edge_deform_face_count', ''))}\n" f"端面Face: {_format_value(plan.get('end_face_id', ''))}\n" f"端面推拉距离: {_format_value(plan.get('push_pull_distance', ''))}\n" f"圆柱Face: {_format_value(plan.get('cylinder_resize_face_id', ''))}\n" f"圆形Edge模式: {_format_value(plan.get('circular_edge_cylinder_mode_label', ''))}\n" f"换算目标直径: {_format_value(plan.get('cylinder_resize_target_diameter', ''))}\n" f"缩放类型: {_format_value(plan.get('affine_transform_label') or plan.get('affine_transform_kind', ''))}\n" f"缩放说明: {_format_value(plan.get('affine_transform_note', ''))}\n" f"缩放目标: {_format_value(plan.get('affine_target_kind', ''))}\n" f"缩放比例: {_format_value(plan.get('affine_scale', ''))}\n" f"预估Edge长度: {_format_value(plan.get('affine_predicted_edge_length', ''))}\n" f"缩放校正: {_format_value(plan.get('affine_scale_correction', ''))}\n" f"轴线来源: {_format_value(plan.get('affine_axis_source', ''))}\n" f"平面采样偏差: {_format_value(plan.get('planar_edge_scale_plane_deviation', ''))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消Edge长度修改") return self._show_edge_length_preview(edge_id, target_length, anchor_mode=anchor_mode) def action(): return self.model.resize_general_edge_length(edge_id, target_length, anchor_mode=anchor_mode) self._run_edit_action( action, operation_name="修改Edge长度", target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "curve": plan.get("curve"), "current_length": plan.get("current_length"), "target_length": plan.get("target_length"), "delta_length": plan.get("delta_length"), "length_change_ratio": plan.get("length_change_ratio"), "edge_length_anchor_mode": plan.get("edge_length_anchor_mode"), "edge_length_anchor_label": plan.get("edge_length_anchor_label"), "local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"), "local_edge_deform_anchor": plan.get("local_edge_deform_anchor"), "local_edge_deform_face_count": plan.get("local_edge_deform_face_count"), "local_edge_deform_start_move": plan.get("local_edge_deform_start_move"), "local_edge_deform_end_move": plan.get("local_edge_deform_end_move"), "local_edge_deform_moved_endpoint_count": plan.get("local_edge_deform_moved_endpoint_count"), "local_edge_deform_risk": plan.get("local_edge_deform_risk"), "local_edge_deform_note": plan.get("local_edge_deform_note"), "end_face_id": plan.get("end_face_id"), "end_face_endpoint_role": plan.get("end_face_endpoint_role"), "end_face_label": plan.get("end_face_label"), "push_pull_distance": plan.get("push_pull_distance"), "resize_strategy": plan.get("resize_strategy"), "affine_scale": plan.get("affine_scale"), "affine_initial_scale": plan.get("affine_initial_scale"), "affine_scale_correction": plan.get("affine_scale_correction"), "affine_predicted_edge_length": plan.get("affine_predicted_edge_length"), "affine_scale_refine_iterations": plan.get("affine_scale_refine_iterations"), "affine_scale_refine_note": plan.get("affine_scale_refine_note"), "affine_transform_kind": plan.get("affine_transform_kind"), "affine_transform_label": plan.get("affine_transform_label"), "affine_transform_note": plan.get("affine_transform_note"), "affine_transform_warning": plan.get("affine_transform_warning"), "affine_transform_risk": plan.get("affine_transform_risk"), "affine_axis_point": plan.get("affine_axis_point"), "affine_axis_direction": plan.get("affine_axis_direction"), "affine_axis_source": plan.get("affine_axis_source"), "affine_anchor_source": plan.get("affine_anchor_source"), "affine_target_kind": plan.get("affine_target_kind"), "planar_edge_scale_sample_count": plan.get("planar_edge_scale_sample_count"), "planar_edge_scale_plane_deviation": plan.get("planar_edge_scale_plane_deviation"), "circular_edge_current_radius": plan.get("circular_edge_current_radius"), "circular_edge_target_radius": plan.get("circular_edge_target_radius"), "circular_edge_length_scale": plan.get("circular_edge_length_scale"), "circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"), "circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"), "circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"), "cylinder_resize_face_id": plan.get("cylinder_resize_face_id"), "cylinder_resize_operation": plan.get("cylinder_resize_operation"), "cylinder_resize_current_diameter": plan.get("cylinder_resize_current_diameter"), "cylinder_resize_target_diameter": plan.get("cylinder_resize_target_diameter"), "cylinder_resize_delta_diameter": plan.get("cylinder_resize_delta_diameter"), "cylinder_resize_delta_ratio": plan.get("cylinder_resize_delta_ratio"), "cylinder_resize_status": plan.get("cylinder_resize_status"), "cylinder_resize_risk": plan.get("cylinder_resize_risk"), "cylinder_resize_feature_guess": plan.get("cylinder_resize_feature_guess"), "cylinder_resize_confidence": plan.get("cylinder_resize_confidence"), "cylinder_resize_same_domain_face_ids": plan.get("cylinder_resize_same_domain_face_ids"), "cylinder_resize_same_domain_face_count": plan.get("cylinder_resize_same_domain_face_count"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="edge", target_id=edge_id, ) def _edge_length_anchor_mode_from_ui(self) -> str: combo = getattr(self, "edge_length_anchor_combo", None) if combo is None: return "auto" data = combo.currentData() return str(data or "auto") def _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_edge_center_point(self) -> None: if self.model is None: return if self._edit_busy("Please wait for the current edit to finish before moving an Edge center."): return if self.selected_edge_id is None: QMessageBox.information(self, "No Edge selected", "Please select an Edge first.") return try: target_center = ( float(self.edge_center_x_input.text()), float(self.edge_center_y_input.text()), float(self.edge_center_z_input.text()), ) except (AttributeError, ValueError): QMessageBox.critical(self, "Edge center coordinate invalid", "Please enter numeric X, Y and Z values.") return edge_id = self.selected_edge_id plan = self.model.edge_center_move_plan(edge_id, target_center) lines = [ f"Edge: {edge_id}", f"Current center: {_format_value(plan.get('current_edge_center'))}", f"Target center: {_format_value(plan.get('target_edge_center'))}", f"Move vector: {_format_value(plan.get('moved_edge_center_delta'))}", f"Edge length: {_format_value(plan.get('current_length'))}", f"Strategy: {_format_value(plan.get('resize_strategy'))}", f"Rebuilt faces: {_format_value(plan.get('local_edge_deform_face_count'))}", ] if not self._confirm_quick_edit_plan( "移动Edge中心", plan, lines, blocked_status="移动Edge中心已阻止", cancelled_status="已取消移动Edge中心", ): return self.clear_edit_preview(render=False) def action(): return self.model.move_edge_center(edge_id, target_center) self._run_edit_action( action, operation_name="移动Edge中心", target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "curve": plan.get("curve"), "current_edge_center": plan.get("current_edge_center"), "target_edge_center": plan.get("target_edge_center"), "moved_edge_center_delta": plan.get("moved_edge_center_delta"), "moved_edge_center_distance": plan.get("moved_edge_center_distance"), "moved_edge_center_ratio": plan.get("moved_edge_center_ratio"), "edge_length": plan.get("current_length"), "expected_start_point": plan.get("expected_start_point"), "expected_end_point": plan.get("expected_end_point"), "local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"), "local_edge_deform_face_count": plan.get("local_edge_deform_face_count"), "local_edge_deform_start_move": plan.get("local_edge_deform_start_move"), "local_edge_deform_end_move": plan.get("local_edge_deform_end_move"), "resize_strategy": plan.get("resize_strategy"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="edge", target_id=edge_id, ) def _move_edge_endpoint(self, endpoint_role: str) -> None: if self.model is None: return if self._edit_busy("Please wait for the current edit to finish before moving an Edge endpoint."): return if self.selected_edge_id is None: QMessageBox.information(self, "No Edge selected", "Please select an Edge first.") return target_point = self._edge_endpoint_target_from_inputs(endpoint_role) if target_point is None: return edge_id = self.selected_edge_id plan = self.model.edge_endpoint_move_plan(edge_id, endpoint_role, target_point) title = "移动Edge起点" if endpoint_role == "start" else "移动Edge终点" lines = [ f"Edge: {edge_id}", f"Endpoint: {_format_value(plan.get('edge_endpoint_label'))}", f"Current endpoint: {_format_value(plan.get('current_endpoint_point'))}", f"Target endpoint: {_format_value(plan.get('target_endpoint_point'))}", f"Move vector: {_format_value(plan.get('moved_endpoint_delta'))}", f"Current Edge length: {_format_value(plan.get('current_length'))}", f"Result Edge length: {_format_value(plan.get('target_length'))}", f"Strategy: {_format_value(plan.get('resize_strategy'))}", f"Rebuilt faces: {_format_value(plan.get('local_edge_deform_face_count'))}", ] if not self._confirm_quick_edit_plan( title, plan, lines, blocked_status=f"{title}已阻止", cancelled_status=f"已取消{title}", ): return self.clear_edit_preview(render=False) def action(): return self.model.move_edge_endpoint(edge_id, endpoint_role, target_point) self._run_edit_action( action, operation_name=title, target=f"Edge {edge_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "curve": plan.get("curve"), "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, ) 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) if plan["status"] == "blocked": QMessageBox.information(self, "不能修改圆锥面", str(plan["message"])) self.statusBar().showMessage("圆锥面修改已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认修改圆锥面", ( f"Face: {face_id}\n" f"当前参考半径: {_format_value(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"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消圆锥面修改") return self.clear_edit_preview(render=False) def action(): return self.model.resize_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"), "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"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="face", target_id=face_id, ) def resize_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) if plan["status"] == "blocked": QMessageBox.information(self, "不能修改球面", str(plan["message"])) self.statusBar().showMessage("球面修改已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, "确认修改球面", ( f"Face: {face_id}\n" f"当前半径: {_format_value(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"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消球面修改") return self.clear_edit_preview(render=False) def action(): return self.model.resize_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"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="face", target_id=face_id, ) def resize_torus_major_radius(self) -> None: self._resize_torus_radius("major") def resize_torus_minor_radius(self) -> None: self._resize_torus_radius("minor") def _resize_torus_radius(self, mode: str) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再修改环面。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择环面", "请先选择一个环面 Face 或环面特征。") return if not hasattr(self, "torus_radius_input"): return try: target_radius = float(self.torus_radius_input.text()) except ValueError: QMessageBox.critical(self, "环面半径无效", "请输入数字形式的目标环面半径。") return mode_key = "major" if mode == "major" else "minor" mode_label = "主半径" if mode_key == "major" else "小半径" face_id = self.selected_face_id plan = self.model.toroidal_radius_plan(face_id, target_radius, mode_key) if plan["status"] == "blocked": QMessageBox.information(self, "不能修改环面", str(plan["message"])) self.statusBar().showMessage("环面修改已阻止") return if plan["risk"] != "low": warnings = str(plan.get("warnings", "")) warnings_line = f"警告: {warnings}\n\n" if warnings else "" result = QMessageBox.question( self, f"确认修改环面{mode_label}", ( f"Face: {face_id}\n" f"当前主半径: {_format_value(plan.get('current_major_radius'))}\n" f"目标主半径: {_format_value(plan.get('target_major_radius'))}\n" f"当前小半径: {_format_value(plan.get('current_minor_radius'))}\n" f"目标小半径: {_format_value(plan.get('target_minor_radius'))}\n" f"变化比例: {_format_percent(plan.get('radius_delta_ratio'))}\n" f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n" f"缩放比例: {_format_value(plan.get('affine_scale'))}\n" f"策略: {_format_value(plan.get('resize_strategy'))}\n" f"风险: {plan['risk']}\n\n" f"{warnings_line}" f"{plan['message']}\n\n" "继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?" ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if result != QMessageBox.StandardButton.Yes: self.statusBar().showMessage("已取消环面修改") return self.clear_edit_preview(render=False) def action(): return self.model.resize_toroidal_radius(face_id, target_radius, mode_key) self._run_edit_action( action, operation_name=f"修改环面{mode_label}", target=f"Face {face_id}", parameters={ "part_id": plan.get("part_id"), "solid_id": plan.get("solid_id"), "surface": "torus", "torus_radius_mode": plan.get("torus_radius_mode"), "current_major_radius": plan.get("current_major_radius"), "target_major_radius": plan.get("target_major_radius"), "current_minor_radius": plan.get("current_minor_radius"), "target_minor_radius": plan.get("target_minor_radius"), "target_radius": plan.get("target_radius"), "delta_radius": plan.get("delta_radius"), "radius_delta_ratio": plan.get("radius_delta_ratio"), "center": plan.get("center"), "axis": plan.get("axis"), "affine_scale": plan.get("affine_scale"), "affine_transform_kind": plan.get("affine_transform_kind"), "affine_transform_label": plan.get("affine_transform_label"), "affine_axis_point": plan.get("affine_axis_point"), "affine_axis_direction": plan.get("affine_axis_direction"), "affine_target_kind": plan.get("affine_target_kind"), "resize_strategy": plan.get("resize_strategy"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), "resize_warnings": plan.get("warnings"), "resize_blockers": plan.get("blockers"), }, target_kind="face", target_id=face_id, ) def resize_face_area(self) -> None: if self.model is None: return if self._edit_busy("请等待当前编辑完成后再修改Face面积。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择Face", "请先选择一个 Face。") return if not hasattr(self, "face_area_input"): return try: target_area = float(self.face_area_input.text()) except ValueError: QMessageBox.critical(self, "Face面积无效", "请输入数字形式的目标Face面积。") 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, "不能修改Face面积", str(plan["message"])) self.statusBar().showMessage("Face面积修改已阻止") return 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('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"风险: {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("已取消Face面积修改") 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="修改Face面积", 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_axis_point": plan.get("affine_axis_point"), "affine_target_kind": plan.get("affine_target_kind"), "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="face", target_id=face_id, ) 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() self.scan_thread = None self.scan_worker = None QTimer.singleShot(0, lambda: self._run_scan_task_sync(action)) def _run_scan_task_sync(self, action) -> None: if not self.scan_in_progress: return QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) QApplication.processEvents() try: result = action() except Exception as exc: self._fail_scan_task_result(str(exc)) else: self._finish_scan_task_result(result) finally: QApplication.restoreOverrideCursor() @Slot(object) def _finish_scan_task_result(self, result: object) -> None: scan_kind = self.pending_scan_kind context = dict(self.pending_scan_context or {}) if scan_kind == "editable": self._finish_editable_scan( result, bool(context.get("show_info", True)), str(context.get("scan_label", "扫描")), int(context.get("limit", 0)), ) return if scan_kind == "cylinder": self._finish_cylinder_scan(result, bool(context.get("show_info", True))) return self._end_scan_task() self.statusBar().showMessage("扫描已完成,但扫描类型已丢失。") @Slot(str) def _fail_scan_task_result(self, message: str) -> None: scan_kind = self.pending_scan_kind if scan_kind == "editable": self._fail_editable_scan(message) return if scan_kind == "cylinder": self._fail_cylinder_scan(message) return self._end_scan_task() QMessageBox.critical(self, "扫描失败", message) self.statusBar().showMessage("扫描失败") def _end_scan_task(self) -> None: self.scan_in_progress = False self.pending_scan_kind = None self.pending_scan_context = None self._update_action_states() self._request_thread_quit(self.scan_thread) def _forget_scan_thread(self) -> None: self.scan_thread = None self.scan_worker = None def list_cylinders(self) -> None: self.refresh_cylinder_candidates(show_info=True) def _clear_editable_candidates(self) -> None: if hasattr(self, "editable_table"): self.editable_table.setRowCount(0) def refresh_editable_candidates(self, show_info: bool = True, deep_scan: bool = False) -> None: if self.model is None: return if self._edit_busy("编辑计算中,暂时不能扫描可编辑对象。"): return limit = 48 if deep_scan else 24 max_scan_faces = 1600 if deep_scan else 700 max_scan_edges = 2500 if deep_scan else 1200 scan_label = "深度扫描" if deep_scan else "扫描" self.statusBar().showMessage(f"正在{scan_label}可编辑对象...") def action(): if self.model is None: return [] return self.model.editable_feature_candidates( limit=limit, max_scan_faces=max_scan_faces, max_scan_edges=max_scan_edges, ) self._run_scan_task( "editable", action, { "show_info": show_info, "scan_label": scan_label, "limit": limit, }, ) def _finish_editable_scan(self, result: object, show_info: bool, scan_label: str, limit: int) -> None: try: candidates = list(result) if isinstance(result, (list, tuple)) else [] self._populate_editable_candidates(candidates, show_info, scan_label, limit) finally: self._end_scan_task() def _fail_editable_scan(self, message: str) -> None: self._end_scan_task() QMessageBox.critical(self, "扫描失败", message) self.statusBar().showMessage("可编辑对象扫描失败") def _populate_editable_candidates( self, candidates: list[dict[str, object]], show_info: bool, scan_label: str, limit: int, ) -> None: if not hasattr(self, "editable_table"): if show_info: self.set_plain_info("可编辑对象面板当前未启用。") return self.editable_table.setRowCount(len(candidates)) lines = [f"可编辑对象:显示 {len(candidates)} 个({scan_label}上限 {limit})"] for row, item in enumerate(candidates): target_kind = str(item.get("target_kind", "face")) target_id = int(item.get("target_id", item.get("face_id", item.get("edge_id", -1)))) operation_item = QTableWidgetItem(str(item["operation"])) operation_item.setData(EDITABLE_TARGET_ID_ROLE, target_id) operation_item.setData(EDITABLE_TARGET_KIND_ROLE, target_kind) operation_item.setData(EDITABLE_ACTION_ROLE, str(item["operation_key"])) id_item = QTableWidgetItem(f"{target_kind} {target_id}") object_item = QTableWidgetItem(str(item["feature_guess"])) current_item = QTableWidgetItem( f"{item['current_value_label']}={_format_value(item['current_value'])}" ) status_item = QTableWidgetItem(str(item["status"])) risk_item = QTableWidgetItem(str(item["risk"])) confidence_item = QTableWidgetItem(str(item["confidence"])) note_item = QTableWidgetItem(str(item["note"])) row_items = [ operation_item, id_item, object_item, current_item, status_item, risk_item, confidence_item, note_item, ] for column, table_item in enumerate(row_items): table_item.setToolTip(table_item.text()) self.editable_table.setItem(row, column, table_item) lines.append( f"{target_kind} {target_id}: {item['operation']}, " f"零件={item['part_id']}, Solid={item['solid_id']}, " f"object={item['feature_guess']}, " f"{item['current_value_label']}={_format_value(item['current_value'])}, " f"status={item['status']}, risk={item['risk']}, " f"confidence={item['confidence']}, note={item['note']}" ) self.editable_table.resizeColumnsToContents() self.statusBar().showMessage(f"已{scan_label}可编辑对象:{len(candidates)} 个") if show_info: self.set_plain_info("\n".join(lines)) def _clear_cylinder_candidates(self) -> None: self.cylinder_candidate_cache = [] self.cylinder_candidates_loaded = False if hasattr(self, "cylinder_table"): self.cylinder_table.setRowCount(0) def refresh_cylinder_candidates(self, show_info: bool = True) -> None: if self.model is None: return if self._edit_busy("编辑计算中,暂时不能扫描圆柱面候选。"): return self.statusBar().showMessage("正在扫描圆柱面候选...") def action(): if self.model is None: return [] return self.model.cylindrical_feature_candidates(limit=60, max_scan_faces=1200) self._run_scan_task( "cylinder", action, {"show_info": show_info}, ) def _finish_cylinder_scan(self, result: object, show_info: bool) -> None: try: self.cylinder_candidate_cache = list(result) if isinstance(result, (list, tuple)) else [] self.cylinder_candidates_loaded = True self._filter_cached_cylinder_candidates(show_info=show_info) finally: self._end_scan_task() def _fail_cylinder_scan(self, message: str) -> None: self._end_scan_task() self._clear_cylinder_candidates() QMessageBox.critical(self, "扫描失败", message) self.statusBar().showMessage("圆柱面候选扫描失败") def _filter_cached_cylinder_candidates(self, show_info: bool = False) -> None: if not hasattr(self, "cylinder_table"): if show_info: self.set_plain_info("圆柱面候选面板当前未启用。") return if not self.cylinder_candidates_loaded: self.cylinder_table.setRowCount(0) if show_info: self.set_plain_info("圆柱面候选尚未扫描。点击 `扫描圆柱面` 后再切换筛选类型。") return candidates = [ item for item in self.cylinder_candidate_cache if self._candidate_matches_filter(str(item["feature_guess"])) ] self._populate_cylinder_table(candidates, show_info=show_info) def _populate_cylinder_table(self, candidates: list[dict[str, object]], show_info: bool = True) -> None: if not hasattr(self, "cylinder_table"): if show_info: self.set_plain_info("圆柱面候选面板当前未启用。") return self.cylinder_table.setRowCount(len(candidates)) lines = [f"圆柱面候选:显示 {len(candidates)} 个"] for row, item in enumerate(candidates): face_item = QTableWidgetItem(str(item["face_id"])) face_item.setData(Qt.UserRole, int(item["face_id"])) guess_item = QTableWidgetItem(str(item["feature_guess"])) diameter_item = QTableWidgetItem(_format_float(float(item["diameter"]))) span_item = QTableWidgetItem(_format_float(float(item["angular_span"]))) height_item = QTableWidgetItem(_format_float(float(item["height_estimate"]))) confidence_item = QTableWidgetItem(str(item["confidence"])) risk_item = QTableWidgetItem(str(item["resize_risk"])) part_item = QTableWidgetItem(str(item["part_id"])) self.cylinder_table.setItem(row, 0, face_item) self.cylinder_table.setItem(row, 1, guess_item) self.cylinder_table.setItem(row, 2, diameter_item) self.cylinder_table.setItem(row, 3, span_item) self.cylinder_table.setItem(row, 4, height_item) self.cylinder_table.setItem(row, 5, confidence_item) self.cylinder_table.setItem(row, 6, risk_item) self.cylinder_table.setItem(row, 7, part_item) lines.append( f"Face {item['face_id']}: 零件 {item['part_id']}, " f"guess={item['feature_guess']}, " f"diameter={_format_float(float(item['diameter']))}, " f"height={_format_float(float(item['height_estimate']))}, " f"confidence={item['confidence']}, " f"risk={item['resize_risk']}" ) self.cylinder_table.resizeColumnsToContents() self.statusBar().showMessage(f"已显示圆柱面候选:{len(candidates)} 个") if show_info: self.set_plain_info("\n".join(lines)) def _candidate_matches_filter(self, feature_guess: str) -> bool: if not hasattr(self, "candidate_filter_combo"): return True current = self.candidate_filter_combo.currentText() if current == "All": return True if current == "Hole/Groove": return feature_guess == "hole/groove candidate" if current == "Round/Fillet": return feature_guess == "round/fillet candidate" if current == "Boss/Outer": return feature_guess == "boss/outer-round candidate" if current == "Unclear": return feature_guess == "cylindrical face" return True def _run_action(self, action, success_message: str | None) -> None: try: result = action() except Exception as exc: QMessageBox.critical(self, "操作失败", str(exc)) self.statusBar().showMessage("操作失败") return message = success_message or str(result) self.statusBar().showMessage(message) self.set_plain_info(message) def _block_unisolated_high_risk_operation(self, title: str, plan: dict[str, object]) -> bool: if str(plan.get("risk", "")) != "high": return False QMessageBox.information( self, title, ( "该操作被稳定性保护阻止。\n\n" "当前计划被判定为 high risk;这类 OCCT 布尔、倒圆或局部重建在复杂 STEP 上" "可能不是普通失败,而是让进程卡死或直接退出。请先尝试更小的参数、修复模型、" "选择更明确的面/边,或等后续子进程隔离执行通道实现后再开放。" ), ) self.statusBar().showMessage("高风险操作已被稳定性保护阻止") return True def _run_edit_action( self, action, operation_name: str, target: str, parameters: dict[str, object], target_kind: str | None = None, target_id: int | None = None, ) -> None: if self.model is None: return if self.operation_in_progress: self.statusBar().showMessage("后台编辑正在计算,请等待当前操作完成。") return target_logical_id = self._edit_target_logical_id(target_kind, target_id) 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, } self._begin_edit_task(operation_name) self.pending_edit_context = context thread = QThread(self) worker = EditWorker(self._make_edit_job(action, context)) worker.moveToThread(thread) thread.started.connect(worker.run) worker.finished.connect(self._finish_edit_action, Qt.ConnectionType.QueuedConnection) worker.failed.connect(self._fail_edit_action, Qt.ConnectionType.QueuedConnection) thread.finished.connect(worker.deleteLater) thread.finished.connect(thread.deleteLater) thread.finished.connect(self._forget_edit_thread) self.edit_thread = thread self.edit_worker = worker thread.start() def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None: if self.model is None or target_id is None or target_kind not in {"face", "feature"}: return None try: numeric_id = int(target_id) except (TypeError, ValueError): return None if 0 <= numeric_id < len(self.model.faces): try: return self.model.face_region_logical_id(numeric_id) except Exception: return None return None def _make_edit_job(self, action, context: dict[str, object]): def job(): if self.model is None: raise RuntimeError("Model is not loaded.") snapshot = self.model.snapshot() target_part_id = self._edit_context_part_id(context) before_stats = self.model.stats() before_part_stats = self._part_stats_or_none(target_part_id) before_geometry = {} try: result = action() after_snapshot = self.model.snapshot() after_stats = self.model.stats() after_part_stats = self._part_stats_or_none(target_part_id) after_geometry = {} except Exception as exc: try: self.model.restore_snapshot(snapshot) except Exception as rollback_exc: raise RuntimeError( f"编辑失败,且回滚到操作前状态也失败:{rollback_exc}\n原始错误:{exc}" ) from exc raise RuntimeError(f"编辑失败,模型已恢复到操作前状态:{exc}") from exc model_polydata = None edge_polydata = None try: 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)) ) except Exception: model_polydata = None edge_polydata = None return { "message": str(result), "snapshot": snapshot, "before_stats": before_stats, "before_part_stats": before_part_stats, "before_geometry": before_geometry, "after_snapshot": after_snapshot, "after_stats": after_stats, "after_part_stats": after_part_stats, "quality_warnings": _edit_quality_warnings(before_part_stats, after_part_stats), "after_geometry": after_geometry, "model_polydata": model_polydata, "edge_polydata": edge_polydata, } return job def _edit_context_part_id(self, context: dict[str, object]) -> int | None: if self.model is None: return None parameters = context.get("parameters") if isinstance(parameters, dict): part_id = parameters.get("part_id") if part_id not in {"", None}: try: return int(part_id) except (TypeError, ValueError): pass target_kind = context.get("target_kind") target_id = context.get("target_id") if target_id is None: return None try: numeric_id = int(target_id) except (TypeError, ValueError): return None if target_kind == "part": return numeric_id if target_kind in {"face", "feature"} and 0 <= numeric_id < len(self.model.face_part_ids): return int(self.model.face_part_ids[numeric_id]) if target_kind == "edge" and 0 <= numeric_id < len(self.model.edge_part_ids): return int(self.model.edge_part_ids[numeric_id]) if target_kind == "solid" and 0 <= numeric_id < len(self.model.solids): return int(self.model.solids[numeric_id][0]) return None def _part_stats_or_none(self, part_id: int | None): if self.model is None or part_id is None: return None try: return self.model.part_topology_stats(part_id) except Exception: return None def _begin_edit_task(self, operation_name: str) -> None: self.operation_in_progress = True self._update_action_states() if "推拉" in operation_name: progress_text = f"{operation_name} 正在后台计算,半透明预览会持续渲染,3D 视图仍可旋转查看。" elif "孔径" in operation_name: progress_text = f"{operation_name} 正在后台计算;为避免复杂模型卡顿,已跳过同步红/绿预览。" elif "槽/半孔宽度" in operation_name: progress_text = f"{operation_name} 正在后台计算;槽宽会在后台换算并重建孔/槽几何。" elif "槽/半孔深度" in operation_name: progress_text = f"{operation_name} 正在后台计算;槽深会在后台换算并重建孔/槽几何。" elif "槽/半孔圆弧长度" in operation_name: progress_text = f"{operation_name} 正在后台计算;圆弧长度会在后台换算并重建孔/槽几何。" elif "凸台高度" in operation_name: progress_text = f"{operation_name} 正在后台计算,预览表示将推拉的凸台端盖范围。" elif "凸台" in operation_name: progress_text = f"{operation_name} 正在后台计算,绿色预览表示扩大补料范围,红色预览表示缩小切削范围。" elif "封堵" in operation_name: progress_text = f"{operation_name} 正在后台计算,绿色预览表示封堵补料范围。" elif "孔深" in operation_name: progress_text = f"{operation_name} 正在后台计算;为避免复杂模型卡顿,已跳过同步孔深预览。" elif "圆角" in operation_name: if "已有" in operation_name: progress_text = f"{operation_name} 正在后台计算,蓝色预览表示将移除并重建的已有圆角面。" else: progress_text = f"{operation_name} 正在后台计算,蓝色预览表示目标边和半径范围。" elif "倒角" in operation_name: progress_text = f"{operation_name} 正在后台计算,橙色预览表示目标边和倒角距离范围。" elif "边长" in operation_name or "Edge长度" in operation_name: progress_text = f"{operation_name} 正在后台计算,预览表示局部边形变、端面推拉或后备策略后的目标范围。" elif "平移" in operation_name: progress_text = f"{operation_name} 正在后台计算,完成后会刷新模型位置和拓扑索引。" elif "旋转" in operation_name: progress_text = f"{operation_name} 正在后台计算,完成后会刷新模型姿态和拓扑索引。" else: progress_text = f"{operation_name} 正在后台计算。" self._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 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 self._end_edit_task(clear_preview=True) QMessageBox.critical(self, "操作失败", message) self._clear_editable_candidates() self._clear_cylinder_candidates() self.statusBar().showMessage("操作失败,模型已保持在编辑前状态") def _restore_failed_edit_snapshot(self, snapshot: object) -> str: if self.model is None or not isinstance(snapshot, dict): return "未找到可用的编辑前快照,请重新加载 STEP 文件确认状态。" try: self.model.restore_snapshot(snapshot) self._reset_selection() self._populate_part_tree() self._rebuild_scene(reset_camera=False) self._clear_editable_candidates() self._clear_cylinder_candidates() except Exception as rollback_exc: return f"尝试回滚到编辑前状态失败:{rollback_exc}。建议重新加载 STEP 文件。" return "模型已回滚到编辑前状态。" def _end_edit_task(self, clear_preview: bool = True) -> None: self.operation_in_progress = False self._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 "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_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, )