"""
Per-action mouse handlers for WolfMapViewer.
Rationale
---------
``On_Mouse_Right_Down`` and ``On_Mouse_Motion`` were monolithic catch-all
chains of ``if/elif`` blocks — one branch per action. This module extracts
those branches into individual handler functions and exposes two dispatch
tables:
* ``ACTION_RDOWN_HANDLERS`` — called on right mouse-button press.
* ``ACTION_MOTION_HANDLERS`` — called on mouse motion while an action is active.
Usage (in WolfMapViewer)::
ctx = MouseContext(x, y, x_snap, y_snap, x_pixel=0, y_pixel=0,
keyboard=KeyboardSnapshot(alt=alt, ctrl=ctrl, shift=shift))
if self.action in ACTION_RDOWN_HANDLERS:
ACTION_RDOWN_HANDLERS[self.action](self, ctx)
Adding a new action
-------------------
1. Write ``_rdown_<action>(viewer, ctx)`` and/or ``_motion_<action>(viewer, ctx)``.
2. Register in ``ACTION_RDOWN_HANDLERS`` / ``ACTION_MOTION_HANDLERS``.
3. Add an ``ActionKind`` entry to ``_action_kind.py`` if not yet present.
Each handler receives the viewer (``WolfMapViewer``) and a ``MouseContext``.
Handlers must NOT import ``WolfMapViewer`` at module level to avoid circular
imports — use ``TYPE_CHECKING`` for type hints only.
"""
from __future__ import annotations
import logging
import numpy as np
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Callable
from ._action_kind import ActionKind
from .PyTranslate import _
from .PyVertex import wolfvertex
if TYPE_CHECKING:
from .PyDraw import WolfMapViewer
# ---------------------------------------------------------------------------
# Keyboard snapshot (polled state + key-event info)
# ---------------------------------------------------------------------------
@dataclass(slots=True)
[docs]
class KeyboardSnapshot:
"""Keyboard state snapshot — covers both polled and event-driven contexts.
*When embedded in a* :class:`MouseContext` *(polled at mouse-event time)*:
``key_code = 0``, ``is_down = True``,
``held`` contains all non-modifier keys currently pressed.
*When built from a* ``wx.KeyEvent`` *(key-down / key-up)*:
``key_code`` = triggering key, ``is_down`` = True/False,
``held`` = empty frozenset (not polled for key events).
"""
[docs]
ctrl: bool = False #: Ctrl modifier held
[docs]
shift: bool = False #: Shift modifier held
[docs]
alt: bool = False #: Alt modifier held
[docs]
key_code: int = 0 #: triggering key code (0 = polled, not an event)
[docs]
is_down: bool = True #: True = key-down event (unused for polled snapshots)
[docs]
held: frozenset = field(default_factory=frozenset)
#: non-modifier keys held at sample time (ord/WXK codes)
[docs]
def is_key_down(self, key_code: int) -> bool:
"""Return ``True`` if *key_code* is currently held."""
return key_code in self.held
# ---------------------------------------------------------------------------
# Mouse event context
# ---------------------------------------------------------------------------
@dataclass(slots=True)
[docs]
class MouseContext:
"""Preprocessed mouse event data passed to every action handler.
All coordinates are in *world* space (map units, not pixels).
Keyboard modifier state is accessible via the :attr:`keyboard` attribute
or the convenience properties :attr:`alt`, :attr:`ctrl`, :attr:`shift`.
"""
[docs]
x: float #: raw world X of the cursor
[docs]
y: float #: raw world Y of the cursor
[docs]
x_snap: float #: grid-snapped world X (== x when snapping is off)
[docs]
y_snap: float #: grid-snapped world Y (== y when snapping is off)
[docs]
x_pixel: int #: pixel X of the cursor (relative to the canvas)
[docs]
y_pixel: int #: pixel Y of the cursor (relative to the canvas)
# ── Keyboard snapshot (modifiers + polled key state) ──
[docs]
keyboard: KeyboardSnapshot = field(default_factory=KeyboardSnapshot)
# ── Button states ──
[docs]
left_down: bool = False #: left mouse button held during motion
[docs]
middle_down: bool = False #: middle mouse button held during motion
[docs]
right_down: bool = False #: right mouse button held / just pressed
[docs]
pressure: float = 1.0 #: stylus pressure in [0, 1] (1.0 = full / mouse)
# ── Wheel extras (non-zero only when built from a wheel event) ──
[docs]
wheel_rotation: int = 0 #: e.GetWheelRotation() (signed, pixels)
[docs]
wheel_delta: int = 120 #: e.GetWheelDelta() (unit per notch, usually 120)
# ── Convenience properties (delegate to keyboard) ────────────────────────
@property
[docs]
def alt(self) -> bool:
"""Alt key held (delegates to ``keyboard.alt``)."""
return self.keyboard.alt
@property
[docs]
def ctrl(self) -> bool:
"""Ctrl key held (delegates to ``keyboard.ctrl``)."""
return self.keyboard.ctrl
@property
[docs]
def shift(self) -> bool:
"""Shift key held (delegates to ``keyboard.shift``)."""
return self.keyboard.shift
# ---------------------------------------------------------------------------
# Type aliases
# ---------------------------------------------------------------------------
#: Right-down / motion handler ``(viewer, MouseContext) -> None``
[docs]
_RightDownHandler = Callable[['WolfMapViewer', MouseContext], None]
#: Left-down handler ``(viewer, MouseContext) -> None``
[docs]
_LeftDownHandler = Callable[['WolfMapViewer', MouseContext], None]
#: Key handler ``(viewer, KeyboardSnapshot) -> bool``
#: Return ``True`` to consume the event (prevents default processing).
[docs]
_KeyHandler = Callable[['WolfMapViewer', KeyboardSnapshot], bool]
#: Paint hook ``(viewer) -> None``
#: Called after all data layers, before UI overlays.
[docs]
_PaintHandler = Callable[['WolfMapViewer'], None]
# ===========================================================================
# RIGHT-DOWN handlers
# ===========================================================================
# --- MOVE_VECTOR -----------------------------------------------------------
[docs]
def _rdown_move_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Right-click handler for MOVE_VECTOR.
First click records the start position; second click commits the move.
Shift constrains vertical movement to zero; Alt constrains horizontal.
"""
if v.active_vector is None:
logging.warning(_('No vector selected -- Please select a vector first !'))
return
if v.active_vector._move_start is None:
v.active_vector._move_start = (ctx.x, ctx.y)
return
delta_x = ctx.x - v.active_vector._move_start[0]
delta_y = ctx.y - v.active_vector._move_start[1]
if ctx.shift:
delta_y = 0.
if ctx.alt:
delta_x = 0.
v.active_vector.move(delta_x, delta_y)
v.active_vector.clear_cache()
v.active_vector._move_start = None
v.end_action(_('End move vector'))
if v.active_fig is not None:
try:
opts = getattr(v, 'active_fig_options', None)
if opts is not None and v.active_vector is opts.get('vector'):
v.active_vector.update_linked_wx(
v.active_fig, opts['linkedarrays']
)
except Exception:
logging.warning(
_('Error while updating the figure linked to the moved vector'
' -- Maybe the figure was closed ?')
)
# --- ROTATE_VECTOR ---------------------------------------------------------
[docs]
def _rdown_rotate_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Right-click handler for ROTATE_VECTOR.
First click sets the rotation centre; second click commits the rotation.
Shift+click sets a discrete rotation step; Shift+Ctrl+click clears it.
"""
if v.active_vector is None:
logging.warning(_('No vector selected -- Please select a vector first !'))
return
if v.active_vector._rotation_center is None:
v.active_vector._rotation_center = (ctx.x, ctx.y)
return
if ctx.shift:
if ctx.ctrl:
v.active_vector._rotation_step = None
else:
v.active_vector._rotation_step = np.degrees(
np.arctan2(
ctx.y - v.active_vector._rotation_center[1],
ctx.x - v.active_vector._rotation_center[0],
)
)
v.active_vector.rotate_xy(ctx.x, ctx.y)
v.active_vector.clear_cache()
v.active_vector._rotation_center = None
v.end_action(_('End rotate vector'))
# --- MOVE_TRIANGLES --------------------------------------------------------
[docs]
def _rdown_move_triangles(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Right-click handler for MOVE_TRIANGLES.
First click records the start; second click commits the move.
Shift constrains vertical movement; Alt constrains horizontal.
"""
if v.active_tri is None:
logging.warning(_('No triangles selected -- Please select a triangulation first !'))
return
if v.active_tri._move_start is None:
v.active_tri._move_start = (ctx.x, ctx.y)
return
delta_x = ctx.x - v.active_tri._move_start[0]
delta_y = ctx.y - v.active_tri._move_start[1]
if ctx.shift:
delta_y = 0.
if ctx.alt:
delta_x = 0.
v.active_tri.move(delta_x, delta_y)
v.active_tri.reset_plot()
v.active_tri._move_start = None
v.active_tri.clear_cache()
v.end_action(_('End move triangulation'))
# --- ROTATE_TRIANGLES ------------------------------------------------------
[docs]
def _rdown_rotate_triangles(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Right-click handler for ROTATE_TRIANGLES.
First click sets the rotation centre; second click commits the rotation.
Shift+click sets a discrete step; Shift+Ctrl+click clears it.
"""
if v.active_tri is None:
logging.warning(_('No vector selected -- Please select a triangulation first !'))
return
if v.active_tri._rotation_center is None:
v.active_tri._rotation_center = (ctx.x, ctx.y)
return
if ctx.shift:
if ctx.ctrl:
v.active_tri._rotation_step = None
else:
v.active_tri._rotation_step = np.degrees(
np.arctan2(
ctx.y - v.active_tri._rotation_center[1],
ctx.x - v.active_tri._rotation_center[0],
)
)
v.active_tri.rotate_xy(ctx.x, ctx.y)
v.active_tri._rotation_center = None
v.active_tri.clear_cache()
v.active_tri.reset_plot()
v.end_action(_('End rotate triangulation'))
# --- MOVE_ZONE -------------------------------------------------------------
[docs]
def _rdown_move_zone(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Right-click handler for MOVE_ZONE.
First click records the start; second click commits the move.
.. note::
The shift/alt constraints set ``delta_x``/``delta_y`` local variables
that are **not** forwarded to ``zone.move()`` — this matches the
original behaviour (preserved intentionally).
"""
if v.active_zone is None:
logging.warning(_('No zone selected -- Please select a zone first !'))
return
if v.active_zone._move_start is None:
v.active_zone._move_start = (ctx.x, ctx.y)
return
if ctx.shift:
delta_y = 0. # noqa: F841 – preserved from original (not used below)
if ctx.alt:
delta_x = 0. # noqa: F841 – preserved from original (not used below)
v.active_zone.move(
ctx.x - v.active_zone._move_start[0],
ctx.y - v.active_zone._move_start[1],
)
v.active_zone.clear_cache()
v.active_zone._move_start = None
v.end_action(_('End move zone'))
# --- ROTATE_ZONE -----------------------------------------------------------
[docs]
def _rdown_rotate_zone(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Right-click handler for ROTATE_ZONE.
First click sets the rotation centre; second click commits the rotation.
"""
if v.active_zone is None:
logging.warning(_('No zone selected -- Please select a zone first !'))
return
if v.active_zone._rotation_center is None:
v.active_zone._rotation_center = (ctx.x, ctx.y)
return
v.active_zone.rotate_xy(ctx.x, ctx.y)
v.active_zone.clear_cache()
v.active_zone._rotation_center = None
v.end_action(_('End rotate zone'))
# --- ADD_POINTS_TO_CLOUD ---------------------------------------------------
[docs]
def _rdown_add_points_to_cloud(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Shift: remove nearest vertex. Ctrl: remove last vertex. Else: add snapped vertex."""
if v.active_cloud is None:
return
if ctx.shift:
v.active_cloud.remove_nearest_vertex(ctx.x, ctx.y)
elif ctx.ctrl:
v.active_cloud.remove_last_vertex()
else:
v.active_cloud.add_vertex(wolfvertex(ctx.x_snap, ctx.y_snap))
# --- MOVE_POINT_IN_CLOUD ---------------------------------------------------
[docs]
def _rdown_move_point_in_cloud(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""First click picks nearest vertex; second click commits the final position."""
if v.active_cloud is None:
logging.warning(_('No active cloud -- Please load data first'))
return
if v.active_cloud_vertex_id is None:
max_dist = v._cloud_move_pick_tolerance()
picked = v.active_cloud.find_nearest_id([[ctx.x, ctx.y]], max_distance=max_dist)
if picked is None:
logging.warning(_('No nearby point found to move'))
return
v.active_cloud_vertex_id = picked
return
moved = v.active_cloud.move_vertex(
v.active_cloud_vertex_id,
ctx.x_snap,
ctx.y_snap,
invalidate_tree=True,
notify=True,
recompute_bounds=True,
)
if moved:
v.active_cloud_vertex_id = None
# --- PLOT_ALARO_XY ---------------------------------------------------------
[docs]
def _rdown_plot_alaro_xy(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if v.active_alaro._gdf is None:
logging.warning(_('No Alaro run loaded -- Please load a run first'))
return
fig = v.active_alaro.plot_Rain_and_TotPrecip4XY(ctx.x, ctx.y)
fig.show()
# --- DISTANCE_ALONG_VECTOR -------------------------------------------------
[docs]
def _rdown_distance_along_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v._tmp_vector_distance.add_vertex(wolfvertex(ctx.x, ctx.y))
# --- PICK_PIE_CENTER -------------------------------------------------------
[docs]
def _rdown_pick_pie_center(v: 'WolfMapViewer', ctx: MouseContext) -> None:
ctrl = getattr(v, '_pie_pick_controller', None)
ed = getattr(v, '_pie_pick_editor', None)
try:
if ctrl is not None:
ctrl.update_geometry(x=ctx.x, y=ctx.y, rebuild=True)
if ed is not None and hasattr(ed, 'refresh_from_controller'):
ed.refresh_from_controller()
ed.Raise()
finally:
v._pie_pick_controller = None
v._pie_pick_editor = None
v.end_action(_('Pie center selected'))
# --- PICK_BAR_POSITION -----------------------------------------------------
[docs]
def _rdown_pick_bar_position(v: 'WolfMapViewer', ctx: MouseContext) -> None:
ctrl = getattr(v, '_bar_pick_controller', None)
ed = getattr(v, '_bar_pick_editor', None)
try:
if ctrl is not None:
ctrl.update_geometry(x=ctx.x, y=ctx.y, rebuild=True)
if ed is not None and hasattr(ed, 'refresh_from_controller'):
ed.refresh_from_controller()
ed.Raise()
finally:
v._bar_pick_controller = None
v._bar_pick_editor = None
v.end_action(_('Bar position selected'))
# --- PICK_CURVE_ORIGIN -----------------------------------------------------
[docs]
def _rdown_pick_curve_origin(v: 'WolfMapViewer', ctx: MouseContext) -> None:
ctrl = getattr(v, '_curve_pick_controller', None)
ed = getattr(v, '_curve_pick_editor', None)
try:
if ctrl is not None:
ctrl.update_geometry(canvas_origin=(ctx.x, ctx.y), rebuild=True)
if ed is not None and hasattr(ed, 'refresh_from_controller'):
ed.refresh_from_controller()
ed.Raise()
finally:
v._curve_pick_controller = None
v._curve_pick_editor = None
v.end_action(_('Curve canvas origin selected'))
# --- PICK_LANDMAP (shared by FULL and LOW) ----------------------------------
[docs]
def _rdown_pick_landmap(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if v.active_landmap is None:
logging.warning(_('No landmap available -- Please activate the data and retry !'))
return
which = 'full' if v.action == ActionKind.PICK_LANDMAP_FULL else 'low'
v.active_landmap.load_texture(ctx.x, ctx.y, which=which)
v.Refresh()
# --- PICK_MUNICIPALITY -----------------------------------------------------
[docs]
def _rdown_pick_municipality(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if v.active_qdfidf is None:
logging.warning(_('No municipality data available -- Please activate the data and retry !'))
return
v.active_qdfidf.pick_municipality(ctx.x, ctx.y, v.get_canvas_bounds())
# --- PICK_A_PICTURE --------------------------------------------------------
[docs]
def _rdown_pick_a_picture(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if v.active_picturecollection is None:
logging.warning(_('No picture collection available -- Please activate the data and retry !'))
return
vec = v.active_picturecollection.find_vector_containing_point(ctx.x, ctx.y)
vec.myprop.imagevisible = not vec.myprop.imagevisible
if ctx.shift:
vec.myprop.legendvisible = not vec.myprop.legendvisible
vec.myprop.update_myprops()
vec.parentzone.reset_listogl()
v.active_picturecollection.Activate_vector(vec)
v.Refresh()
# --- PICK_BRIDGE -----------------------------------------------------------
[docs]
def _rdown_pick_bridge(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.pick_bridge(ctx.x, ctx.y)
# --- PICK_WEIR -------------------------------------------------------------
[docs]
def _rdown_pick_weir(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.pick_weir(ctx.x, ctx.y)
# --- BRIDGE_GLTF -----------------------------------------------------------
[docs]
def _rdown_bridge_gltf(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Delegate to PyDraw._bridge_gltf_dialog() which owns the wx dialogs."""
v._bridge_gltf_dialog(ctx.x, ctx.y)
# --- PLOT_CROSS_SECTION ----------------------------------------------------
[docs]
def _rdown_plot_cross_section(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.plot_cross(ctx.x, ctx.y)
# --- SET_1D_PROFILE --------------------------------------------------------
[docs]
def _rdown_set_1d_profile(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Delegate to PyDraw._set_1d_profile_rdown() which owns the notebook/wx logic."""
v._set_1d_profile_rdown(ctx.x, ctx.y)
# --- SELECT_NEAREST_PROFILE ------------------------------------------------
[docs]
def _rdown_select_nearest_profile(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Delegate to PyDraw._select_nearest_profile_rdown() which owns the notebook/wx logic."""
v._select_nearest_profile_rdown(ctx.x, ctx.y)
# --- SELECT_ACTIVE_TILE ----------------------------------------------------
[docs]
def _rdown_select_active_tile(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.active_tile.select_vectors_from_point(ctx.x, ctx.y, True)
v.active_vector = v.active_tile.get_selected_vectors()
tilearray = v.active_tile.get_array(v.active_vector)
if tilearray is not None:
if v.active_vector.myname == '':
bbox = v.active_vector.get_bounds()
id_label = '{}-{}'.format(bbox[0][0], bbox[1][1])
else:
id_label = v.active_vector.myname
v.add_object('array', newobj=tilearray, ToCheck=True, id=id_label)
# --- SELECT_ACTIVE_IMAGE_TILE ----------------------------------------------
[docs]
def _rdown_select_active_image_tile(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.active_imagestiles.select_vectors_from_point(ctx.x, ctx.y, True)
active_tile = v.active_imagestiles.get_selected_vectors()
active_tile.myprop.imagevisible = not active_tile.myprop.imagevisible
# --- PICK_CATCHMENT_LIDAXE -------------------------------------------------
[docs]
def _rdown_pick_catchment_lidaxe(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if v.active_lidaxe is None:
logging.warning(_('No Lidaxe data available -- Please activate the data and retry !'))
return
catchment, accumulation, direction = v.active_lidaxe.get_catchment(ctx.x, ctx.y)
if catchment is not None:
v.add_object('array', newobj=catchment, ToCheck=True,
id='catchment_{}_{}'.format(ctx.x, ctx.y))
if accumulation is not None:
v.add_object('array', newobj=accumulation, ToCheck=True,
id='accumulation_{}_{}'.format(ctx.x, ctx.y))
accumulation.mypal.distribute_values(0., 10_000.)
v.Refresh()
# --- PICK_PATH_LIDAXE ------------------------------------------------------
[docs]
def _rdown_pick_path_lidaxe(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if v.active_lidaxe is None:
logging.warning(_('No Lidaxe data available -- Please activate the data and retry !'))
return
path = v.active_lidaxe.get_longest_flow_path(ctx.x, ctx.y, snap_to_max=False)
v.Active_vector(path)
v.Refresh()
# --- SELECT_ACTIVE_VECTOR (4 variants share this function) -----------------
[docs]
def _rdown_select_active_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Handler inspects v.action to differentiate inside/outside and single/all-zone."""
inside = v.action in (ActionKind.SELECT_ACTIVE_VECTOR_INSIDE,
ActionKind.SELECT_ACTIVE_VECTOR2_INSIDE)
onlyonezone = v.action in (ActionKind.SELECT_ACTIVE_VECTOR2_INSIDE,
ActionKind.SELECT_ACTIVE_VECTOR2_ALL)
if onlyonezone:
v.active_zone.select_vectors_from_point(ctx.x, ctx.y, inside)
v.active_vector = v.active_zone.get_selected_vectors()[0]
if v.active_vector is not None:
v.active_zone.parent.Activate_vector(v.active_vector)
v.active_zone.active_vector = v.active_vector
v.active_zones.active_zone = v.active_vector.parentzone
else:
v.active_zones.select_vectors_from_point(ctx.x, ctx.y, inside)
v.active_vector = v.active_zones.get_selected_vectors()
if v.active_vector is not None:
v.active_zones.Activate_vector(v.active_vector)
v.active_zone = v.active_vector.parentzone
v.active_zones.expand_tree(v.active_zone)
# --- SELECT_NODE (2 variants share this function) --------------------------
[docs]
def _rdown_select_node(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if v.action == ActionKind.SELECT_NODE_BY_NODE_RESULTS:
if v.active_res2d is None:
logging.warning(_('No 2D results available -- Please load a file or create data !'))
v.end_action(_('Force end select node by node'))
return
curobj = v.active_res2d.SelectionData
else:
if v.active_array is None:
logging.warning(_('No array available -- Please load a file or create data !'))
v.end_action(_('Force end select node by node'))
return
curobj = v.active_array.SelectionData
if curobj.myselection == 'all':
logging.warning(_('All nodes are selected !!'))
logging.warning(_('Selecting node by node will force to reset the selection'))
logging.warning(_('and start from scratch'))
curobj.add_node_to_selection(ctx.x, ctx.y)
curobj.update_nb_nodes_selection()
v.Paint()
# --- SELECT_BY_VECTOR (5 variants share this function) --------------------
[docs]
def _rdown_select_by_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.active_vector.add_vertex(wolfvertex(ctx.x, ctx.y))
# --- LAZ_TMP_VECTOR --------------------------------------------------------
[docs]
def _rdown_laz_tmp_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.active_vector.add_vertex(wolfvertex(ctx.x, ctx.y))
v.active_vector.find_minmax()
# --- CREATE_POLYGON_TILES --------------------------------------------------
[docs]
def _rdown_create_polygon_tiles(v: 'WolfMapViewer', ctx: MouseContext) -> None:
v.active_vector.add_vertex(wolfvertex(ctx.x, ctx.y))
v.active_vector.find_minmax()
# --- CAPTURE_VERTICES ------------------------------------------------------
[docs]
def _rdown_capture_vertices(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Update the trailing preview vertex, append a new one, optionally sample Z."""
new_wv = wolfvertex(ctx.x, ctx.y)
last_wv = v.active_vector.myvertices[-1]
last_wv.x = ctx.x_snap
last_wv.y = ctx.y_snap
v.active_vector.add_vertex(new_wv)
v.active_vertex = new_wv
if ctx.ctrl:
if v.active_array is not None:
last_wv.z = v.active_array.get_value(ctx.x_snap, ctx.y_snap)
else:
logging.warning(_('No array available and ctrl is pressed -- Please load a file or create data !'))
v.active_vector.find_minmax()
v.active_zone.find_minmax()
# --- OFFSET_SCALE_IMAGE ----------------------------------------------------
[docs]
def _rdown_offset_scale_image(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""First click records start; second click commits the image offset."""
if v.active_vector is None:
logging.warning(_('No vector selected -- Please select a vector first !'))
return
if v.active_vector.myprop.textureimage is None:
logging.warning(_('No image available -- Please load an image first !'))
return
if v.active_vector._move_start is None:
v.active_vector._move_start = (ctx.x, ctx.y)
return
delta_x = ctx.x - v.active_vector._move_start[0]
delta_y = ctx.y - v.active_vector._move_start[1]
v.active_vector.myprop.offset_image(delta_x, delta_y)
v.active_vector.myprop.update_myprops()
v.active_vector._move_start = None
v.end_action(_('End offset/scale image'))
# --- DYNAMIC_PARALLEL ------------------------------------------------------
[docs]
def _rdown_dynamic_parallel(v: 'WolfMapViewer', ctx: MouseContext) -> None:
if ctx.ctrl:
if v.active_array is not None:
v.active_vector.myvertices[-1].z = v.active_array.get_value(ctx.x, ctx.y)
else:
logging.warning(_('No array available and ctrl is pressed -- Please load a file or create data !'))
v.active_vector.add_vertex(wolfvertex(ctx.x, ctx.y))
v.active_zone.parallel_active(v.dynapar_dist)
# --- MODIFY_VERTICES -------------------------------------------------------
[docs]
def _rdown_modify_vertices(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""First click picks nearest vertex; second click confirms (optionally samples Z)."""
if v.active_vector is None:
logging.warning(_('No vector selected -- Please select a vector first !'))
return
if v.active_vertex is None:
v.active_vertex = v.active_vector.find_nearest_vertex(ctx.x, ctx.y)
else:
v.active_vertex.limit2bounds(v.active_vector._mylimits)
if ctx.ctrl:
if v.active_array is not None:
v.active_vertex.z = v.active_array.get_value(ctx.x_snap, ctx.y_snap)
else:
logging.warning(_('No array available and ctrl is pressed -- Please load a file or create data !'))
v.active_vertex = None
# --- INSERT_VERTICES -------------------------------------------------------
[docs]
def _rdown_insert_vertices(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""First click inserts vertex at nearest edge; second click confirms."""
if v.active_vector is None:
logging.warning(_('No vector selected -- Please select a vector first !'))
return
if v.active_vertex is None:
v.active_vertex = v.active_vector.insert_nearest_vert(ctx.x, ctx.y)
else:
if ctx.ctrl:
if v.active_array is not None:
v.active_vertex.z = v.active_array.get_value(ctx.x_snap, ctx.y_snap)
else:
logging.warning(_('No array available and ctrl is pressed -- Please load a file or create data !'))
v.active_vertex = None
# ===========================================================================
# MOTION handlers — migrated from On_Mouse_Motion
# ===========================================================================
[docs]
def _motion_move_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Motion handler for MOVE_VECTOR — live preview while the start is set."""
if v.active_vector is not None and v.active_vector._move_start is not None:
delta_x = ctx.x - v.active_vector._move_start[0]
delta_y = ctx.y - v.active_vector._move_start[1]
if ctx.shift:
delta_y = 0.
if ctx.alt:
delta_x = 0.
v.active_vector.move(delta_x, delta_y)
[docs]
def _motion_rotate_vector(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Motion handler for ROTATE_VECTOR — live preview while centre is set."""
if v.active_vector is not None and v.active_vector._rotation_center is not None:
v.active_vector.rotate_xy(ctx.x, ctx.y)
[docs]
def _motion_move_triangles(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Motion handler for MOVE_TRIANGLES — live preview while start is set."""
if v.active_tri is not None and v.active_tri._move_start is not None:
delta_x = ctx.x - v.active_tri._move_start[0]
delta_y = ctx.y - v.active_tri._move_start[1]
if ctx.shift:
delta_y = 0.
if ctx.alt:
delta_x = 0.
v.active_tri.move(delta_x, delta_y)
v.active_tri.reset_plot()
[docs]
def _motion_rotate_triangles(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Motion handler for ROTATE_TRIANGLES — live preview while centre is set."""
if v.active_tri is not None and v.active_tri._rotation_center is not None:
v.active_tri.rotate_xy(ctx.x, ctx.y)
v.active_tri.reset_plot()
[docs]
def _motion_move_zone(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Motion handler for MOVE_ZONE — live preview while start is set."""
if v.active_zone is not None and v.active_zone._move_start is not None:
delta_x = ctx.x - v.active_zone._move_start[0]
delta_y = ctx.y - v.active_zone._move_start[1]
if ctx.shift:
delta_y = 0.
if ctx.alt:
delta_x = 0.
v.active_zone.move(delta_x, delta_y)
[docs]
def _motion_rotate_zone(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Motion handler for ROTATE_ZONE — live preview while centre is set."""
if v.active_zone is not None and v.active_zone._rotation_center is not None:
v.active_zone.rotate_xy(ctx.x, ctx.y)
[docs]
def _motion_move_point_in_cloud(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Live preview for MOVE_POINT_IN_CLOUD while vertex is picked."""
if v.active_cloud is not None and v.active_cloud_vertex_id is not None:
v.active_cloud.move_vertex(
v.active_cloud_vertex_id,
ctx.x_snap,
ctx.y_snap,
invalidate_tree=False,
notify=True,
recompute_bounds=False,
)
[docs]
def _motion_dynamic_parallel(v: 'WolfMapViewer', ctx: MouseContext) -> None:
"""Recompute the parallel line on every mouse-move (no coords needed)."""
v.active_zone.parallel_active(v.dynapar_dist)
# ===========================================================================
# Dispatch tables
# ===========================================================================
#: Maps ``ActionKind`` → right-down handler.
#: To register a new action, add one entry here — no changes to the
#: monolithic ``On_Mouse_Right_Down`` body are needed.
[docs]
ACTION_RDOWN_HANDLERS: dict[ActionKind, _RightDownHandler] = {
# --- triangulation ---
ActionKind.MOVE_TRIANGLES: _rdown_move_triangles,
ActionKind.ROTATE_TRIANGLES: _rdown_rotate_triangles,
# --- vector / zone ---
ActionKind.MOVE_VECTOR: _rdown_move_vector,
ActionKind.ROTATE_VECTOR: _rdown_rotate_vector,
ActionKind.MOVE_ZONE: _rdown_move_zone,
ActionKind.ROTATE_ZONE: _rdown_rotate_zone,
# --- cloud ---
ActionKind.ADD_POINTS_TO_CLOUD: _rdown_add_points_to_cloud,
ActionKind.MOVE_POINT_IN_CLOUD: _rdown_move_point_in_cloud,
# --- alaro ---
ActionKind.PLOT_ALARO_XY: _rdown_plot_alaro_xy,
# --- distance ---
ActionKind.DISTANCE_ALONG_VECTOR: _rdown_distance_along_vector,
# --- asset picks ---
ActionKind.PICK_PIE_CENTER: _rdown_pick_pie_center,
ActionKind.PICK_BAR_POSITION: _rdown_pick_bar_position,
ActionKind.PICK_CURVE_ORIGIN: _rdown_pick_curve_origin,
# --- landmap (two variants share same function) ---
ActionKind.PICK_LANDMAP_FULL: _rdown_pick_landmap,
ActionKind.PICK_LANDMAP_LOW: _rdown_pick_landmap,
# --- municipality / pictures ---
ActionKind.PICK_MUNICIPALITY: _rdown_pick_municipality,
ActionKind.PICK_A_PICTURE: _rdown_pick_a_picture,
# --- bridges / weirs ---
ActionKind.PICK_BRIDGE: _rdown_pick_bridge,
ActionKind.PICK_WEIR: _rdown_pick_weir,
ActionKind.BRIDGE_GLTF: _rdown_bridge_gltf,
# --- cross sections ---
ActionKind.PLOT_CROSS_SECTION: _rdown_plot_cross_section,
ActionKind.SET_1D_PROFILE: _rdown_set_1d_profile,
ActionKind.SELECT_NEAREST_PROFILE: _rdown_select_nearest_profile,
# --- tiles ---
ActionKind.SELECT_ACTIVE_TILE: _rdown_select_active_tile,
ActionKind.SELECT_ACTIVE_IMAGE_TILE: _rdown_select_active_image_tile,
# --- lidaxe ---
ActionKind.PICK_CATCHMENT_LIDAXE: _rdown_pick_catchment_lidaxe,
ActionKind.PICK_PATH_LIDAXE: _rdown_pick_path_lidaxe,
# --- select active vector (4 variants share same function) ---
ActionKind.SELECT_ACTIVE_VECTOR_INSIDE: _rdown_select_active_vector,
ActionKind.SELECT_ACTIVE_VECTOR_ALL: _rdown_select_active_vector,
ActionKind.SELECT_ACTIVE_VECTOR2_INSIDE: _rdown_select_active_vector,
ActionKind.SELECT_ACTIVE_VECTOR2_ALL: _rdown_select_active_vector,
# --- select node (2 variants share same function) ---
ActionKind.SELECT_NODE_BY_NODE: _rdown_select_node,
ActionKind.SELECT_NODE_BY_NODE_RESULTS: _rdown_select_node,
# --- select by vector (5 variants share same function) ---
ActionKind.SELECT_BY_VECTOR_INSIDE: _rdown_select_by_vector,
ActionKind.SELECT_BY_VECTOR_OUTSIDE: _rdown_select_by_vector,
ActionKind.SELECT_BY_VECTOR_ALONG: _rdown_select_by_vector,
ActionKind.SELECT_BY_TMP_VECTOR_INSIDE: _rdown_select_by_vector,
ActionKind.SELECT_BY_TMP_VECTOR_ALONG: _rdown_select_by_vector,
# --- misc geometry ---
ActionKind.LAZ_TMP_VECTOR: _rdown_laz_tmp_vector,
ActionKind.CREATE_POLYGON_TILES: _rdown_create_polygon_tiles,
ActionKind.CAPTURE_VERTICES: _rdown_capture_vertices,
ActionKind.DYNAMIC_PARALLEL: _rdown_dynamic_parallel,
# --- image / vertex editing ---
ActionKind.OFFSET_SCALE_IMAGE: _rdown_offset_scale_image,
ActionKind.MODIFY_VERTICES: _rdown_modify_vertices,
ActionKind.INSERT_VERTICES: _rdown_insert_vertices,
}
#: Maps ``ActionKind`` → motion handler.
#: To register a new action, add one entry here — no changes to the
#: monolithic ``On_Mouse_Motion`` body are needed.
[docs]
ACTION_MOTION_HANDLERS: dict[ActionKind, _RightDownHandler] = {
ActionKind.MOVE_VECTOR: _motion_move_vector,
ActionKind.ROTATE_VECTOR: _motion_rotate_vector,
ActionKind.MOVE_TRIANGLES: _motion_move_triangles,
ActionKind.ROTATE_TRIANGLES: _motion_rotate_triangles,
ActionKind.MOVE_ZONE: _motion_move_zone,
ActionKind.ROTATE_ZONE: _motion_rotate_zone,
ActionKind.MOVE_POINT_IN_CLOUD: _motion_move_point_in_cloud,
ActionKind.DYNAMIC_PARALLEL: _motion_dynamic_parallel,
}