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_model = ( self.model is not None and not self.operation_in_progress and not self.scan_in_progress and not self.load_in_progress ) selected_kind = self.selected_kind self.export_all_button.setEnabled(has_model) self.export_check_button.setEnabled(has_model) if hasattr(self, "repair_model_button"): self._set_control_state( self.repair_model_button, has_model, "对当前完整模型执行 ShapeFix 和同域面/边合并,并写入撤销历史。", "请先加载 STEP 文件,或等待当前后台任务完成。", ) self._set_control_state( self.repair_selected_button, has_model and (self.selected_solid_id is not None or self.selected_part_id is not None), "优先修复当前选中对象所属 solid;没有 solid 时修复所属零件,并写入撤销历史。", "请先选择 part、solid、face 或 edge,或等待当前后台任务完成。", ) self.export_part_button.setEnabled(has_model and selected_kind == "part" and self.selected_part_id is not None) self.export_solid_button.setEnabled(has_model and selected_kind == "solid" and self.selected_solid_id is not None) self.export_face_button.setEnabled(has_model and selected_kind == "face" and self.selected_face_id is not None) self.export_feature_button.setEnabled(has_model and selected_kind == "feature" and self.selected_face_id is not None) self.export_edge_button.setEnabled(has_model and selected_kind == "edge" and self.selected_edge_id is not None) 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_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" self._set_control_state( self.push_button, has_model and is_plane, "对当前平面 face 执行推拉。", "请先选择一个平面 face,或在 Feature 模式下选择可推拉平面候选。", ) self._set_control_state( self.resize_button, has_model and is_hole_or_groove, "调整当前圆柱孔/槽候选的直径。", "请先选择被识别为孔/槽候选的圆柱 face。", ) if hasattr(self, "resize_slot_button"): self._set_control_state( self.resize_slot_button, has_model and is_slot_or_half_hole, "按目标槽宽调整当前槽/半孔候选;第一版会换算为对应圆柱直径后执行。", "请先选择被识别为槽/半孔的部分圆柱 face。", ) self._set_control_state( self.resize_boss_button, has_model and is_boss and is_full_cylinder, "调整当前完整圆柱凸台候选的直径。", "请先选择被识别为完整凸台/外圆候选的圆柱 face。", ) self._set_control_state( self.suppress_button, has_model and is_hole_or_groove and is_full_cylinder, "封堵当前完整圆柱孔候选。", "请先选择接近完整圆柱的孔候选;半孔/槽不会放行。", ) self._set_control_state( self.resize_depth_button, has_model and is_hole_or_groove and ((is_blind and has_bottom) or has_manual_bottom), "调整当前盲孔/盲槽深度;自动底面不稳定时可手动填写底面 Face ID。", "请先选择孔/槽圆柱面;如果没有自动识别到底面,请填写底面 Face ID。", ) self._set_control_state( self.fillet_edge_button, has_model and is_line_edge, "给当前直线 edge 添加新圆角。", "请先选择一条直线 edge。", ) self._set_control_state( self.resize_existing_fillet_button, has_model and is_existing_fillet and has_fillet_support, "尝试修改当前已有圆角/倒圆候选的半径。", "请先选择一个已有圆角/倒圆候选 face;第一版需要识别到至少两个支撑 face。", ) self._set_control_state( self.chamfer_edge_button, has_model and is_line_edge, "给当前直线 edge 添加倒角。", "请先选择一条直线 edge。", ) self._set_control_state( self.resize_edge_length_button, has_model and has_edge, "直接修改当前 edge 的长度;直线优先端面推拉,其他 edge 使用高风险几何 fallback。", "请先选择一条 edge。", ) self._set_control_state( self.translate_part_button, has_model and self.selected_part_id is not None, "平移当前选中对象所属零件。", "请先选择一个零件,或选择属于某个零件的对象。", ) self._set_control_state( self.rotate_part_button, has_model and self.selected_part_id is not None, "旋转当前选中对象所属零件。", "请先选择一个零件,或选择属于某个零件的对象。", ) self._set_control_state( self.translate_solid_button, has_model and self.selected_solid_id is not None, "平移当前选中对象所属 solid。", "请先选择一个 solid,或选择属于某个 solid 的 face/edge。", ) self._set_control_state( self.rotate_solid_button, has_model and self.selected_solid_id is not None, "旋转当前选中对象所属 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.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") == "cylinder": 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 "模型已恢复到操作前状态,历史记录未移动。"