{ "cells": [ { "cell_type": "markdown", "id": "9ce97706", "metadata": {}, "source": [ "# Action plugin — complete reference\n", "\n", "This notebook demonstrates all five event hooks available in the per-instance plugin system:\n", "\n", "| Hook | Event | Signature |\n", "|---|---|---|\n", "| `rdown_handler` | Right mouse-button press | `(viewer, MouseContext) -> None` |\n", "| `motion_handler` | Mouse motion (any button) | `(viewer, MouseContext) -> None` |\n", "| `ldown_handler` | Left mouse-button press | `(viewer, MouseContext) -> None` |\n", "| `key_handler` | Key press | `(viewer, KeyboardSnapshot) -> bool` |\n", "| `paint_handler` | OpenGL paint (after data, before UI) | `(viewer) -> None` |\n", "\n", "The plugin built here lets the user:\n", "- **Right-click** → add a marker point\n", "- **Left-click** → select / highlight the nearest previously added point\n", "- **Mouse motion** → show live coordinates in the status bar\n", "- **Ctrl+Z** → undo last marker \n", "- **Ctrl+C** → clear all markers \n", "- **Paint hook** → draw crosses on every marker in OpenGL" ] }, { "cell_type": "markdown", "id": "0d6eefcc", "metadata": {}, "source": [ "## 1 — wx startup\n", "\n", "> **Important**: always use `%gui wx` in a notebook — never `wx.App()`. \n", "> `wx.App()` captures the event loop and blocks the kernel." ] }, { "cell_type": "code", "execution_count": 1, "id": "8f6afcf1", "metadata": {}, "outputs": [], "source": [ "import sys\n", "%gui wx" ] }, { "cell_type": "markdown", "id": "1d223589", "metadata": {}, "source": [ "## 2 — Create the MapViewer" ] }, { "cell_type": "code", "execution_count": 2, "id": "3f9c3133", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Importing wolfhece modules\n", "INFO:root:wolfhece modules imported\n", "INFO:wolfhece.tablet_wintab:WinTab : contexte ouvert sur HWND=0x20906F6 (pressure_max=32767)\n" ] }, { "data": { "text/plain": [ "False" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from wolfhece.PyDraw import WolfMapViewer\n", "\n", "viewer = WolfMapViewer(None, title=\"Plugin complete demo\", w=1200, h=800)\n", "viewer.Show()" ] }, { "cell_type": "markdown", "id": "c7a7bb60", "metadata": {}, "source": [ "## 3 — Plugin state\n", "\n", "All mutable state lives in plain Python objects — accessible from anywhere in the notebook." ] }, { "cell_type": "code", "execution_count": 3, "id": "7fa94425", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "State initialised — action id: 'mark points'\n" ] } ], "source": [ "import numpy as np\n", "from wolfhece._viewer_plugin_handlers import MouseContext, KeyboardSnapshot\n", "\n", "# ── Persistent state ──────────────────────────────────────────────────────\n", "markers: list[tuple[float, float]] = [] # list of (x, y) world positions\n", "selected_idx: list[int] = [-1] # mutable box for the selected marker index\n", "\n", "ACTION_ID = 'mark points' # action string — arbitrary lowercase\n", "\n", "print(f\"State initialised — action id: '{ACTION_ID}'\")" ] }, { "cell_type": "markdown", "id": "3af44794", "metadata": {}, "source": [ "## 4 — Handler definitions\n", "\n", "### 4a — Right-click: add a marker" ] }, { "cell_type": "code", "execution_count": 4, "id": "c0daa5f4", "metadata": {}, "outputs": [], "source": [ "def _rdown_add_marker(v: WolfMapViewer, ctx: MouseContext) -> None:\n", " \"\"\"Right-click adds a marker at the snapped cursor position.\"\"\"\n", " markers.append((ctx.x_snap, ctx.y_snap))\n", " n = len(markers)\n", " print(f\"[Marker #{n}] X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}\")\n", " v.Paint() # force immediate redraw to show the new cross" ] }, { "cell_type": "markdown", "id": "c7b6c51b", "metadata": {}, "source": [ "### 4b — Left-click: select nearest marker" ] }, { "cell_type": "code", "execution_count": 5, "id": "5aebf6e0", "metadata": {}, "outputs": [], "source": [ "def _ldown_select_nearest(v: WolfMapViewer, ctx: MouseContext) -> None:\n", " \"\"\"Left-click selects the marker closest to the cursor.\"\"\"\n", " if not markers:\n", " return\n", " pts = np.array(markers)\n", " dists = np.hypot(pts[:, 0] - ctx.x, pts[:, 1] - ctx.y)\n", " idx = int(np.argmin(dists))\n", " selected_idx[0] = idx\n", " x, y = markers[idx]\n", " print(f\"Selected marker #{idx + 1} X={x:.3f} Y={y:.3f} (dist={dists[idx]:.1f})\")\n", " v.Paint()" ] }, { "cell_type": "markdown", "id": "0f171a11", "metadata": {}, "source": [ "### 4c — Mouse motion: live coordinates in status bar" ] }, { "cell_type": "code", "execution_count": 6, "id": "cbf0ae1c", "metadata": {}, "outputs": [], "source": [ "def _motion_show_coords(v: WolfMapViewer, ctx: MouseContext) -> None:\n", " \"\"\"Display cursor world-coordinates in the viewer status bar.\"\"\"\n", " v.set_statusbar_text(f\"X={ctx.x:.2f} Y={ctx.y:.2f} | {len(markers)} marker(s)\")" ] }, { "cell_type": "markdown", "id": "6d28a343", "metadata": {}, "source": [ "### 4d — Key handler: Ctrl+Z undo, Ctrl+C clear\n", "\n", "The key handler must return `True` to **consume** the event (prevent the default viewer behaviour)." ] }, { "cell_type": "code", "execution_count": 7, "id": "1e3475c1", "metadata": {}, "outputs": [], "source": [ "import wx\n", "\n", "def _key_handler(v: WolfMapViewer, kb: KeyboardSnapshot) -> bool:\n", " \"\"\"Ctrl+Z = undo last marker. Ctrl+C = clear all.\"\"\"\n", " if not kb.ctrl:\n", " return False # let default processing handle non-ctrl keys\n", "\n", " if kb.key_code == ord('Z'):\n", " if markers:\n", " removed = markers.pop()\n", " if selected_idx[0] >= len(markers):\n", " selected_idx[0] = len(markers) - 1\n", " print(f\"Undo — removed marker at X={removed[0]:.3f} Y={removed[1]:.3f}\")\n", " v.Paint()\n", " return True # consumed\n", "\n", " if kb.key_code == ord('C'):\n", " markers.clear()\n", " selected_idx[0] = -1\n", " print(\"All markers cleared.\")\n", " v.Paint()\n", " return True # consumed\n", "\n", " return False # not our key — let default process it" ] }, { "cell_type": "markdown", "id": "633c129d", "metadata": {}, "source": [ "### 4e — Paint hook: draw markers in OpenGL\n", "\n", "The paint hook runs after all data layers (`_plotting()`) but **before** the UI overlays (toolbar, palette, etc.). \n", "Use raw `OpenGL.GL` calls — the projection matrix is already set to world coordinates." ] }, { "cell_type": "code", "execution_count": 8, "id": "f79024a4", "metadata": {}, "outputs": [], "source": [ "from OpenGL.GL import (\n", " glBegin, glEnd, glVertex2f, glColor4f, glLineWidth,\n", " GL_LINES,\n", ")\n", "\n", "# Cross size as a fraction of the visible width\n", "CROSS_FRACTION = 0.008\n", "\n", "def _paint_markers(v: WolfMapViewer) -> None:\n", " \"\"\"Draw a cross at every marker; selected marker is highlighted.\"\"\"\n", " if not markers:\n", " return\n", "\n", " half = (v.xmax - v.xmin) * CROSS_FRACTION\n", "\n", " glLineWidth(2.0)\n", " glBegin(GL_LINES)\n", " for i, (mx, my) in enumerate(markers):\n", " if i == selected_idx[0]:\n", " glColor4f(1.0, 0.8, 0.0, 1.0) # gold = selected\n", " else:\n", " glColor4f(1.0, 0.0, 0.0, 1.0) # red = normal\n", " glVertex2f(mx - half, my); glVertex2f(mx + half, my) # horizontal bar\n", " glVertex2f(mx, my - half); glVertex2f(mx, my + half) # vertical bar\n", " glEnd()\n", " glLineWidth(1.0)" ] }, { "cell_type": "markdown", "id": "85cf7737", "metadata": {}, "source": [ "## 5 — Register and activate" ] }, { "cell_type": "code", "execution_count": 9, "id": "494aaa81", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : Right-click: add marker | Left-click: select nearest | Ctrl+Z: undo | Ctrl+C: clear\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Action 'mark points' registered and active.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "[Marker #1] X=2.956 Y=20.453\n", "[Marker #2] X=22.024 Y=25.672\n", "Selected marker #1 X=2.956 Y=20.453 (dist=0.2)\n", "Selected marker #2 X=22.024 Y=25.672 (dist=0.4)\n", "Selected marker #1 X=2.956 Y=20.453 (dist=0.4)\n" ] } ], "source": [ "viewer.register_action(\n", " ACTION_ID,\n", " rdown_handler = _rdown_add_marker,\n", " ldown_handler = _ldown_select_nearest,\n", " motion_handler = _motion_show_coords,\n", " key_handler = _key_handler,\n", " paint_handler = _paint_markers,\n", ")\n", "\n", "viewer.start_action(\n", " ACTION_ID,\n", " 'Right-click: add marker | Left-click: select nearest | Ctrl+Z: undo | Ctrl+C: clear'\n", ")\n", "print(f\"Action '{ACTION_ID}' registered and active.\")" ] }, { "cell_type": "markdown", "id": "82dc1da5", "metadata": {}, "source": [ "## 6 — Inspect markers" ] }, { "cell_type": "code", "execution_count": 10, "id": "bbb6a239", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2 marker(s) | selected index: 0\n", " 1. X=2.956 Y=20.453 ← selected\n", " 2. X=22.024 Y=25.672\n" ] } ], "source": [ "print(f\"{len(markers)} marker(s) | selected index: {selected_idx[0]}\")\n", "for i, (x, y) in enumerate(markers):\n", " flag = \" ← selected\" if i == selected_idx[0] else \"\"\n", " print(f\" {i+1:3d}. X={x:.3f} Y={y:.3f}{flag}\")" ] }, { "cell_type": "markdown", "id": "290d91d3", "metadata": {}, "source": [ "## 7 — Export markers as numpy array" ] }, { "cell_type": "code", "execution_count": 11, "id": "0c965409", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Shape: (2, 2)\n", "[[ 2.95636527 20.45272155]\n", " [22.02393162 25.67233468]]\n" ] } ], "source": [ "if markers:\n", " pts = np.array(markers, dtype=float)\n", " print(f\"Shape: {pts.shape}\")\n", " print(pts)\n", "else:\n", " print(\"No markers yet.\")" ] }, { "cell_type": "markdown", "id": "005f7173", "metadata": {}, "source": [ "## 8 — Deactivate and clean up" ] }, { "cell_type": "code", "execution_count": 12, "id": "eb39c92f", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : End mark points\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Plugin deactivated and unregistered.\n" ] } ], "source": [ "viewer.end_action('End mark points')\n", "viewer.unregister_action(ACTION_ID)\n", "print(\"Plugin deactivated and unregistered.\")" ] }, { "cell_type": "markdown", "id": "b3f9aaa5", "metadata": {}, "source": [ "## Appendix — Available hooks summary\n", "\n", "```python\n", "viewer.register_action(\n", " 'my action',\n", "\n", " # (viewer, MouseContext) -> None\n", " rdown_handler = ..., # right mouse-button pressed\n", " motion_handler = ..., # mouse moved (any button state)\n", " ldown_handler = ..., # left mouse-button pressed\n", "\n", " # (viewer, KeyboardSnapshot) -> bool\n", " # Return True to consume the key event (prevents viewer default).\n", " key_handler = ...,\n", "\n", " # (viewer) -> None — raw OpenGL, world-coordinate projection active\n", " # Called: after all _plotting() layers, before sculpt cursor + UI overlays.\n", " paint_handler = ...,\n", ")\n", "```\n", "\n", "### `MouseContext` attributes\n", "| Attribute | Type | Description |\n", "|---|---|---|\n", "| `x`, `y` | `float` | Raw world coordinates |\n", "| `x_snap`, `y_snap` | `float` | Grid-snapped world coordinates |\n", "| `x_pixel`, `y_pixel` | `int` | Screen pixel coordinates |\n", "| `shift`, `ctrl`, `alt` | `bool` | Keyboard modifiers |\n", "| `left_down`, `right_down`, `middle_down` | `bool` | Button states during motion |\n", "| `pressure` | `float` | Stylus pressure `[0, 1]` (1.0 = mouse) |\n", "\n", "### `KeyboardSnapshot` attributes\n", "| Attribute | Type | Description |\n", "|---|---|---|\n", "| `key_code` | `int` | `ord('A')`, `wx.WXK_F1`, etc. |\n", "| `ctrl`, `shift`, `alt` | `bool` | Modifiers |\n", "| `is_down` | `bool` | `True` = key-down event |\n", "| `held` | `frozenset[int]` | All non-modifier keys currently held (polled) |" ] } ], "metadata": { "kernelspec": { "display_name": "python311", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.9" } }, "nbformat": 4, "nbformat_minor": 5 }