Files
Dispatch/Dispatch_V0.1.1/gui/components/model_view_widget.py
2026-04-29 08:18:54 +04:00

273 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# gui/components/model_view_widget.py
"""Виджет отображения и управления 3D-моделью с поддержкой зон.
Данный файл использует фасад composition-слоя `gui.components.model_view.contour`
и миксины с функционалом, которые внедрены в `ModelViewWidget`.
"""
from typing import Optional, Tuple, Callable
from gui.containers import VContainer, SContainer
from PySide6.QtCore import Signal
from gui.theme_bus import theme_bus
from error_logger import log_exception
from gui.components.model_view._interaction_scenario import InteractionManager
from gui.components.model_view._scenario_camera import CameraScenario
from gui.components.model_view._scenario_facility_browse import FacilityBrowseScenario
from gui.components.model_view.contour import (
ModelViewContourFacade,
LegacyContourBindingComponent,
)
from gui.components.model_view import (
ModelLoadingMixin,
ZoneManagementMixin,
InteractionMixin,
VisualHelpersMixin,
GridCoreMixin,
DimensionLinesMixin,
RackPlacementMixin,
ScenePresentationMixin,
SceneModesMixin,
RackCameraTransitionMixin,
ZoneCameraTransitionMixin,
RackPlacementIOMixin,
)
class ModelViewWidget(
InteractionMixin,
ModelLoadingMixin,
ZoneManagementMixin,
VisualHelpersMixin,
GridCoreMixin,
DimensionLinesMixin,
RackPlacementMixin,
ScenePresentationMixin,
SceneModesMixin,
RackCameraTransitionMixin,
ZoneCameraTransitionMixin,
RackPlacementIOMixin,
SContainer,
):
"""Виджет для отображения 3D моделей помещения и зон."""
# Сигналы
error_occurred = Signal(str)
zone_selected = Signal(str) # ид. зоны
zone_double_clicked = Signal(str) # двойной ЛКМ по зоне
camera_mode_changed = Signal(bool) # True = заблокирована
grid_volume_changed = Signal(float, float, float) # ширина, глубина, высота
contour_ready_changed = Signal(bool) # контур ортогонален и пригоден для объёма
rack_layout_changed = Signal(str) # zone_id
rack_slot_visibility_changed = Signal(str, str, bool) # rack_id, slot_id, occupied
rack_selected_changed = Signal(str) # ид. стеллажа или ""
rack_double_clicked = Signal(str, str) # rack_id, zone_id
shelf_slot_selected = Signal(str, str) # rack_id, slot_id
shelf_slot_double_clicked = Signal(str, str, int) # rack_id, slot_id, shelf_index
def __init__(
self,
width_percent: int | None = None,
height_percent: int | None = None,
margin: int | tuple[int, int, int, int] = 0,
content_fit: bool = True,
parent=None,
style: str | None = None,
active_style: str | None = None,
is_active: bool | None = None,
):
super().__init__(
width_percent=width_percent,
height_percent=height_percent,
content_fit=content_fit,
parent=parent,
style=style,
active_style=active_style,
is_active=is_active,
)
self._content_margin = margin
self._content_fit = content_fit
self._plotter = None
self._models_loaded = False
self._camera_locked = False
self._theme = "dark"
# Данные моделей
self._floor_mesh = None
self._walls_mesh = None
self._ceiling_mesh = None
self._truss_mesh = None
self._rack_meshes: list = []
self._zones: dict = {} # ид. зоны -> mesh
self._zone_data: dict = {} # zone_id -> (min_x, max_x, min_y, max_y, min_z, max_z)
self._zone_polygons: dict = {} # ид. зоны -> list[(x, y)]
self._zone_heights: dict = {} # zone_id -> (start_z, height)
self._model_actors: dict = {} # ключ -> actor
self._missing_models: set = set()
# Интерактивное размещение
self._preview_zone = None
self._current_zone_size = 100 # мм
self._current_zone_z_size = 100 # мм, шаг по высоте Z
self._current_zone_color = "#FF6B6B80"
# Ограничивающий параллелепипед
self._room_bounds = None # (min_x, max_x, min_y, max_y, min_z, max_z)
# Режим выбора зон с сеткой
self._zone_selection_mode = False
self._zone_pick_enabled = True
self._selected_zone_highlight_id: Optional[str] = None
self._selected_zone_original_opacity: Optional[float] = None
self._selected_zone_original_visibility: Optional[bool] = None
self._hover_highlighted_zone_id: Optional[str] = None
self._hover_zone_original_opacity: Optional[float] = None
self._hover_zone_original_edges: bool = False
self._hover_zone_original_visibility: Optional[bool] = None
self._hover_zone_contour_original_color: Optional[tuple] = None
self._grid_meshes: list = []
self._selected_cells: set = set()
self._grid_cells: dict = {}
self._grid_origin = (0, 0, 0)
self._selected_height = 0
self._selection_anchor = None # (min_x, min_y)
self._contour_zone_overlay_enabled = False
self._contour_zone_mode = None
self._contour_zone_id = None
self._contour_ignore_zone_id = None
self._contour_ready = False
self._grid_plane_z_override = None
# Кэш сетки — повторное использование сетки без пересоздания
self._grid_ready = False
self._grid_cached_cell_size = 0
self._grid_cached_z_size = 0
# Атрибуты контура/сетки — инициализация для стабильности
self._grid_nodes: list = []
self._grid_surface_meshes: list = []
self._grid_surface_nodes: list[tuple[float, float]] = []
self._contour_points: list = []
self._final_contour_points: list = []
self._contour_points_actor = None
self._contour_lines_actor = None
self._contour_drag_active = False
self._contour_drag_point_index: Optional[int] = None
self._contour_drag_moved = False
self._selection_start_cell = None
self._volume_locked_from_contour = False
self._last_volume_start_height = 0.0
# Перетаскивание
self._dragging = False
self._drag_start_y = 0
self._cam_rotate_active = False
self._cam_pan_active = False
self._cam_last_pos: Optional[Tuple[float, float]] = None
self._last_safe_render_ts = 0.0
self._custom_click_handler: Optional[Callable[[float, float, float], bool]] = None
self._hover_handler: Optional[Callable[[float, float, float], None]] = None
self._hover_screen_handler: Optional[Callable[[float, float], None]] = None
self._origin_marker = None
self._origin_preview_marker = None
self._quadrant_actors: list = []
self._quadrant_label_actors: list = []
self._axes_actors: list = []
self._last_hover_point: Optional[Tuple[float, float, float]] = None
self._corner_points: list[tuple[float, float, float]] = []
self._measure_ref_point: tuple[float, float, float] = (0.0, 0.0, 0.0)
self._measure_direction: tuple[float, float] = (1.0, 1.0)
self._ignore_next_plotter_click = False
# Размерные линии
self.init_dimension_lines()
self.init_rack_placement()
# Менеджер сценариев взаимодействия
self._interaction_manager = InteractionManager(self)
self._interaction_manager.set_camera_scenario(CameraScenario())
self._interaction_manager.push(FacilityBrowseScenario())
self._legacy_contour_binding = LegacyContourBindingComponent(self)
self._legacy_contour_binding.install()
self.contour_facade = ModelViewContourFacade(self)
self.setup_ui()
theme_bus.theme_changed.connect(self.set_theme)
# -- Интерфейс -------------------------------------------------------------------
def setup_ui(self):
"""Настройка интерфейса."""
self._main_container = VContainer(
margin=self._content_margin,
content_fit=self._content_fit,
parent=self,
)
# -- простые сеттеры / свойства ------------------------------------------
def show_error(self, message):
"""Показать сообщение об ошибке."""
self.error_occurred.emit(message)
def update_scene(self) -> None:
"""Перерисовать 3D-сцену."""
if self._plotter:
self._plotter.update()
def set_ignore_next_click(self, ignore: bool) -> None:
"""Пропустить следующий клик по plotter."""
self._ignore_next_plotter_click = bool(ignore)
def is_camera_locked(self) -> bool:
"""Заблокирована ли камера."""
return bool(self._camera_locked)
def is_scenario_active(self, name: str) -> bool:
"""Проверить, активен ли сценарий взаимодействия по имени."""
mgr = getattr(self, "_interaction_manager", None)
if mgr is not None:
return mgr.is_active(name)
return False
def pop_interaction_scenario(self, name: str) -> None:
"""Снять сценарий взаимодействия по имени."""
if self._interaction_manager is not None:
self._interaction_manager.pop_by_name(name)
def set_theme(self, theme: str) -> None:
theme = (theme or "").strip().lower()
if theme not in ("dark", "light"):
return
if self._theme == theme:
return
self._theme = theme
if hasattr(self, "_apply_grid_theme"):
try:
self._apply_grid_theme()
except Exception as e:
log_exception(__name__, "set_theme", e)
def set_zone_pick_enabled(self, enabled: bool) -> None:
"""Разрешить выбор существующих зон по клику."""
self._zone_pick_enabled = bool(enabled)
if not enabled:
self.clear_selected_zone_highlight()
self._unhighlight_hover_zone()
def set_current_zone_color(self, color: str) -> None:
"""Установить цвет предпросмотра зоны (#RRGGBBAA)."""
self._current_zone_color = color
@property
def room_bounds(self):
return self._room_bounds
@property
def zone_selection_mode(self) -> bool:
"""Возвращает режим выбора зоны в 3D сцене."""
return self._zone_selection_mode