from __future__ import annotations from datetime import datetime import math from pathlib import Path import vtk from PySide6.QtCore import Qt, QThread, QTimer, Slot from PySide6.QtWidgets import ( QApplication, QFileDialog, QMessageBox, QTableWidgetItem, QTreeWidgetItem, ) from .model import StepModel from .records import OperationRecord from .ui_helpers import * # noqa: F403 from .workers import EditWorker, LoadWorker, ScanWorker class WindowStateMixin: def _reset_selection(self, clear_highlight: bool = True) -> None: self.selected_kind = None self.selected_part_id = None self.selected_solid_id = None self.selected_face_id = None self.selected_edge_id = None self.selected_pick_position = None if clear_highlight: self._clear_highlight() self._update_action_states() def _show_pick_marker(self, pick_position: tuple[float, float, float] | None) -> None: if pick_position is None: return radius = self._pick_marker_radius() sphere = vtk.vtkSphereSource() sphere.SetCenter(*pick_position) sphere.SetRadius(radius) sphere.SetThetaResolution(20) sphere.SetPhiResolution(12) mapper = vtk.vtkPolyDataMapper() mapper.SetInputConnection(sphere.GetOutputPort()) actor = vtk.vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetColor(0.1, 0.95, 0.95) actor.GetProperty().SetSpecular(0.4) actor.GetProperty().SetSpecularPower(18) self.pick_marker_actor = actor self.renderer.AddActor(actor) self.render_window.Render() def _pick_marker_radius(self) -> float: bounds = self.model_actor.GetBounds() if self.model_actor is not None else None if bounds is None: return 1.0 dx = bounds[1] - bounds[0] dy = bounds[3] - bounds[2] dz = bounds[5] - bounds[4] diagonal = math.sqrt(dx * dx + dy * dy + dz * dz) return max(diagonal * 0.004, 0.1) def _with_pick_info( self, info: dict[str, object], pick_position: tuple[float, float, float] | None, ) -> dict[str, object]: if pick_position is None: return info enriched = dict(info) enriched["pick_position"] = pick_position return enriched def _selection_status(self, message: str, pick_position: tuple[float, float, float] | None) -> str: if pick_position is None: return message return f"{message},拾取点 {_format_value(pick_position)}" def _edit_busy(self, message: str = "编辑计算中,请等待当前操作完成。") -> bool: if self.load_in_progress: self.statusBar().showMessage("STEP background loading is still running.") return True if self.operation_in_progress: self.statusBar().showMessage(message) return True if self.scan_in_progress: self.statusBar().showMessage("扫描中,请等待扫描完成后再执行该操作。") return True return False def _sync_id_picker(self, kind: str, target_id: int) -> None: self.last_id_kind = kind self.id_input.setText(str(target_id)) def _update_action_states(self) -> None: if not hasattr(self, "export_all_button"): return has_loaded_model = self.model is not None busy = self.operation_in_progress or self.scan_in_progress or self.load_in_progress has_model = has_loaded_model and not busy selected_kind = self.selected_kind has_selection = any( item is not None for item in ( self.selected_part_id, self.selected_solid_id, self.selected_face_id, self.selected_edge_id, ) ) has_part = self.selected_part_id is not None has_solid = self.selected_solid_id is not None has_face = self.selected_face_id is not None and selected_kind in {"face", "feature"} has_edge = self.selected_edge_id is not None and selected_kind == "edge" id_text = self.id_input.text().strip() if hasattr(self, "id_input") else "" history_row = self.history_list.currentRow() if hasattr(self, "history_list") else -1 has_history = has_model and bool(getattr(self, "operation_history", [])) has_history_selection = has_history and 0 <= history_row < len(self.operation_history) has_diff_preview = has_model and bool(getattr(self, "diff_actors", [])) has_measure_points = bool(getattr(self, "measure_point_a", None) or getattr(self, "measure_point_b", None)) has_measure_result = bool(getattr(self, "measure_point_a", None) and getattr(self, "measure_point_b", None)) pick_value = getattr(self, "current_info_values", {}).get("pick_position") has_pick_position = self.selected_pick_position is not None or ( isinstance(pick_value, (tuple, list)) and len(pick_value) == 3 ) selected_id_text = self._selected_id_text() if hasattr(self, "_selected_id_text") else "" has_current_info = bool(getattr(self, "current_info_text", "")) wait_or_load_tip = "请先加载 STEP 文件,或等待当前后台任务完成。" if hasattr(self, "open_button"): self._set_control_state( self.open_button, not busy, "选择并打开一个 .step 或 .stp 文件。", "请等待当前后台任务完成后再打开其他 STEP 文件。", ) self._set_control_state( self.reload_button, has_loaded_model and not busy, "从磁盘重新读取当前 STEP 文件。", wait_or_load_tip, ) if hasattr(self, "part_tree"): self._set_control_state( self.part_tree, has_model, "从结构树中选择 Part、Solid 或装配节点。", wait_or_load_tip, ) if hasattr(self, "mode_combo"): self._set_control_state( self.mode_combo, not busy, "选择鼠标点击模型时要选什么对象。", "请等待当前后台任务完成后再切换鼠标选择模式。", ) if hasattr(self, "id_input"): self._set_control_state( self.id_input, has_model, "输入要选中的对象 ID。", wait_or_load_tip, ) self._set_control_state( self.select_id_button, has_model and bool(id_text), "按当前选择模式跳转到输入的 ID。", "请先加载模型并输入整数 ID。", ) self._set_control_state( self.export_all_button, has_model, "导出当前完整 STEP 模型。", wait_or_load_tip, ) self._set_control_state( self.export_check_button, has_model, "检查当前导出对象的 B-Rep、数量、体积和包围盒等质量信息。", wait_or_load_tip, ) if hasattr(self, "repair_model_button"): self._set_control_state( self.repair_model_button, has_model, "对当前完整模型执行 ShapeFix 和同域面/边合并,并写入撤销历史。", wait_or_load_tip, ) self._set_control_state( self.repair_selected_button, has_model and (has_solid or has_part), "优先修复当前选中对象所属Solid;没有Solid时修复所属零件,并写入撤销历史。", "请先选择 Part、Solid、Face 或 Edge,或等待当前后台任务完成。", ) self._set_control_state( self.export_part_button, has_model and has_part, "导出当前选中对象所属零件。", "请先选择 Part,或选择属于某个 Part 的 Solid、Face 或 Edge。", ) self._set_control_state( self.export_solid_button, has_model and has_solid, "导出当前选中对象所属 Solid。", "请先选择 Solid,或选择属于某个 Solid 的 Face 或 Edge。", ) self._set_control_state( self.export_face_button, has_model and has_face, "导出当前选中的 Face 或同域面区域。", "请先在 Face 或 Feature 模式下选择一个 Face。", ) self._set_control_state( self.export_feature_button, has_model and selected_kind == "feature" and self.selected_face_id is not None, "导出 Feature 模式识别到的局部特征区域。", "请先切换到 Feature 模式并选择孔、槽、圆角或凸台候选。", ) self._set_control_state( self.export_edge_button, has_model and has_edge, "导出当前选中的 Edge。", "请先切换到 Edge 模式并选择一条 Edge。", ) if hasattr(self, "isolate_button"): self._set_control_state( self.isolate_button, has_model and has_selection, "只显示当前选中的对象或区域。", "请先加载模型并选择 Part、Solid、Face、Edge 或 Feature。", ) self._set_control_state( self.fit_button, has_model and has_selection, "把相机对准当前选中对象。", "请先加载模型并选择一个对象。", ) self._set_control_state( self.show_all_button, has_model, "恢复显示完整模型。", wait_or_load_tip, ) self._set_control_state( self.show_internal_edges_checkbox, has_model, "显示或隐藏同域内部拓扑边。", wait_or_load_tip, ) if hasattr(self, "set_measure_a_button"): measure_point_tip = "把当前选中对象的拾取点或中心设为测量点。" self._set_control_state( self.set_measure_a_button, has_model and has_selection, measure_point_tip, "请先加载模型并选择一个对象。", ) self._set_control_state( self.set_measure_b_button, has_model and has_selection, measure_point_tip, "请先加载模型并选择一个对象。", ) self._set_control_state( self.copy_measure_button, has_measure_result, "复制 A/B 两点距离和 X/Y/Z 差值。", "请先设置测量点 A 和 B。", ) self._set_control_state( self.clear_measure_button, has_measure_points, "清除 A/B 测量点和 3D 测量线。", "当前没有可清除的测量点。", ) if hasattr(self, "history_list"): self._set_control_state( self.history_list, has_history, "点击历史记录可查看参数、定位目标并显示差异预览。", "当前还没有编辑历史。", ) self._set_control_state( self.clear_diff_button, has_diff_preview, "清除当前显示的差异预览。", "当前没有正在显示的差异预览。", ) self._set_control_state( self.export_diff_button, has_history_selection, "导出当前选中历史记录的差异报告。", "请先在操作历史中选择一条记录。", ) self._set_control_state( self.export_history_button, has_history, "导出本次会话的完整编辑历史 JSON。", "当前还没有可导出的编辑历史。", ) if hasattr(self, "copy_id_button"): self._set_control_state( self.copy_id_button, bool(selected_id_text), "复制当前选中对象的 ID。", "请先选择一个对象。", ) self._set_control_state( self.copy_pick_button, has_pick_position, "复制最近一次鼠标拾取到的三维坐标。", "当前没有可复制的拾取坐标。", ) self._set_control_state( self.copy_info_button, has_current_info, "复制当前属性区里的完整文本。", "当前属性区还没有可复制的信息。", ) self._update_edit_action_states(has_model) def _update_edit_action_states(self, has_model: bool) -> None: if not hasattr(self, "push_button"): return action_info = self._selected_action_info() if has_model else {} surface = str(action_info.get("surface", "")) curve = str(action_info.get("curve", "")) feature_guess = str(action_info.get("feature_guess", "")) angular_span = _float_or_none(action_info.get("angular_span")) has_face = self.selected_face_id is not None and self.selected_kind in {"face", "feature"} has_edge = self.selected_edge_id is not None and self.selected_kind == "edge" is_plane = has_face and surface == "plane" is_shell_candidate = is_plane and action_info.get("shell_region_status") == "candidate" is_cylinder = has_face and surface == "cylinder" and "diameter" in action_info is_hole_or_groove = is_cylinder and feature_guess == "hole/groove candidate" is_boss = is_cylinder and feature_guess == "boss/outer-round candidate" is_existing_fillet = is_cylinder and feature_guess == "round/fillet candidate" is_full_cylinder = angular_span is not None and angular_span >= math.tau * 0.92 is_slot_or_half_hole = ( is_hole_or_groove and angular_span is not None and angular_span < math.tau * 0.92 and _float_or_none(action_info.get("slot_chord_width_estimate")) is not None ) is_blind = action_info.get("cylinder_end_type") == "blind" has_bottom = bool(_int_values(action_info.get("feature_bottom_face_ids"))) has_manual_bottom = bool( hasattr(self, "hole_bottom_face_input") and self.hole_bottom_face_input.text().strip() ) has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2 is_line_edge = has_edge and curve == "line" can_push = has_model and is_plane can_resize_shell = has_model and is_shell_candidate can_resize_hole = has_model and is_hole_or_groove can_resize_slot = has_model and is_slot_or_half_hole can_resize_boss = has_model and is_boss and is_full_cylinder can_suppress_hole = has_model and is_hole_or_groove and is_full_cylinder can_resize_depth = has_model and is_hole_or_groove and ((is_blind and has_bottom) or has_manual_bottom) can_fillet_edge = has_model and is_line_edge can_resize_existing_fillet = has_model and is_existing_fillet and has_fillet_support can_chamfer_edge = has_model and is_line_edge can_resize_edge_length = has_model and has_edge can_transform_part = has_model and self.selected_part_id is not None can_transform_solid = has_model and self.selected_solid_id is not None self._set_control_state( self.offset_input, can_push, "输入平面 Face 的推拉距离。", "请先选择一个可推拉的平面 Face。", ) self._set_control_state( self.push_button, can_push, "对当前平面 Face 执行推拉。", "请先选择一个平面 Face,或在 Feature 模式下选择可推拉平面候选。", ) if hasattr(self, "resize_shell_thickness_button"): self._set_control_state( self.shell_thickness_input, can_resize_shell, "输入薄壁/壳体局部区域的目标厚度。", "请先选择一个已识别到相对平面的薄壁/壳体平面候选。", ) self._set_control_state( self.resize_shell_thickness_button, can_resize_shell, "按目标厚度推拉当前薄壁/壳体平面区域。", "请先选择一个已识别到相对平面的薄壁/壳体平面候选。", ) self._set_control_state( self.hole_diameter_input, can_resize_hole, "输入圆柱孔/槽的目标直径。", "请先选择被识别为孔/槽候选的圆柱 Face。", ) self._set_control_state( self.resize_button, can_resize_hole, "调整当前圆柱孔/槽候选的直径。", "请先选择被识别为孔/槽候选的圆柱 Face。", ) if hasattr(self, "resize_slot_button"): self._set_control_state( self.slot_width_input, can_resize_slot, "输入槽/半孔候选的目标宽度。", "请先选择被识别为槽/半孔的部分圆柱 Face。", ) self._set_control_state( self.resize_slot_button, can_resize_slot, "按目标槽宽调整当前槽/半孔候选;当前版本会换算为对应圆柱直径后执行。", "请先选择被识别为槽/半孔的部分圆柱 Face。", ) self._set_control_state( self.boss_diameter_input, can_resize_boss, "输入完整圆柱凸台候选的目标直径。", "请先选择被识别为完整凸台/外圆候选的圆柱 Face。", ) self._set_control_state( self.resize_boss_button, can_resize_boss, "调整当前完整圆柱凸台候选的直径。", "请先选择被识别为完整凸台/外圆候选的圆柱 Face。", ) self._set_control_state( self.suppress_button, can_suppress_hole, "封堵当前完整圆柱孔候选。", "请先选择接近完整圆柱的孔候选;半孔/槽不会放行。", ) self._set_control_state( self.hole_depth_input, can_resize_hole, "输入盲孔/盲槽的目标深度。", "请先选择孔/槽圆柱面。", ) self._set_control_state( self.hole_bottom_face_input, can_resize_hole, "必要时输入底面 Face ID,用来明确盲孔/盲槽深度。", "请先选择孔/槽圆柱面。", ) self._set_control_state( self.resize_depth_button, can_resize_depth, "调整当前盲孔/盲槽深度;自动底面不稳定时可手动填写底面 Face ID。", "请先选择孔/槽圆柱面;如果没有自动识别到底面,请填写底面 Face ID。", ) self._set_control_state( self.edge_fillet_radius_input, can_fillet_edge or can_resize_existing_fillet, "输入新圆角或已有圆角的目标半径。", "请先选择直线Edge,或选择可修改的已有圆角 Face。", ) self._set_control_state( self.fillet_edge_button, can_fillet_edge, "给当前直线Edge添加新圆角。", "请先选择一条直线Edge。", ) self._set_control_state( self.resize_existing_fillet_button, can_resize_existing_fillet, "尝试修改当前已有圆角/倒圆候选的半径。", "请先选择一个已有圆角/倒圆候选 Face;当前版本需要识别到至少两个支撑 Face。", ) self._set_control_state( self.edge_chamfer_distance_input, can_chamfer_edge, "输入当前直线Edge的倒角距离。", "请先选择一条直线Edge。", ) self._set_control_state( self.chamfer_edge_button, can_chamfer_edge, "给当前直线Edge添加倒角。", "请先选择一条直线Edge。", ) self._set_control_state( self.edge_target_length_input, can_resize_edge_length, "输入当前Edge的目标长度。", "请先选择一条Edge。", ) self._set_control_state( self.edge_length_anchor_combo, can_resize_edge_length, "选择修改Edge长度时尽量固定哪一端。", "请先选择一条Edge。", ) self._set_control_state( self.resize_edge_length_button, can_resize_edge_length, "修改当前Edge的长度;直线Edge优先局部形变,圆/椭圆/平面曲线优先用局部或径向策略。", "请先选择一条Edge。", ) for widget in (self.translate_x_input, self.translate_y_input, self.translate_z_input): self._set_control_state( widget, can_transform_part or can_transform_solid, "输入选中零件或 Solid 的平移距离。", "请先选择一个零件、Solid,或其所属 Face/Edge。", ) self._set_control_state( self.translate_part_button, can_transform_part, "平移当前选中对象所属零件。", "请先选择一个零件,或选择属于某个零件的对象。", ) self._set_control_state( self.rotate_part_button, can_transform_part, "旋转当前选中对象所属零件。", "请先选择一个零件,或选择属于某个零件的对象。", ) self._set_control_state( self.rotate_axis_combo, can_transform_part or can_transform_solid, "选择选中零件或 Solid 的旋转轴。", "请先选择一个零件、Solid,或其所属 Face/Edge。", ) self._set_control_state( self.rotate_angle_input, can_transform_part or can_transform_solid, "输入选中零件或 Solid 的旋转角度。", "请先选择一个零件、Solid,或其所属 Face/Edge。", ) self._set_control_state( self.translate_solid_button, can_transform_solid, "平移当前选中对象所属 Solid。", "请先选择一个 Solid,或选择属于某个 Solid 的 Face/Edge。", ) self._set_control_state( self.rotate_solid_button, can_transform_solid, "旋转当前选中对象所属 Solid。", "请先选择一个 Solid,或选择属于某个 Solid 的 Face/Edge。", ) self._set_control_state( self.cylinders_button, has_model, "扫描当前模型中的圆柱面候选。", "请先加载 STEP 文件。", ) if hasattr(self, "editable_refresh_button"): self._set_control_state( self.editable_refresh_button, has_model, "扫描可编辑对象。", "请先加载 STEP 文件,或等待当前后台任务完成。", ) self._set_control_state( self.editable_deep_scan_button, has_model, "深度扫描更多可编辑对象。", "请先加载 STEP 文件,或等待当前后台任务完成。", ) self._set_control_state( self.editable_table, has_model and self.editable_table.rowCount() > 0, "点击一行可选中对应对象并填入相关编辑参数。", "当前没有可编辑对象列表;请先点击扫描对象。", ) if hasattr(self, "candidate_filter_combo"): has_cylinder_candidates = has_model and bool(getattr(self, "cylinder_candidates_loaded", False)) self._set_control_state( self.candidate_filter_combo, has_cylinder_candidates, "筛选已扫描的圆柱面候选。", "请先点击扫描圆柱面。", ) self._set_control_state( self.cylinder_table, has_model and self.cylinder_table.rowCount() > 0, "点击一行可选中对应圆柱 Face。", "当前没有圆柱面候选列表;请先点击扫描圆柱面。", ) self._set_control_state( self.undo_button, has_model and bool(self.undo_stack), "撤销上一步编辑。", "当前没有可撤销的编辑。", ) self._set_control_state( self.redo_button, has_model and bool(self.redo_stack), "重做刚撤销的编辑。", "当前没有可重做的编辑。", ) def _set_control_state(self, widget, enabled: bool, enabled_tip: str, disabled_tip: str) -> None: widget.setEnabled(enabled) tip = enabled_tip if enabled else disabled_tip if hasattr(self, "_set_help_tip"): self._set_help_tip(widget, tip) else: widget.setToolTip(tip) def _selected_action_info(self) -> dict[str, object]: if self.model is None: return {} try: if self.selected_kind == "feature" and self.selected_face_id is not None: return self.model.feature_info(self.selected_face_id) if self.selected_kind == "face" and self.selected_face_id is not None: info = self.model.face_info(self.selected_face_id) if info.get("surface") in {"cylinder", "plane"}: return self.model.feature_info(self.selected_face_id) return info if self.selected_kind == "edge" and self.selected_edge_id is not None: return self.model.edge_info(self.selected_edge_id) except Exception: return dict(self.current_info_values) return dict(self.current_info_values) def _locate_operation_record(self, record: OperationRecord) -> str: if self.model is None: return "" self._reset_selection(clear_highlight=True) located = False locator_note = "" if record.target_kind in {"face", "feature"} and record.target_id is not None: lookup_id = record.target_logical_id if record.target_logical_id is not None else record.target_id resolved_face_id = self.model.resolve_face_selection_id(lookup_id) if resolved_face_id is not None: info = self.model.feature_info(resolved_face_id) if record.target_kind == "feature" else self.model.face_info(resolved_face_id) self.selected_kind = "feature" if record.target_kind == "feature" else "face" self.selected_face_id = resolved_face_id self.selected_part_id = int(info["part_id"]) self.selected_solid_id = int(info["solid_id"]) if int(info["solid_id"]) >= 0 else None self.selected_pick_position = record.pick_position self.mode_combo.setCurrentText("Feature" if record.target_kind == "feature" else "Face") current_logical_id = self.model.face_region_logical_id(resolved_face_id) self._sync_id_picker("Feature" if record.target_kind == "feature" else "Face", current_logical_id) face_ids = ( _int_values(info.get("feature_highlight_face_ids")) if record.target_kind == "feature" else self.model.face_region_ids(resolved_face_id) ) if not face_ids: face_ids = self.model.face_region_ids(resolved_face_id) self._highlight_faces(face_ids=face_ids) located = True region_note = f";同域面区域 {len(face_ids)} 个Face" if len(face_ids) > 1 else "" id_note = ( f"逻辑 Face ID {current_logical_id}" if record.target_logical_id is not None else f"Face {record.target_id}" ) if resolved_face_id != record.target_id: id_note += f"(当前拓扑Face {resolved_face_id})" locator_note = ( f"定位: 已尝试高亮当前模型中的 {id_note}{region_note}。" "布尔编辑后Face ID可能发生语义变化,请结合拾取点确认。" ) else: if record.target_logical_id is not None: locator_note = f"定位: 原目标逻辑 Face ID {record.target_logical_id} 在当前模型索引中已经不存在。" else: locator_note = f"定位: 原目标Face {record.target_id} 在当前模型索引中已经不存在。" elif record.target_kind == "edge" and record.target_id is not None: if 0 <= record.target_id < len(self.model.edges): info = self.model.edge_info(record.target_id) self.selected_kind = "edge" self.selected_edge_id = record.target_id self.selected_part_id = int(info["part_id"]) self.selected_solid_id = int(info.get("solid_id", -1)) if int(info.get("solid_id", -1)) >= 0 else None self.selected_pick_position = record.pick_position self.mode_combo.setCurrentText("Edge") self._sync_id_picker("Edge", record.target_id) self._highlight_edge(record.target_id) located = True locator_note = ( f"定位: 已尝试高亮当前模型中的Edge {record.target_id}。" "布尔/倒圆编辑后Edge ID可能发生语义变化,请结合拾取点确认。" ) else: locator_note = f"定位: 原目标Edge {record.target_id} 在当前模型索引中已经不存在。" if record.pick_position is not None: self._show_pick_marker(record.pick_position) if not locator_note: locator_note = "定位: 已显示当时记录的拾取点。" elif located: locator_note += f"\n拾取点: {_format_value(record.pick_position)}" else: locator_note += f"\n已显示当时记录的拾取点: {_format_value(record.pick_position)}" if not locator_note: locator_note = "定位: 这条历史记录没有可定位的目标或拾取点。" self._update_action_states() self.statusBar().showMessage(locator_note.splitlines()[0]) return locator_note def undo_edit(self) -> None: if self.model is None: return if self._edit_busy("编辑计算中,暂时不能撤销。"): return if not self.undo_stack: self.statusBar().showMessage("没有可撤销的编辑") return current = self.model.snapshot() snapshot = self.undo_stack[-1] undone = self.operation_history[-1] if self.operation_history else OperationRecord("编辑", "编辑") try: self._restore_snapshot(snapshot) except Exception as exc: rollback_message = self._restore_after_failed_undo_redo(current) QMessageBox.critical(self, "撤销失败", f"{exc}\n\n{rollback_message}") self.statusBar().showMessage("撤销失败,模型已尽量恢复到撤销前状态") return self.undo_stack.pop() if self.operation_history: self.operation_history.pop() self.redo_stack.append(current) self.redo_history.append(undone) self._refresh_history_list() self._update_action_states() self.statusBar().showMessage(f"已撤销:{undone.summary}") def redo_edit(self) -> None: if self.model is None: return if self._edit_busy("编辑计算中,暂时不能重做。"): return if not self.redo_stack: self.statusBar().showMessage("没有可重做的编辑") return current = self.model.snapshot() snapshot = self.redo_stack[-1] redone = self.redo_history[-1] if self.redo_history else OperationRecord("编辑", "编辑") try: self._restore_snapshot(snapshot) except Exception as exc: rollback_message = self._restore_after_failed_undo_redo(current) QMessageBox.critical(self, "重做失败", f"{exc}\n\n{rollback_message}") self.statusBar().showMessage("重做失败,模型已尽量恢复到重做前状态") return self.redo_stack.pop() if self.redo_history: self.redo_history.pop() self.undo_stack.append(current) self.operation_history.append(redone) self._refresh_history_list() self._update_action_states() self.statusBar().showMessage(f"已重做:{redone.summary}") def _restore_snapshot(self, snapshot: dict[int, object]) -> None: if self.model is None: return 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() stats = self.model.stats() self.set_info( { "parts": stats.parts, "solids": stats.solids, "faces": stats.faces, "edges": stats.edges, "vertices": stats.vertices, } ) def _restore_after_failed_undo_redo(self, snapshot: dict[int, object]) -> str: try: self._restore_snapshot(snapshot) except Exception as rollback_exc: return f"恢复原状态也失败:{rollback_exc}。建议重新加载 STEP 文件。" return "模型已恢复到操作前状态,历史记录未移动。"