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,625 @@
# -*- coding: utf-8 -*-
# gui/components/_mv/_mv_visual.py
# Маркеры, квадранты, оси, координатные преобразования
from __future__ import annotations
import math
from typing import Optional, Tuple, TYPE_CHECKING
from error_logger import log_exception
if TYPE_CHECKING: # pragma: no cover
from gui.components.model_view_widget import ModelViewWidget
try:
import pyvista as pv
_PV = True
except ImportError:
_PV = False
class VisualHelpersMixin:
"""Маркер начала координат, квадранты, оси, world↔display преобразования."""
# -- маркер начала координат ----------------------------------------------
_ORIGIN_MARKER_NAME = "__origin_marker"
_ORIGIN_PREVIEW_MARKER_NAME = "__origin_preview_marker"
def show_origin_marker(self: "ModelViewWidget", x: float, y: float, z: float) -> None:
if not self._plotter:
return
try:
self._plotter.remove_actor(self._ORIGIN_MARKER_NAME)
except Exception as _exc:
log_exception(__name__, "show_origin_marker", _exc)
try:
sphere = pv.Sphere(radius=80, center=(x, y, z))
self._origin_marker = self._plotter.add_mesh(
sphere, color=(1, 1, 0), opacity=0.8, name=self._ORIGIN_MARKER_NAME,
)
self._plotter.update()
except Exception as _exc:
log_exception(__name__, "show_origin_marker", _exc)
def show_origin_preview_marker(self: "ModelViewWidget", x: float, y: float, z: float) -> None:
"""Показать динамический маркер origin, следующий за курсором."""
if not self._plotter:
return
try:
self._plotter.remove_actor(self._ORIGIN_PREVIEW_MARKER_NAME)
except Exception as _exc:
log_exception(__name__, "show_origin_preview_marker", _exc)
try:
sphere = pv.Sphere(radius=55, center=(x, y, z))
self._origin_preview_marker = self._plotter.add_mesh(
sphere,
color=(0.2, 0.9, 1.0),
opacity=0.7,
name=self._ORIGIN_PREVIEW_MARKER_NAME,
)
self._plotter.update()
except Exception as _exc:
log_exception(__name__, "show_origin_preview_marker", _exc)
def clear_origin_marker(self: "ModelViewWidget") -> None:
if not self._plotter:
return
try:
self._plotter.remove_actor(self._ORIGIN_MARKER_NAME)
except Exception as _exc:
log_exception(__name__, "clear_origin_marker", _exc)
self._origin_marker = None
try:
if hasattr(self, "_safe_render"):
self._safe_render(min_interval_s=0.05)
else:
self._plotter.render()
except Exception as _exc:
log_exception(__name__, "clear_origin_marker", _exc)
def clear_origin_preview_marker(self: "ModelViewWidget") -> None:
if not self._plotter:
return
try:
self._plotter.remove_actor(self._ORIGIN_PREVIEW_MARKER_NAME)
except Exception as _exc:
log_exception(__name__, "clear_origin_preview_marker", _exc)
self._origin_preview_marker = None
try:
if hasattr(self, "_safe_render"):
self._safe_render(min_interval_s=0.05)
else:
self._plotter.render()
except Exception as _exc:
log_exception(__name__, "clear_origin_preview_marker", _exc)
def clear_origin_markers(self: "ModelViewWidget") -> None:
"""Очистить фиксированный и динамический маркеры origin."""
self.clear_origin_preview_marker()
self.clear_origin_marker()
# -- угловые точки --------------------------------------------------------
def set_corner_points(self: "ModelViewWidget", points: list[tuple[float, float, float]]) -> None:
self._corner_points = list(points)
def get_corner_points(self: "ModelViewWidget") -> list[tuple[float, float, float]]:
return list(self._corner_points)
# -- квадранты -------------------------------------------------------------
def show_quadrants(self: "ModelViewWidget", origin: Tuple[float, float, float], size: float = 6000.0) -> None:
if not self._plotter:
return
self.clear_quadrants()
x0, y0, z0 = origin
half = size / 2.0
# Фиксированная высота квадранта: 100 мм.
z_min = z0
z_max = z0 + 100.0
quadrant_bounds = [
((x0, x0 + half), (y0, y0 + half)), # 0: ++
((x0 - half, x0), (y0, y0 + half)), # 1: -+
((x0 - half, x0), (y0 - half, y0)), # 2: --
((x0, x0 + half), (y0 - half, y0)), # 3: +-
]
colors = [(0.6, 0.6, 0.6)] * 4
for idx, (x_range, y_range) in enumerate(quadrant_bounds):
x_min, x_max = x_range
y_min, y_max = y_range
box = pv.Box(bounds=(x_min, x_max, y_min, y_max, z_min, z_max))
name = f"__quadrant_plane_{idx}"
actor = self._plotter.add_mesh(
box,
color=colors[idx],
opacity=0.12,
show_edges=True,
edge_color=(0.7, 0.7, 0.7),
line_width=1.0,
name=name,
)
try:
actor.PickableOn()
except Exception as _exc:
log_exception(__name__, "show_quadrants", _exc)
self._quadrant_actors.append(actor)
for idx, (x_range, y_range) in enumerate(quadrant_bounds):
x_min, x_max = x_range
y_min, y_max = y_range
center_x = (x_min + x_max) / 2.0
center_y = (y_min + y_max) / 2.0
label_pos = [center_x, center_y, z0 + 100]
name = f"__quadrant_label_{idx}"
label_actor = self._plotter.add_point_labels(
[label_pos],
[f"Q{idx}"],
font_size=72,
text_color="yellow",
point_color="yellow",
point_size=30,
render_points_as_spheres=True,
always_visible=True,
shape_opacity=0.9,
name=name,
)
try:
if label_actor is not None:
if hasattr(label_actor, "PickableOff"):
label_actor.PickableOff()
elif hasattr(label_actor, "SetPickable"):
label_actor.SetPickable(False)
except Exception as _exc:
log_exception(__name__, "show_quadrants", _exc)
self._quadrant_label_actors.append(name)
self._plotter.update()
def animate_focus_on_quadrants(
self: "ModelViewWidget",
*,
duration_ms: int = 420,
margin_factor: float = 1.2,
) -> bool:
"""Плавно сфокусировать камеру так, чтобы текущие квадранты целиком были в кадре."""
if not self._plotter:
return False
cam = getattr(self._plotter, "camera", None)
if cam is None:
return False
actors = list(getattr(self, "_quadrant_actors", []) or [])
if not actors:
return False
try:
min_x = float("inf")
max_x = float("-inf")
min_y = float("inf")
max_y = float("-inf")
min_z = float("inf")
max_z = float("-inf")
for actor in actors:
if actor is None or not hasattr(actor, "GetBounds"):
continue
b = actor.GetBounds()
if not b or len(b) < 6:
continue
min_x = min(min_x, float(b[0]))
max_x = max(max_x, float(b[1]))
min_y = min(min_y, float(b[2]))
max_y = max(max_y, float(b[3]))
min_z = min(min_z, float(b[4]))
max_z = max(max_z, float(b[5]))
if not all(math.isfinite(v) for v in (min_x, max_x, min_y, max_y, min_z, max_z)):
return False
cx = (min_x + max_x) * 0.5
cy = (min_y + max_y) * 0.5
sx = max(1.0, max_x - min_x)
sy = max(1.0, max_y - min_y)
sz = max(1.0, max_z - min_z)
span = max(sx, sy, sz) * max(1.0, float(margin_factor))
cz = min_z + (sz * 0.55)
start_pos = tuple(float(v) for v in cam.GetPosition())
start_focal = tuple(float(v) for v in cam.GetFocalPoint())
start_up = tuple(float(v) for v in cam.GetViewUp())
vx, vy, vz = 1.0, -1.0, 0.72
norm = (vx * vx + vy * vy + vz * vz) ** 0.5
vx, vy, vz = vx / norm, vy / norm, vz / norm
ux, uy, uz = 0.0, 0.0, 1.0
distance = max(3000.0, span * 2.35)
target_focal = (cx, cy, cz)
target_pos = (
cx + vx * distance,
cy + vy * distance,
cz + vz * distance + (sz * 0.08),
)
if hasattr(self, "_animate_camera_transition"):
self._animate_camera_transition(
start_pos=start_pos,
start_focal=start_focal,
start_up=start_up,
target_pos=target_pos,
target_focal=target_focal,
target_up=(ux, uy, uz),
duration_ms=int(duration_ms),
steps=18,
)
else:
cam.SetPosition(*target_pos)
cam.SetFocalPoint(*target_focal)
if hasattr(cam, "SetViewUp"):
cam.SetViewUp(ux, uy, uz)
if hasattr(self, "_reset_camera_clipping_range"):
self._reset_camera_clipping_range()
self._plotter.update()
return True
except Exception as exc:
log_exception(__name__, "animate_focus_on_quadrants", exc)
return False
def show_grid_step_quadrants(
self: "ModelViewWidget",
origin: Tuple[float, float, float],
direction: Tuple[float, float],
step_values: list[int],
cell_size: float = 3000.0,
height: float = 100.0,
) -> None:
"""Показать 6 площадок выбора шага сетки (3x2) в положительных локальных координатах."""
if not self._plotter:
return
self.clear_quadrants()
x0, y0, z0 = origin
dir_x = 1.0 if float(direction[0]) >= 0.0 else -1.0
dir_y = 1.0 if float(direction[1]) >= 0.0 else -1.0
z_min = float(z0)
z_max = float(z0) + float(height)
grid_actors = list(getattr(self, "_quadrant_grid_actors", []) or [])
for actor in grid_actors:
try:
self._plotter.remove_actor(actor)
except Exception as _exc:
log_exception(__name__, "show_grid_step_quadrants", _exc)
self._quadrant_grid_actors = []
def _add_segment(
pts: list[list[float]],
cells: list[int],
p1: tuple[float, float, float],
p2: tuple[float, float, float],
) -> None:
i1 = len(pts)
pts.append([float(p1[0]), float(p1[1]), float(p1[2])])
i2 = len(pts)
pts.append([float(p2[0]), float(p2[1]), float(p2[2])])
cells.extend([2, i1, i2])
for idx, step in enumerate(step_values):
row = idx // 3
col = idx % 3
lx0 = float(col) * float(cell_size)
lx1 = float(col + 1) * float(cell_size)
ly0 = float(row) * float(cell_size)
ly1 = float(row + 1) * float(cell_size)
wx0 = float(x0) + lx0 * dir_x
wx1 = float(x0) + lx1 * dir_x
wy0 = float(y0) + ly0 * dir_y
wy1 = float(y0) + ly1 * dir_y
x_min, x_max = (min(wx0, wx1), max(wx0, wx1))
y_min, y_max = (min(wy0, wy1), max(wy0, wy1))
box = pv.Box(bounds=(x_min, x_max, y_min, y_max, z_min, z_max))
name = f"__quadrant_plane_{idx}"
actor = self._plotter.add_mesh(
box,
color=(0.6, 0.6, 0.6),
opacity=0.12,
show_edges=True,
edge_color=(0.7, 0.7, 0.7),
line_width=1.0,
name=name,
)
try:
actor.PickableOn()
except Exception as _exc:
log_exception(__name__, "_add_segment", _exc)
self._quadrant_actors.append(actor)
# Явная разметка сеткой с шагом площадки (поверх плоскости).
line_points: list[list[float]] = []
line_cells: list[int] = []
z_grid = float(z_max) + 0.8
step_size = max(10.0, float(step))
x_pos = float(x_min)
while x_pos <= float(x_max) + 1e-6:
_add_segment(line_points, line_cells, (x_pos, y_min, z_grid), (x_pos, y_max, z_grid))
x_pos += step_size
y_pos = float(y_min)
while y_pos <= float(y_max) + 1e-6:
_add_segment(line_points, line_cells, (x_min, y_pos, z_grid), (x_max, y_pos, z_grid))
y_pos += step_size
if line_points and line_cells:
try:
grid_lines = pv.PolyData(line_points)
grid_lines.lines = line_cells
grid_actor = self._plotter.add_mesh(
grid_lines,
color=(0.0, 0.95, 1.0),
line_width=2.0,
opacity=0.95,
lighting=False,
)
self._quadrant_grid_actors.append(grid_actor)
except Exception as _exc:
log_exception(__name__, "_add_segment", _exc)
label_pos = [(x_min + x_max) / 2.0, (y_min + y_max) / 2.0, float(z0) + float(height)]
label_name = f"__quadrant_label_{idx}"
label_actor = self._plotter.add_point_labels(
[label_pos],
[f"{int(step)} мм"],
font_size=56,
text_color="yellow",
point_color="yellow",
point_size=24,
render_points_as_spheres=True,
always_visible=True,
shape_opacity=0.9,
name=label_name,
)
try:
if label_actor is not None:
if hasattr(label_actor, "PickableOff"):
label_actor.PickableOff()
elif hasattr(label_actor, "SetPickable"):
label_actor.SetPickable(False)
except Exception as _exc:
log_exception(__name__, "_add_segment", _exc)
self._quadrant_label_actors.append(label_name)
self._plotter.update()
def highlight_quadrant(self: "ModelViewWidget", index: int) -> None:
if not self._plotter:
return
for i, actor in enumerate(self._quadrant_actors):
try:
if i == index:
actor.GetProperty().SetOpacity(1.0)
actor.GetProperty().SetColor(1.0, 1.0, 0.0)
else:
actor.GetProperty().SetOpacity(0.12)
actor.GetProperty().SetColor(0.6, 0.6, 0.6)
except Exception as _exc:
log_exception(__name__, "highlight_quadrant", _exc)
try:
if hasattr(self, "_safe_render"):
self._safe_render(min_interval_s=0.01)
else:
self._plotter.render()
except Exception as _exc:
log_exception(__name__, "highlight_quadrant", _exc)
def clear_quadrants(self: "ModelViewWidget") -> None:
if not self._plotter:
return
for actor in self._quadrant_actors:
try:
self._plotter.remove_actor(actor)
except Exception as _exc:
log_exception(__name__, "clear_quadrants", _exc)
self._quadrant_actors = []
for name in self._quadrant_label_actors:
try:
self._plotter.remove_actor(name)
except Exception as _exc:
log_exception(__name__, "clear_quadrants", _exc)
self._quadrant_label_actors = []
for actor in list(getattr(self, "_quadrant_grid_actors", []) or []):
try:
self._plotter.remove_actor(actor)
except Exception as _exc:
log_exception(__name__, "clear_quadrants", _exc)
self._quadrant_grid_actors = []
try:
if hasattr(self, "_safe_render"):
self._safe_render(min_interval_s=0.05)
else:
self._plotter.render()
except Exception as _exc:
log_exception(__name__, "clear_quadrants", _exc)
# -- оси -------------------------------------------------------------------
def show_axes(self: "ModelViewWidget", origin: Tuple[float, float, float], dir_x: float, dir_y: float) -> None:
if not self._plotter:
return
for actor in self._axes_actors:
try:
self._plotter.remove_actor(actor)
except Exception as _exc:
log_exception(__name__, "show_axes", _exc)
self._axes_actors = []
x0, y0, z0 = origin
axes = [
((dir_x, 0, 0), (1, 0, 0), "X"), # X: red
((0, dir_y, 0), (0, 1, 0), "Y"), # Y: green
((0, 0, 1), (0, 0, 1), "Z"), # Z-up: blue
]
axis_len = 1500.0
label_offset = 1700.0
for direction, color, label in axes:
arrow = pv.Arrow(start=(x0, y0, z0), direction=direction, scale=axis_len)
actor = self._plotter.add_mesh(
arrow,
color=color,
opacity=1.0,
lighting=False,
ambient=1.0,
diffuse=0.0,
specular=0.0,
)
self._axes_actors.append(actor)
try:
dx, dy, dz = float(direction[0]), float(direction[1]), float(direction[2])
label_pos = [[x0 + dx * label_offset, y0 + dy * label_offset, z0 + dz * label_offset]]
label_actor = self._plotter.add_point_labels(
label_pos,
[label],
font_size=36,
text_color=color,
point_size=1,
shape_opacity=0.0,
always_visible=True,
)
if label_actor is not None:
self._axes_actors.append(label_actor)
except Exception as _exc:
log_exception(__name__, "show_axes", _exc)
try:
if hasattr(self, "_reset_camera_clipping_range"):
self._reset_camera_clipping_range()
except Exception as _exc:
log_exception(__name__, "show_axes", _exc)
try:
if hasattr(self, "_safe_render"):
self._safe_render(min_interval_s=0.0)
else:
self._plotter.render()
except Exception as _exc:
log_exception(__name__, "show_axes", _exc)
# -- преобразования координат ----------------------------------------------
def world_to_display(self: "ModelViewWidget", x: float, y: float, z: float) -> Optional[Tuple[float, float]]:
if not self._plotter or not self._plotter.renderer:
return None
try:
self._plotter.renderer.SetWorldPoint(x, y, z, 1.0)
self._plotter.renderer.WorldToDisplay()
dx, dy, _ = self._plotter.renderer.GetDisplayPoint()
return (dx, dy)
except Exception as exc:
log_exception(__name__, "world_to_display", exc)
return None
def pick_world(self: "ModelViewWidget", sx: float, sy: float) -> Optional[Tuple[float, float, float]]:
if not self._plotter or not self._plotter.renderer or not self._plotter.picker:
return None
try:
self._plotter.picker.Pick(sx, sy, 0, self._plotter.renderer)
pos = self._plotter.picker.GetPickPosition()
if pos is None:
return None
x, y, z = pos[0], pos[1], pos[2]
if all(math.isfinite(v) for v in (x, y, z)):
return (x, y, z)
except Exception as exc:
log_exception(__name__, "pick_world", exc)
return None
return None
def screen_to_world_on_plane(
self: "ModelViewWidget", sx: float, sy: float, z_plane: float,
) -> Optional[Tuple[float, float, float]]:
"""Преобразовать экранные координаты в мировые на плоскости Z=z_plane."""
if not self._plotter or not self._plotter.renderer:
return None
try:
renderer = self._plotter.renderer
renderer.SetDisplayPoint(sx, sy, 0)
renderer.DisplayToWorld()
p0 = renderer.GetWorldPoint()
renderer.SetDisplayPoint(sx, sy, 1)
renderer.DisplayToWorld()
p1 = renderer.GetWorldPoint()
if p0[3] == 0 or p1[3] == 0:
return None
p0 = (p0[0] / p0[3], p0[1] / p0[3], p0[2] / p0[3])
p1 = (p1[0] / p1[3], p1[1] / p1[3], p1[2] / p1[3])
dz = p1[2] - p0[2]
if dz == 0:
return None
t = (z_plane - p0[2]) / dz
wx = p0[0] + t * (p1[0] - p0[0])
wy = p0[1] + t * (p1[1] - p0[1])
return (wx, wy, z_plane)
except Exception as exc:
log_exception(__name__, "screen_to_world_on_plane", exc)
return None
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
# Визуальные вспомогательные функции: маркеры, оси, перевод координат, подбор точек.
#
# 2) Последовательность действий и вызовов:
# A. Класс VisualHelpersMixin: точки входа
# Публичные методы сценария:
# - VisualHelpersMixin.show_origin_marker(...)
# - VisualHelpersMixin.show_origin_preview_marker(...)
# - VisualHelpersMixin.clear_origin_marker(...)
# - VisualHelpersMixin.clear_origin_preview_marker(...)
# - VisualHelpersMixin.clear_origin_markers(...)
# - VisualHelpersMixin.set_corner_points(...)
# - VisualHelpersMixin.get_corner_points(...)
# - VisualHelpersMixin.show_quadrants(...)
# - VisualHelpersMixin.highlight_quadrant(...)
# - VisualHelpersMixin.clear_quadrants(...)
# - VisualHelpersMixin.show_axes(...)
# - VisualHelpersMixin.world_to_display(...)
# - VisualHelpersMixin.pick_world(...)
# - VisualHelpersMixin.screen_to_world_on_plane(...)
#
# B. VisualHelpersMixin: запуск и настройка:
# VisualHelpersMixin.set_corner_points(...)
# Назначение: устанавливает corner points в рамках текущего сценария модуля.
#
# C. VisualHelpersMixin: основной сценарий:
# VisualHelpersMixin.show_origin_marker(...)
# Назначение: показывает origin marker в рамках текущего сценария модуля.
# VisualHelpersMixin.show_origin_preview_marker(...)
# Назначение: показывает динамический маркер origin в рамках текущего сценария модуля.
# VisualHelpersMixin.show_quadrants(...)
# Назначение: показывает quadrants в рамках текущего сценария модуля.
# Последовательность внутренних вызовов:
# -> VisualHelpersMixin.clear_quadrants(...)
# VisualHelpersMixin.show_axes(...)
# Назначение: показывает axes в рамках текущего сценария модуля.
# VisualHelpersMixin.pick_world(...)
# Назначение: выполняет шаг "pick world" в рамках текущего сценария модуля.
#
# D. VisualHelpersMixin: завершение и очистка:
# VisualHelpersMixin.clear_origin_marker(...)
# Назначение: очищает origin marker в рамках текущего сценария модуля.
# VisualHelpersMixin.clear_origin_preview_marker(...)
# Назначение: очищает origin preview marker в рамках текущего сценария модуля.
# VisualHelpersMixin.clear_origin_markers(...)
# Назначение: очищает все маркеры origin в рамках текущего сценария модуля.
# VisualHelpersMixin.clear_quadrants(...)
# Назначение: очищает quadrants в рамках текущего сценария модуля.
#
# E. VisualHelpersMixin: вспомогательные расчёты:
# VisualHelpersMixin.get_corner_points(...)
# Назначение: возвращает corner points в рамках текущего сценария модуля.
# VisualHelpersMixin.highlight_quadrant(...)
# Назначение: подсвечивает quadrant в рамках текущего сценария модуля.
# VisualHelpersMixin.world_to_display(...)
# Назначение: выполняет шаг "world to display" в рамках текущего сценария модуля.
# VisualHelpersMixin.screen_to_world_on_plane(...)
# Назначение: Преобразовать экранные координаты в мировые на плоскости Z=z_plane.
#
# 3) Важные ограничения и инварианты:
# - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
# - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
# - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
# - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
# - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
# - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
# - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).