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, ScanWorker class InfoPanelMixin: def set_info(self, info: dict[str, object]) -> None: if hasattr(self, "_invalidate_selected_action_info_cache"): self._invalidate_selected_action_info_cache() self.current_info_values = dict(info) self.current_info_text = _info_to_text(info) self.info_text.setPlainText(self.current_info_text) diagnostic_content = getattr(self, "diagnostic_info_content", None) diagnostics_visible = bool( diagnostic_content is None or not hasattr(diagnostic_content, "isVisible") or diagnostic_content.isVisible() ) if diagnostics_visible: self._populate_info_tree(info) self.info_tabs.setCurrentWidget(self.info_tree) else: self.info_tree.clear() if hasattr(self, "_refresh_property_editor"): self._refresh_property_editor() if hasattr(self, "_update_selected_object_title"): self._update_selected_object_title() if hasattr(self, "mode_combo"): self._update_action_states() def set_plain_info(self, text: str) -> None: if hasattr(self, "_invalidate_selected_action_info_cache"): self._invalidate_selected_action_info_cache() self.current_info_values = {} self.current_info_text = text self.info_tree.clear() self.info_text.setPlainText(text) self.info_tabs.setCurrentWidget(self.info_text) if hasattr(self, "_clear_property_editor"): self._clear_property_editor() if hasattr(self, "mode_combo"): self._update_action_states() def _on_diagnostic_info_toggled(self, checked: bool) -> None: if hasattr(self, "diagnostic_info_content"): self.diagnostic_info_content.setVisible(bool(checked)) if checked and self.current_info_values: self._populate_info_tree(self.current_info_values) self.info_tabs.setCurrentWidget(self.info_tree) elif not checked and hasattr(self, "info_tree"): self.info_tree.clear() def _populate_info_tree(self, info: dict[str, object]) -> None: self.info_tree.clear() emitted: set[str] = set() for group_name, keys in INFO_GROUPS: items = [(key, info[key]) for key in keys if key in info] if not items: continue self._add_info_group(group_name, items) emitted.update(key for key, _value in items) remaining = [(key, value) for key, value in info.items() if key not in emitted] if remaining: self._add_info_group("其他", remaining) self.info_tree.expandAll() self.info_tree.resizeColumnToContents(0) def _add_info_group(self, group_name: str, items: list[tuple[str, object]]) -> None: group = QTreeWidgetItem([group_name, ""]) group.setFirstColumnSpanned(True) self.info_tree.addTopLevelItem(group) for key, value in items: child = QTreeWidgetItem([INFO_LABELS.get(key, key), _format_info_value(key, value)]) child.setData(0, Qt.UserRole, key) child.setToolTip(0, key) child.setToolTip(1, _format_value(value)) group.addChild(child) def copy_selected_id(self) -> None: text = self._selected_id_text() if not text: self.statusBar().showMessage("没有可复制的对象 ID") return QApplication.clipboard().setText(text) self.statusBar().showMessage(f"已复制 {text}") def copy_pick_position(self) -> None: pick_position = self.selected_pick_position if pick_position is None and "pick_position" in self.current_info_values: value = self.current_info_values["pick_position"] if isinstance(value, tuple) and len(value) == 3: pick_position = (float(value[0]), float(value[1]), float(value[2])) if pick_position is None: self.statusBar().showMessage("没有可复制的拾取坐标") return text = _format_value(pick_position) QApplication.clipboard().setText(text) self.statusBar().showMessage(f"已复制拾取坐标 {text}") def copy_current_info(self) -> None: if not self.current_info_text: self.statusBar().showMessage("没有可复制的信息") return QApplication.clipboard().setText(self.current_info_text) self.statusBar().showMessage("已复制当前信息") def set_measure_point(self, label: str) -> None: point, source = self._current_measure_point() if point is None: QMessageBox.information(self, "没有可测量的点", "请先在模型中选择一个对象,最好用鼠标点击到具体位置。") self.statusBar().showMessage("没有可设为测量点的坐标") return if label.upper() == "A": self.measure_point_a = point self.measure_label_a = source self.statusBar().showMessage(f"已设置测量点 A: {_format_value(point)}") else: self.measure_point_b = point self.measure_label_b = source self.statusBar().showMessage(f"已设置测量点 B: {_format_value(point)}") self._refresh_measurement_panel() self._update_action_states() def clear_measurement(self) -> None: self.measure_point_a = None self.measure_point_b = None self.measure_label_a = "" self.measure_label_b = "" self._clear_measure_actor() self._refresh_measurement_panel() self.statusBar().showMessage("已清除测量") self._update_action_states() def copy_measurement(self) -> None: text = self.measure_text.toPlainText() if hasattr(self, "measure_text") else "" if not text or "尚未设置" in text: self.statusBar().showMessage("没有可复制的测量结果") return QApplication.clipboard().setText(text) self.statusBar().showMessage("已复制测量结果") def _current_measure_point(self) -> tuple[tuple[float, float, float] | None, str]: selected_label = self._selected_id_text() or "当前对象" if self.selected_pick_position is not None: return self._tuple3(self.selected_pick_position), f"{selected_label} 拾取点" for key, readable in ( ("pick_position", "拾取点"), ("area_center", "面积中心"), ("length_center", "长度中心"), ("surface_center", "表面积中心"), ("center_of_mass", "重心"), ("center", "中心"), ("rotation_center", "旋转中心"), ): point = self._tuple3(self.current_info_values.get(key)) if point is not None: return point, f"{selected_label} {readable}" bbox_min = self._tuple3(self.current_info_values.get("bbox_min")) bbox_max = self._tuple3(self.current_info_values.get("bbox_max")) if bbox_min is not None and bbox_max is not None: center = ( (bbox_min[0] + bbox_max[0]) / 2.0, (bbox_min[1] + bbox_max[1]) / 2.0, (bbox_min[2] + bbox_max[2]) / 2.0, ) return center, f"{selected_label} 包围盒中心" return None, "" def _refresh_measurement_panel(self) -> None: if not hasattr(self, "measure_text"): return lines = ["两点测量"] if self.measure_point_a is None: lines.append("A: 尚未设置") else: lines.append(f"A: {_format_value(self.measure_point_a)}") if self.measure_label_a: lines.append(f" 来源: {self.measure_label_a}") if self.measure_point_b is None: lines.append("B: 尚未设置") else: lines.append(f"B: {_format_value(self.measure_point_b)}") if self.measure_label_b: lines.append(f" 来源: {self.measure_label_b}") if self.measure_point_a is not None and self.measure_point_b is not None: dx = self.measure_point_b[0] - self.measure_point_a[0] dy = self.measure_point_b[1] - self.measure_point_a[1] dz = self.measure_point_b[2] - self.measure_point_a[2] distance = math.sqrt(dx * dx + dy * dy + dz * dz) lines.extend( [ f"距离: {_format_value(distance)}", f"ΔX/ΔY/ΔZ: {_format_value((dx, dy, dz))}", ] ) else: lines.append("距离: 需要同时设置 A 和 B") self.measure_text.setPlainText("\n".join(lines)) self._update_measure_actor() def _update_measure_actor(self) -> None: self._clear_measure_actor(render=False) if self.measure_point_a is None or self.measure_point_b is None: return if not hasattr(self, "renderer") or not hasattr(self, "render_window"): return line = vtk.vtkLineSource() line.SetPoint1(*self.measure_point_a) line.SetPoint2(*self.measure_point_b) mapper = vtk.vtkPolyDataMapper() mapper.SetInputConnection(line.GetOutputPort()) actor = vtk.vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetColor(0.1, 0.95, 1.0) actor.GetProperty().SetLineWidth(4) actor.GetProperty().SetAmbient(0.9) actor.PickableOff() self.measure_actor = actor self.renderer.AddActor(actor) self.render_window.Render() def _clear_measure_actor(self, render: bool = True) -> None: actor = getattr(self, "measure_actor", None) if actor is not None and hasattr(self, "renderer"): try: self.renderer.RemoveActor(actor) except Exception: pass self.measure_actor = None if render and hasattr(self, "render_window"): self.render_window.Render() def _tuple3(self, value: object) -> tuple[float, float, float] | None: if isinstance(value, (tuple, list)) and len(value) == 3: try: return (float(value[0]), float(value[1]), float(value[2])) except (TypeError, ValueError): return None return None def _selected_id_text(self) -> str: if self.selected_kind == "part" and self.selected_part_id is not None: return f"part {self.selected_part_id}" if self.selected_kind == "solid" and self.selected_solid_id is not None: return f"solid {self.selected_solid_id}" if self.selected_kind in {"face", "feature"} and self.selected_face_id is not None: for key in ("selection_display_id", "face_region_logical_id", "logical_face_id"): value = self.current_info_values.get(key) if value in {None, ""}: continue try: return f"face {int(value)}" except (TypeError, ValueError): continue return f"face {self.selected_face_id}" if self.selected_kind == "edge" and self.selected_edge_id is not None: return f"edge {self.selected_edge_id}" for kind, key in (("face", "face_id"), ("edge", "edge_id"), ("solid", "solid_id"), ("part", "part_id")): if key in self.current_info_values: return f"{kind} {self.current_info_values[key]}" return ""