{ "cells": [ { "cell_type": "markdown", "id": "bb2e3cca", "metadata": {}, "source": [ "# Companion plugin — complete reference\n", "\n", "This notebook mirrors **`plugin_action_complete.ipynb`** using `AbstractCompanion`.\n", "\n", "All five event hooks are demonstrated:\n", "\n", "| Hook | Companion method signature | Event |\n", "|---|---|---|\n", "| `rdown` | `_xxx(self, viewer, ctx: MouseContext)` | Right mouse-button press |\n", "| `motion` | `_xxx(self, viewer, ctx: MouseContext)` | Mouse motion |\n", "| `ldown` | `_xxx(self, viewer, ctx: MouseContext)` | Left mouse-button press |\n", "| `key` | `_xxx(self, viewer, kb: KeyboardSnapshot) -> bool` | Key press |\n", "| `paint` | `_xxx(self, viewer)` | OpenGL paint hook |\n", "\n", "The companion built here lets the user:\n", "- **Right-click** → add a marker at the snapped cursor\n", "- **Left-click** → select / highlight the nearest marker\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\n", "\n", "### What the companion adds vs the raw API\n", "- All state is encapsulated — `self.markers`, `self._selected`\n", "- Menu is declared once in `menu_build()` — **no `import wx`** needed\n", "- `self._set_status(msg)` replaces `viewer.set_statusbar_text(msg)`\n", "- `self._force_redraw()` replaces `viewer.Paint()`\n", "- `self.destroy()` unregisters every action automatically" ] }, { "cell_type": "markdown", "id": "c812abb6", "metadata": {}, "source": [ "## 1 — wx startup" ] }, { "cell_type": "code", "execution_count": null, "id": "e6bc4cc6", "metadata": {}, "outputs": [], "source": [ "import sys\n", "%gui wx" ] }, { "cell_type": "markdown", "id": "e40a18b7", "metadata": {}, "source": [ "## 2 — Create the MapViewer" ] }, { "cell_type": "code", "execution_count": null, "id": "58b1de78", "metadata": {}, "outputs": [], "source": [ "from wolfhece.PyDraw import WolfMapViewer\n", "\n", "viewer = WolfMapViewer(None, title=\"Companion — complete demo\", w=1200, h=800)\n", "viewer.Show()" ] }, { "cell_type": "markdown", "id": "6f932b27", "metadata": {}, "source": [ "## 3 — The MarkerCompanion\n", "\n", "All state, menu entries, and event handlers live in a single class.\n", "Notice that **there is no `import wx`** anywhere — the companion helpers\n", "abstract it away." ] }, { "cell_type": "code", "execution_count": null, "id": "4bb59f94", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "from wolfhece._menu_companion_abc import (\n", " AbstractCompanion,\n", " ActionItem,\n", " SEPARATOR,\n", ")\n", "from wolfhece._viewer_plugin_handlers import MouseContext, KeyboardSnapshot\n", "\n", "\n", "class MarkerCompanion(AbstractCompanion):\n", " \"\"\"Places and manages coloured markers on the map.\"\"\"\n", "\n", " # ---------------------------------------------------------------\n", " # State\n", " # ---------------------------------------------------------------\n", " def __init__(self, viewer):\n", " super().__init__(viewer, namespace='markers')\n", " #: List of (x, y) world-coordinate pairs — read from the notebook.\n", " self.markers: list[tuple[float, float]] = []\n", " #: Index of the currently selected marker (-1 = none).\n", " self._selected: int = -1\n", "\n", " @property\n", " def selected(self) -> int:\n", " \"\"\"Index of the currently selected marker (-1 = none).\"\"\"\n", " return self._selected\n", "\n", " # ---------------------------------------------------------------\n", " # Menu\n", " # ---------------------------------------------------------------\n", " def menu_build(self) -> None:\n", " self._build_menu('Markers', [\n", " ActionItem('Start', self._on_start,\n", " 'Activate: right-click = add, left-click = select'),\n", " ActionItem('Stop', self._on_stop,\n", " 'Deactivate the marker action'),\n", " SEPARATOR,\n", " ActionItem('Show markers', self._on_show, 'Display all markers'),\n", " ActionItem('Clear markers', self._on_clear, 'Delete all markers'),\n", " ])\n", " # One register_action call covers all five hooks.\n", " self._register_action(\n", " self._action_id('place'),\n", " rdown = self._rdown_add,\n", " ldown = self._ldown_select,\n", " motion = self._motion_coords,\n", " key = self._key_handler,\n", " paint = self._paint_markers, # ← wires up the OpenGL overlay\n", " )\n", "\n", " # ---------------------------------------------------------------\n", " # Public API — callable directly from the notebook.\n", " # ---------------------------------------------------------------\n", " def start(self) -> None:\n", " \"\"\"Activate: right-click = add, left-click = select.\"\"\"\n", " self._start_action(\n", " self._action_id('place'),\n", " 'Right-click: add | Left-click: select | Ctrl+Z: undo | Ctrl+C: clear',\n", " )\n", "\n", " def stop(self) -> None:\n", " \"\"\"Deactivate the marker action.\"\"\"\n", " self._end_action()\n", "\n", " # ---------------------------------------------------------------\n", " # Menu-item callbacks\n", " # ---------------------------------------------------------------\n", " def _on_start(self, _event) -> None:\n", " self.start()\n", "\n", " def _on_stop(self, _event) -> None:\n", " self.stop()\n", "\n", " def _on_show(self, _event) -> None:\n", " if not self.markers:\n", " self._show_info(\"No markers yet.\", title=\"Markers\")\n", " return\n", " lines = \"\\n\".join(\n", " f\" {i+1:3d}. X={x:.3f} Y={y:.3f}\"\n", " for i, (x, y) in enumerate(self.markers)\n", " )\n", " self._show_info(f\"{len(self.markers)} marker(s):\\n{lines}\", title=\"Markers\")\n", "\n", " def _on_clear(self, _event) -> None:\n", " if not self.markers:\n", " self._show_info(\"No markers to clear.\", title=\"Markers\")\n", " return\n", " if self._confirm(f\"Delete all {len(self.markers)} marker(s)?\", default='no'):\n", " self.markers.clear()\n", " self._selected = -1\n", " self._force_redraw()\n", " print(\"All markers cleared.\")\n", "\n", " # ---------------------------------------------------------------\n", " # rdown handler — right-click adds a marker\n", " # ---------------------------------------------------------------\n", " def _rdown_add(self, viewer, ctx: MouseContext) -> None:\n", " self.markers.append((ctx.x_snap, ctx.y_snap))\n", " n = len(self.markers)\n", " print(f\"[Marker #{n}] X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}\")\n", " self._force_redraw()\n", "\n", " # ---------------------------------------------------------------\n", " # ldown handler — left-click selects the nearest marker\n", " # ---------------------------------------------------------------\n", " def _ldown_select(self, viewer, ctx: MouseContext) -> None:\n", " if not self.markers:\n", " return\n", " pts = np.array(self.markers)\n", " dists = np.hypot(pts[:, 0] - ctx.x, pts[:, 1] - ctx.y)\n", " self._selected = int(np.argmin(dists))\n", " x, y = self.markers[self._selected]\n", " print(f\"Selected marker #{self._selected + 1} X={x:.3f} Y={y:.3f} \"\n", " f\"(dist={dists[self._selected]:.1f})\")\n", " self._force_redraw()\n", "\n", " # ---------------------------------------------------------------\n", " # motion handler — live coordinate display in the status bar\n", " # ---------------------------------------------------------------\n", " def _motion_coords(self, viewer, ctx: MouseContext) -> None:\n", " self._set_status(\n", " f\"X={ctx.x:.2f} Y={ctx.y:.2f} | {len(self.markers)} marker(s)\"\n", " )\n", "\n", " # ---------------------------------------------------------------\n", " # key handler — Ctrl+Z undo, Ctrl+C clear\n", " # Return True to consume the event (prevents default viewer behaviour).\n", " # ---------------------------------------------------------------\n", " def _key_handler(self, viewer, kb: KeyboardSnapshot) -> bool:\n", " if not kb.ctrl:\n", " return False\n", "\n", " if kb.key_code == ord('Z'):\n", " if self.markers:\n", " removed = self.markers.pop()\n", " if self._selected >= len(self.markers):\n", " self._selected = len(self.markers) - 1\n", " print(f\"Undo — removed X={removed[0]:.3f} Y={removed[1]:.3f}\")\n", " self._force_redraw()\n", " return True\n", "\n", " if kb.key_code == ord('C'):\n", " self.markers.clear()\n", " self._selected = -1\n", " print(\"All markers cleared.\")\n", " self._force_redraw()\n", " return True\n", "\n", " return False\n", "\n", " # ---------------------------------------------------------------\n", " # paint hook — draw crosses via the ABC helper (no raw GL needed)\n", " # Called after all data layers, before the viewer's UI overlays.\n", " # ---------------------------------------------------------------\n", " def _paint_markers(self, viewer) -> None:\n", " half = self._viewport_fraction(0.008) # scales with zoom\n", " self._draw_crosses(\n", " self.markers,\n", " half,\n", " selected_idx=self._selected,\n", " )" ] }, { "cell_type": "markdown", "id": "5ef89508", "metadata": {}, "source": [ "## 4 — Attach the menu and activate" ] }, { "cell_type": "code", "execution_count": null, "id": "3dd2d465", "metadata": {}, "outputs": [], "source": [ "mc = MarkerCompanion(viewer)\n", "mc.menu_build()\n", "\n", "# Start the action directly from the notebook.\n", "mc.start()\n", "\n", "print(mc)" ] }, { "cell_type": "markdown", "id": "3e987bb4", "metadata": {}, "source": [ "## 5 — Inspect the markers" ] }, { "cell_type": "code", "execution_count": null, "id": "d185baa6", "metadata": {}, "outputs": [], "source": [ "print(f\"{len(mc.markers)} marker(s) | selected index: {mc.selected}\")\n", "for i, (x, y) in enumerate(mc.markers):\n", " flag = \" ← selected\" if i == mc.selected else \"\"\n", " print(f\" {i+1:3d}. X={x:.3f} Y={y:.3f}{flag}\")" ] }, { "cell_type": "markdown", "id": "e21dea53", "metadata": {}, "source": [ "## 6 — Export markers as a numpy array" ] }, { "cell_type": "code", "execution_count": null, "id": "33d5d127", "metadata": {}, "outputs": [], "source": [ "if mc.markers:\n", " pts = np.array(mc.markers, dtype=float)\n", " print(f\"Shape: {pts.shape}\")\n", " print(pts)\n", "else:\n", " print(\"No markers yet.\")" ] }, { "cell_type": "markdown", "id": "3b06ed30", "metadata": {}, "source": [ "## 7 — Cleanup" ] }, { "cell_type": "code", "execution_count": null, "id": "42d3cf91", "metadata": {}, "outputs": [], "source": [ "mc.stop()\n", "mc.destroy() # unregisters 'markers.place' automatically\n", "\n", "print(\"Companion destroyed.\")\n", "print(mc)" ] }, { "cell_type": "markdown", "id": "97c4ea95", "metadata": {}, "source": [ "## Appendix — helper reference\n", "\n", "### Public API (call from notebook cells)\n", "\n", "| Method | Description |\n", "|---|---|\n", "| `companion.start()` | Activate the action |\n", "| `companion.stop()` | Deactivate the action |\n", "| `companion.destroy()` | Unregister all actions and remove the menu |\n", "\n", "### Protected helpers (use *inside* the companion class only)\n", "\n", "| Companion method | Equivalent raw call | When to use |\n", "|---|---|---|\n", "| `self._set_status(msg)` | `viewer.set_statusbar_text(msg)` | motion handler, live feedback |\n", "| `self._force_redraw()` | `viewer.Paint()` | after changing drawn data |\n", "| `self._start_action(id, hint)` | `viewer.start_action(id, hint)` | inside `start()` |\n", "| `self._end_action()` | `viewer.end_action()` | inside `stop()` — resets action, active_vertex, fires sculpt/assets callbacks |\n", "\n", "### OpenGL paint helpers (no `import OpenGL` required)\n", "\n", "Wire the paint hook with `paint=self._paint_xxx` in `_register_action`, then call these helpers from inside the method body.\n", "\n", "| Method | Description |\n", "|---|---|\n", "| `self._viewport_fraction(f=0.008)` | Returns `f × viewport_width` in world units — use as cross/dot size that scales with zoom |\n", "| `self._draw_crosses(points, half_size, color, *, selected_idx, selected_color, line_width)` | Cross markers at each `(x, y)` point; optional single selection highlight |\n", "| `self._draw_polyline(points, color, *, closed, line_width)` | Connected line string; `closed=True` adds a closing segment |\n", "| `self._draw_segments(segments, color, *, line_width)` | Individual segments as `(x1, y1, x2, y2)` tuples |\n", "| `self._draw_points(points, color, *, point_size)` | Filled GL dots at each point |\n", "\n", "**Minimal paint handler example:**\n", "\n", "```python\n", "def _paint_markers(self, viewer) -> None:\n", " half = self._viewport_fraction(0.008)\n", " self._draw_crosses(self.points, half, selected_idx=self._selected)\n", "```\n", "\n", "### Dialog helpers (no `import wx` required)\n", "\n", "| Method | Description |\n", "|---|---|\n", "| `self._show_info(msg, title='')` | Information dialog |\n", "| `self._show_error(msg, title='')` | Error dialog |\n", "| `self._confirm(msg, default='yes')` | Yes/No confirmation — returns `bool` |\n", "| `self._ask_text(msg, default='')` | Free-text prompt — returns `str \\| None` |\n", "| `self._ask_float(msg, default=0)` | Float prompt — returns `float \\| None` |\n", "| `self._ask_integer(msg, min_value=, max_value=)` | Integer spin dialog |\n", "| `self._ask_single_choice(msg, title, choices)` | List picker — returns `str \\| None` |\n", "| `self._ask_file_open(msg, wildcard=)` | File-open dialog |\n", "| `self._ask_file_save(msg, wildcard=)` | File-save dialog |\n", "| `self._ask_directory(msg)` | Directory chooser |\n", "\n", "### Event handler signatures\n", "\n", "```python\n", "def _rdown_xxx(self, viewer, ctx: MouseContext) -> None: ...\n", "def _ldown_xxx(self, viewer, ctx: MouseContext) -> None: ...\n", "def _motion_xxx(self, viewer, ctx: MouseContext) -> None: ...\n", "def _key_xxx(self, viewer, kb: KeyboardSnapshot) -> bool: ...\n", "def _paint_xxx(self, viewer) -> None: ... # wire with paint=self._paint_xxx\n", "```\n", "\n", "### `MouseContext` quick reference\n", "\n", "| Attribute | Description |\n", "|---|---|\n", "| `ctx.x`, `ctx.y` | Raw world coordinates |\n", "| `ctx.x_snap`, `ctx.y_snap` | Grid-snapped world coordinates |\n", "| `ctx.x_pixel`, `ctx.y_pixel` | Screen pixel coordinates |\n", "| `ctx.shift`, `ctx.ctrl`, `ctx.alt` | Keyboard modifiers |\n", "\n", "### `KeyboardSnapshot` quick reference\n", "\n", "| Attribute | Description |\n", "|---|---|\n", "| `kb.key_code` | `ord('A')`, `wx.WXK_F1`, etc. |\n", "| `kb.ctrl`, `kb.shift`, `kb.alt` | Modifiers |\n", "| `kb.is_down` | `True` = key-down event |" ] } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }