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

253 lines
10 KiB
Python

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:
self.current_info_values = dict(info)
self.current_info_text = _info_to_text(info)
self.info_text.setPlainText(self.current_info_text)
self._populate_info_tree(info)
self.info_tabs.setCurrentWidget(self.info_tree)
if hasattr(self, "_refresh_property_editor"):
self._refresh_property_editor()
if hasattr(self, "mode_combo"):
self._update_action_states()
def set_plain_info(self, text: str) -> None:
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 _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_value(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:
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 ""