626 lines
28 KiB
Python
626 lines
28 KiB
Python
# -*- 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) Правило сопровождения:
|
||
# - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
|
||
# - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
|