Add Dispatch_V0.1.1

This commit is contained in:
2026-04-29 08:18:54 +04:00
commit a7ede6ded4
404 changed files with 39167 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""Устаревший пакет для простого просмотра моделей."""
from .model_view_widget import ModelViewWidget
__all__ = ["ModelViewWidget"]
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
#
# 1) Назначение модуля:
# Пакетный __init__: реэкспорт SimpleModelViewWidget как ModelViewWidget
# для автономного 3D-просмотра моделей (используется PartVisualizer).
#
# 2) Зависимости модуля:
# Импортирует ModelViewWidget из .model_view_widget.
#
# 3) Экспорт:
# __all__ = ["ModelViewWidget"] — единственный публичный символ.

View File

@@ -0,0 +1,384 @@
# -*- coding: utf-8 -*-
# gui/components/simple_model_viewer/model_view_widget.py
"""Автономный простой 3D-просмотрщик моделей, используемый PartVisualizer."""
import math
from pathlib import Path
from PySide6.QtCore import Qt, Signal, QEvent
from PySide6.QtWidgets import QWidget
from gui.containers import VContainer
from gui.styles import APP_STYLES
from error_logger import log_exception
try:
import pyvista as pv
from pyvistaqt import QtInteractor
PYVISTA_AVAILABLE = True
except ImportError:
PYVISTA_AVAILABLE = False
print("Warning: PyVista is not installed. 3D preview is unavailable.")
class ModelViewWidget(QWidget):
"""Компактный виджет предпросмотра STL с ограниченным управлением мышью."""
error_occurred = Signal(str)
def __init__(self, *_, **__):
super().__init__()
self._plotter = None
self._model_loaded = False
self._cam_rotate_active = False
self._cam_pan_active = False
self._cam_last_pos: tuple[float, float] | None = None
self._setup_ui()
def _setup_ui(self) -> None:
main_container = VContainer()
main_container.layout().setAlignment(Qt.AlignmentFlag.AlignCenter)
self._model_layout = main_container.layout()
self.setLayout(main_container.layout())
self.setStyleSheet(APP_STYLES.get("VIEW_WIDGET_BACKGROUND", ""))
def load_model(self, model_path: str | Path) -> None:
if not PYVISTA_AVAILABLE:
self._show_error("PyVista is not installed")
return
path = Path(model_path)
if not path.exists():
self._show_error(f"Model file not found: {path}")
return
self.clear_model()
try:
mesh = pv.read(path)
self._plotter = QtInteractor(self)
self._plotter.set_background((240 / 255, 240 / 255, 240 / 255))
self._plotter.add_mesh(mesh, color="lightblue", show_edges=True)
self._plotter.camera_position = "xy"
self._plotter.reset_camera()
self._configure_mouse_controls()
self._model_layout.addWidget(self._plotter)
self._model_loaded = True
except Exception as exc:
self._show_error(f"Model load error: {exc}")
def clear_model(self) -> None:
if self._plotter is not None:
try:
self._plotter.close()
except Exception as e:
log_exception(__name__, "clear_model.close_plotter", e)
self._plotter = None
while self._model_layout.count():
item = self._model_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.setParent(None)
widget.deleteLater()
self._model_loaded = False
def set_isometric_view(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.view_isometric()
self._plotter.update()
def set_top_view(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.view_xy()
self._plotter.update()
def set_front_view(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.view_xz()
self._plotter.update()
def set_left_view(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.view_zy()
self._plotter.update()
def set_right_view(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.view_yz()
self._plotter.update()
def reset_view(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.reset_camera()
self._plotter.camera_position = "xy"
self._plotter.update()
def zoom_in(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.camera.zoom(1.2)
self._plotter.update()
def zoom_out(self) -> None:
if not self._model_loaded or not self._plotter:
return
self._plotter.camera.zoom(0.8)
self._plotter.update()
def wheelEvent(self, event) -> None:
if self._model_loaded and self._plotter:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_in()
else:
self.zoom_out()
event.accept()
def resizeEvent(self, event) -> None:
super().resizeEvent(event)
if self._model_loaded and self._plotter:
self._plotter.update()
def _show_error(self, message: str) -> None:
self.clear_model()
self.error_occurred.emit(message)
def _configure_mouse_controls(self) -> None:
"""ПКМ — вращение, колесо — масштаб, СКМ — панорамирование, ЛКМ — отключена."""
if not self._plotter:
return
try:
self._plotter.enable_trackball_style()
except Exception as e:
log_exception(__name__, "_configure_mouse_controls.enable_trackball", e)
self._setup_trackball_right_button()
try:
interactor = getattr(self._plotter, "interactor", None)
if interactor is not None:
interactor.installEventFilter(self)
except Exception as e:
log_exception(__name__, "_configure_mouse_controls.install_event_filter", e)
def _setup_trackball_right_button(self) -> None:
"""Настроить trackball: ЛКМ drag выключен, ПКМ/СКМ разрешены."""
if not self._plotter:
return
try:
interactor = getattr(self._plotter, "interactor", None)
style = (
interactor.GetInteractorStyle()
if interactor and hasattr(interactor, "GetInteractorStyle")
else None
)
if style is not None:
if hasattr(style, "SetLeftButtonMotion"):
style.SetLeftButtonMotion(False)
if hasattr(style, "SetRightButtonMotion"):
style.SetRightButtonMotion(True)
if hasattr(style, "SetMiddleButtonMotion"):
style.SetMiddleButtonMotion(True)
except Exception as e:
log_exception(__name__, "_setup_trackball_right_button.configure_style", e)
def _pan_camera_by_pixels(self, dx: float, dy: float) -> None:
"""Сместить камеру в плоскости экрана на величину в пикселях."""
if not self._plotter or not getattr(self._plotter, "camera", None):
return
try:
cam = self._plotter.camera
pos = cam.GetPosition()
foc = cam.GetFocalPoint()
up = cam.GetViewUp()
vx = foc[0] - pos[0]
vy = foc[1] - pos[1]
vz = foc[2] - pos[2]
dist = math.sqrt(vx * vx + vy * vy + vz * vz) or 1.0
rx = vy * up[2] - vz * up[1]
ry = vz * up[0] - vx * up[2]
rz = vx * up[1] - vy * up[0]
rlen = math.sqrt(rx * rx + ry * ry + rz * rz) or 1.0
rx, ry, rz = rx / rlen, ry / rlen, rz / rlen
ulen = math.sqrt(up[0] * up[0] + up[1] * up[1] + up[2] * up[2]) or 1.0
ux, uy, uz = up[0] / ulen, up[1] / ulen, up[2] / ulen
scale = dist / 700.0
tx = (-dx * rx + dy * ux) * scale
ty = (-dx * ry + dy * uy) * scale
tz = (-dx * rz + dy * uz) * scale
cam.SetPosition(pos[0] + tx, pos[1] + ty, pos[2] + tz)
cam.SetFocalPoint(foc[0] + tx, foc[1] + ty, foc[2] + tz)
except Exception as e:
log_exception(__name__, "_pan_camera_by_pixels.move_camera", e)
return
def eventFilter(self, watched, event):
interactor = getattr(self._plotter, "interactor", None) if self._plotter else None
if interactor is not None and watched is interactor:
if event.type() == QEvent.Type.MouseButtonPress:
if event.button() == Qt.MouseButton.RightButton:
self._cam_rotate_active = True
self._cam_last_pos = (event.position().x(), event.position().y())
return True
if event.button() == Qt.MouseButton.MiddleButton:
self._cam_pan_active = True
self._cam_last_pos = (event.position().x(), event.position().y())
return True
if event.button() == Qt.MouseButton.LeftButton:
return True
if event.type() == QEvent.Type.MouseMove:
if hasattr(event, "buttons") and (event.buttons() & Qt.MouseButton.LeftButton):
return True
if (
self._cam_rotate_active
and hasattr(event, "buttons")
and (event.buttons() & Qt.MouseButton.RightButton)
):
cx, cy = self._cam_last_pos or (event.position().x(), event.position().y())
nx, ny = event.position().x(), event.position().y()
dx = float(nx - cx)
dy = float(ny - cy)
self._cam_last_pos = (nx, ny)
try:
cam = self._plotter.camera
cam.Azimuth(-dx * 0.35)
cam.Elevation(dy * 0.35)
cam.OrthogonalizeViewUp()
self._plotter.update()
except Exception as e:
log_exception(__name__, "eventFilter.rotate_camera", e)
return True
if (
self._cam_pan_active
and hasattr(event, "buttons")
and (event.buttons() & Qt.MouseButton.MiddleButton)
):
cx, cy = self._cam_last_pos or (event.position().x(), event.position().y())
nx, ny = event.position().x(), event.position().y()
dx = float(nx - cx)
dy = float(ny - cy)
self._cam_last_pos = (nx, ny)
self._pan_camera_by_pixels(dx, dy)
try:
self._plotter.update()
except Exception as e:
log_exception(__name__, "eventFilter.pan_update", e)
return True
if event.type() == QEvent.Type.MouseButtonRelease:
if event.button() == Qt.MouseButton.RightButton:
self._cam_rotate_active = False
self._cam_last_pos = None
return True
if event.button() == Qt.MouseButton.MiddleButton:
self._cam_pan_active = False
self._cam_last_pos = None
return True
if event.button() == Qt.MouseButton.LeftButton:
return True
return super().eventFilter(watched, event)
__all__ = ["ModelViewWidget"]
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
#
# 1) Назначение модуля:
# Автономный компактный 3D-просмотрщик STL-моделей, используемый
# PartVisualizer. Управление мышью ограничено: ПКМ — вращение,
# колёсико — зум, СКМ — панорамирование, ЛКМ отключён.
#
# 2) Зависимости модуля:
# Импорты: math, Path (pathlib),
# Qt, Signal, QEvent (PySide6.QtCore),
# QWidget (PySide6.QtWidgets),
# VContainer (gui.containers),
# APP_STYLES (gui.styles)
# Хост-класс / базовый класс: QWidget (не SContainer!)
# Внешние библиотеки: pyvista (опционально), pyvistaqt (опционально).
# При отсутствии — PYVISTA_AVAILABLE=False, 3D недоступен; вместо
# ошибки выводится warning.
#
# 3) Экспорт:
# Класс ModelViewWidget — компактный 3D-вьюер.
# Сигнал: error_occurred(str).
# Методы: load_model(path), clear_model(), set_isometric_view(),
# set_top/front/left/right_view(), reset_view(),
# zoom_in(), zoom_out().
# __all__ = ["ModelViewWidget"].
#
# 4) Состояние (поля):
# _plotter: QtInteractor|None — pyvista 3D-рендерер
# _model_loaded: bool — загружена ли модель
# _cam_rotate_active: bool — активно ли вращение ПКМ
# _cam_pan_active: bool — активно ли панорамирование СКМ
# _cam_last_pos: tuple|None — последняя позиция мыши
# _model_layout: QVBoxLayout — layout для plotter-виджета
#
# 5) Последовательность действий и вызовов:
# __init__()
# -> QWidget.__init__() -> _setup_ui()
# -> VContainer() -> setLayout -> setStyleSheet
# load_model(model_path)
# -> проверка PYVISTA_AVAILABLE и Path.exists()
# -> clear_model()
# -> pv.read(path) -> QtInteractor -> add_mesh -> camera_position
# -> _configure_mouse_controls()
# -> enable_trackball_style()
# -> _setup_trackball_right_button() — отключение ЛКМ drag
# -> interactor.installEventFilter(self)
# eventFilter(watched, event)
# -> RightButton: _cam_rotate_active → Azimuth/Elevation
# -> MiddleButton: _cam_pan_active → _pan_camera_by_pixels()
# -> LeftButton: return True (блокировка)
# _pan_camera_by_pixels(dx, dy)
# -> вычисление right/up-вектора камеры
# -> camera.SetPosition/SetFocalPoint — сдвиг в экранной плоскости
#
# 6) Побочные эффекты:
# - Создаёт QtInteractor (тяжёлый OpenGL-контекст).
# - Читает STL-файлы с диска.
# - Перехватывает mouse events через eventFilter.
# - Испускает error_occurred.
#
# 7) Границы ответственности:
# Модуль — только предпросмотр. НЕ управляет зонами, сетками,
# стеллажами. НЕ подключается к theme_bus. НЕ поддерживает
# multiple mesh. НЕ наследует SContainer.
#
# 8) Обработка ошибок:
# PYVISTA_AVAILABLE=False → _show_error при попытке загрузки.
# load_model() try/except → _show_error → error_occurred.
# clear_model() try/except при закрытии plotter.
# eventFilter — все операции камеры обёрнуты в try/except.
#
# 9) Инварианты и контракты:
# - _plotter может быть None до load_model().
# - _model_loaded == True только если plotter успешно создан и mesh добавлен.
# - ЛКМ всегда заблокирован (eventFilter возвращает True).
# - Зум-коэффициенты: in=1.2, out=0.8.
#
# 10) Правило сопровождения:
# При обновлении PyVista API — проверить enable_trackball_style(),
# SetLeftButtonMotion, GetInteractorStyle. eventFilter — главная
# точка кастомизации мышиного управления. Не наследовать от
# SContainer — виджет намеренно легковесный (QWidget).