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,272 @@
# -*- 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