{ "cells": [ { "cell_type": "markdown", "id": "6b24db27", "metadata": {}, "source": [ "# Companion factory — sélection interactive de géométries\n", "\n", "Ce notebook présente cinq compagnons préconfigurés disponibles dans `wolfhece._companion_factory`.\n", "Chacun répond à un besoin courant de saisie interactive sur la carte.\n", "\n", "| Compagnon | Résultat | Cas d'usage typique |\n", "|---|---|---|\n", "| `PointPickerCompanion` | `comp.points` — liste de `(x, y)` | Saisie de points de contrôle, nœuds d'un maillage |\n", "| `PolylineCompanion` | `comp.vertices` — liste ordonnée | Profil en long, tracé d'axe, chemin d'écoulement |\n", "| `MultiPolylineCompanion` | `comp.polylines` — liste de listes | Réseau de drains, contours de sections |\n", "| `MultiPolylineZonesCompanion` | `comp.zones` — objet `Zones` | Idem, mais stockage dans une couche vectorielle réutilisable |\n", "| `PolygonCompanion` | `comp.polygons` — liste de polygones fermés | Zones d'inondation, bassins versants |\n", "\n", "## Interaction commune à tous\n", "\n", "| Touche / action | Effet |\n", "|---|---|\n", "| Clic droit | Ajoute un sommet |\n", "| **Entrée** | Valide la géométrie en cours (≥ 2 pts pour lignes, ≥ 3 pour polygones) |\n", "| **Esc** | Annule / arrête |\n", "| `comp.stop()` | Désactive l'action (données conservées) |\n", "| `comp.destroy()` | Désactive + désenregistre tous les handlers |\n", "\n", "## Usage minimal (one-liner)\n", "\n", "```python\n", "from wolfhece._companion_factory import point_picker\n", "\n", "comp = point_picker(viewer) # crée, enregistre ET active en une ligne\n", "# … clic droit sur la carte …\n", "print(comp.points)\n", "comp.destroy()\n", "```\n" ] }, { "cell_type": "markdown", "id": "c9bfc4ef", "metadata": {}, "source": [ "## 1 — Démarrage wx" ] }, { "cell_type": "code", "execution_count": 1, "id": "95e328b7", "metadata": {}, "outputs": [], "source": [ "import sys\n", "%gui wx" ] }, { "cell_type": "markdown", "id": "0716382f", "metadata": {}, "source": [ "## 2 — Créer le viewer" ] }, { "cell_type": "code", "execution_count": 2, "id": "e8335b6b", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:Importing wolfhece modules\n", "INFO:root:wolfhece modules imported\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=\"Companion factory\", w=1200, h=800)\n", "viewer.Show()" ] }, { "cell_type": "markdown", "id": "903f9af9", "metadata": {}, "source": [ "## 3 — Importer la factory" ] }, { "cell_type": "code", "execution_count": 3, "id": "dc77b86e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Factory importée.\n" ] } ], "source": [ "from wolfhece._companion_factory import (\n", " PointPickerCompanion,\n", " PolylineCompanion,\n", " MultiPolylineCompanion,\n", " MultiPolylineZonesCompanion,\n", " PolygonCompanion,\n", " point_picker,\n", " polyline,\n", " multi_polyline,\n", " multi_polyline_zones,\n", " polygon,\n", ")\n", "print('Factory importée.')\n" ] }, { "cell_type": "markdown", "id": "a086d88a", "metadata": {}, "source": [ "---\n", "## A — `PointPickerCompanion` : sélection de points isolés\n", "\n", "**Clic droit** → ajoute un point. \n", "**Clic gauche** → sélectionne le point le plus proche (croix en or). \n", "**Ctrl+Z** → annule le dernier point. \n", "**Esc** → désactive.\n", "\n", "Données disponibles après interaction : `comp.points` (liste de `(x, y)`)." ] }, { "cell_type": "code", "execution_count": 4, "id": "2734b6e9", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "# One-liner : crée + enregistre + active\n", "comp_pts = point_picker(viewer)\n", "print(comp_pts)" ] }, { "cell_type": "code", "execution_count": 5, "id": "e99541e6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "5 point(s) | sélectionné : 4\n", " 1. X=4.767 Y=25.779\n", " 2. X=23.782 Y=27.803\n", " 3. X=11.531 Y=20.346\n", " 4. X=27.510 Y=15.446\n", " 5. X=33.795 Y=28.229 ←\n" ] } ], "source": [ "# Inspecter les points collectés (exécutez cette cellule à tout moment)\n", "print(f\"{len(comp_pts.points)} point(s) | sélectionné : {comp_pts.selected}\")\n", "for i, (x, y) in enumerate(comp_pts.points, 1):\n", " flag = ' ←' if i - 1 == comp_pts.selected else ''\n", " print(f\" {i:3d}. X={x:.3f} Y={y:.3f}{flag}\")" ] }, { "cell_type": "code", "execution_count": 6, "id": "dea73952", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Shape : (5, 2)\n", "[[ 4.76725146 25.7788574 ]\n", " [23.78155646 27.80278902]\n", " [11.53144399 20.34619883]\n", " [27.50985155 15.44615385]\n", " [33.79469186 28.22887989]]\n" ] } ], "source": [ "# Convertir en tableau numpy pour traitement\n", "import numpy as np\n", "if comp_pts.points:\n", " pts_array = np.array(comp_pts.points) # shape (N, 2)\n", " print(f\"Shape : {pts_array.shape}\")\n", " print(pts_array)" ] }, { "cell_type": "code", "execution_count": 7, "id": "fe588fac", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "PointPickerCompanion détruit.\n" ] } ], "source": [ "comp_pts.stop()\n", "comp_pts.destroy()\n", "print('PointPickerCompanion détruit.')" ] }, { "cell_type": "markdown", "id": "6e1ea5e5", "metadata": {}, "source": [ "---\n", "## B — `PolylineCompanion` : saisie d'une polyligne\n", "\n", "**Clic droit** → ajoute un sommet. \n", "**Entrée** → valide (au moins 2 sommets requis). \n", "**Esc** → annule et efface tous les sommets.\n", "\n", "Données disponibles après `Enter` : `comp.vertices` (liste ordonnée de sommets), `comp.finished` (`True`)." ] }, { "cell_type": "code", "execution_count": 8, "id": "9ae33c57", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : Right-click: add vertex | Enter: finalise (≥2 pts) | Esc: cancel\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "comp_line = polyline(viewer)\n", "print(comp_line)" ] }, { "cell_type": "code", "execution_count": 9, "id": "25aa5208", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "finished=True | 10 sommet(s)\n", " 1. X=-3.009 Y=15.606\n", " 2. X=-3.435 Y=30.093\n", " 3. X=9.188 Y=31.212\n", " 4. X=22.397 Y=30.945\n", " 5. X=22.823 Y=23.861\n", " 6. X=7.910 Y=14.115\n", " 7. X=19.574 Y=10.546\n", " 8. X=31.291 Y=17.257\n", " 9. X=34.008 Y=35.472\n", " 10. X=10.573 Y=37.177\n" ] } ], "source": [ "print(f\"finished={comp_line.finished} | {len(comp_line.vertices)} sommet(s)\")\n", "for i, (x, y) in enumerate(comp_line.vertices, 1):\n", " print(f\" {i:3d}. X={x:.3f} Y={y:.3f}\")" ] }, { "cell_type": "code", "execution_count": 10, "id": "eb64dc73", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Longueur totale : 132.904 m\n", "Nb segments : 9\n" ] } ], "source": [ "# Calculer la longueur totale de la polyligne\n", "if comp_line.finished and len(comp_line.vertices) >= 2:\n", " import numpy as np\n", " v = np.array(comp_line.vertices)\n", " segments = np.hypot(np.diff(v[:, 0]), np.diff(v[:, 1]))\n", " print(f\"Longueur totale : {segments.sum():.3f} m\")\n", " print(f\"Nb segments : {len(segments)}\")" ] }, { "cell_type": "code", "execution_count": 11, "id": "264737f6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "PolylineCompanion détruit.\n" ] } ], "source": [ "comp_line.destroy()\n", "print('PolylineCompanion détruit.')" ] }, { "cell_type": "markdown", "id": "3e30cffa", "metadata": {}, "source": [ "---\n", "## C — `MultiPolylineCompanion` : plusieurs polylignes en une session\n", "\n", "**Clic droit** → ajoute un sommet à la ligne en cours. \n", "**Entrée** → finalise la ligne courante (≥ 2 pts) et commence une nouvelle. \n", "**Esc** → abandonne la ligne en cours et désactive l'action.\n", "\n", "Couleurs : lignes finalisées en **bleu**, ligne en cours en **orange**.\n", "\n", "Données : `comp.polylines` (liste de listes de sommets)." ] }, { "cell_type": "code", "execution_count": null, "id": "d14ffdc1", "metadata": {}, "outputs": [], "source": [ "comp_lines = multi_polyline(viewer)\n", "print(comp_lines)" ] }, { "cell_type": "code", "execution_count": null, "id": "dc9c1d6e", "metadata": {}, "outputs": [], "source": [ "print(f\"{len(comp_lines.polylines)} polyligne(s) finalisée(s)\")\n", "for j, line in enumerate(comp_lines.polylines, 1):\n", " print(f\" Ligne {j} : {len(line)} sommets\")\n", " for i, (x, y) in enumerate(line, 1):\n", " print(f\" {i:3d}. X={x:.3f} Y={y:.3f}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "738cae8b", "metadata": {}, "outputs": [], "source": [ "comp_lines.destroy()\n", "print('MultiPolylineCompanion détruit.')" ] }, { "cell_type": "markdown", "id": "03274581", "metadata": {}, "source": [ "---\n", "## D — `MultiPolylineZonesCompanion` : polylignes dans un objet `Zones`\n", "\n", "Identique à `MultiPolylineCompanion` pour l'interaction, mais chaque polyligne\n", "acceptée est immédiatement stockée dans un objet\n", "[`Zones`](../../api/wolfhece.pyvertexvectors.rst) —\n", "hiérarchie `Zones` → `zone` → `vector` — et, par défaut, ajoutée automatiquement\n", "au viewer comme couche vectorielle.\n", "\n", "**Clic droit** → ajoute un sommet à la ligne en cours. \n", "**Entrée** → finalise la ligne courante (≥ 2 pts) et l'enregistre dans `comp.zones`. \n", "**Esc** → abandonne la ligne en cours ; les lignes déjà validées sont conservées.\n", "\n", "Données : `comp.zones` (objet `Zones`, `None` avant la première acceptation).\n", "\n", "> **Avantage clé** : l'objet `Zones` peut être exporté, manipulé, ou transmis à\n", "> d'autres outils wolfhece directement sans conversion.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "ceeced3f", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "INFO:root:ACTION : Right-click: vertex | Enter: accept line (≥2 pts) | Esc: stop\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "# One-liner : crée, enregistre et active\n", "# Par défaut auto_attach=True → le Zones est ajouté au viewer à la 1re ligne acceptée\n", "comp_zones = multi_polyline_zones(viewer, zones_id='demo_zones')\n", "print(comp_zones)\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "b0342a91", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 zone(s) enregistrée(s)\n", " Zone 1 'zone_001' — 10 sommet(s)\n", " 1. X=1.625 Y=24.820\n", " 2. X=9.614 Y=28.122\n", " 3. X=18.935 Y=28.442\n", " 4. X=25.113 Y=24.501\n", " 5. X=23.995 Y=21.571\n", " 6. X=12.756 Y=20.666\n", " 7. X=16.964 Y=15.926\n", " 8. X=28.362 Y=13.475\n", " 9. X=33.582 Y=29.827\n", " 10. X=18.242 Y=34.301\n" ] } ], "source": [ "# Inspecter les lignes dans le Zones (exécutez à tout moment)\n", "if comp_zones.zones is not None:\n", " z_obj = comp_zones.zones\n", " print(f\"{z_obj.nbzones} zone(s) enregistrée(s)\")\n", " for j, z in enumerate(z_obj.myzones, 1):\n", " for v in z.myvectors:\n", " pts = [(vert.x, vert.y) for vert in v.myvertices]\n", " print(f\" Zone {j} '{z.myname}' — {len(pts)} sommet(s)\")\n", " for i, (x, y) in enumerate(pts, 1):\n", " print(f\" {i:3d}. X={x:.3f} Y={y:.3f}\")\n", "else:\n", " print(\"Aucune ligne acceptée pour l'instant (appuyez sur Entrée après ≥ 2 clics).\")\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "24745145", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Shape : (10, 2)\n", "[[ 1.62483131 24.82015295]\n", " [ 9.61403509 28.12235717]\n", " [18.93477283 28.44192533]\n", " [25.11309042 24.5005848 ]\n", " [23.99460189 21.57121008]\n", " [12.75645524 20.66576698]\n", " [16.96410256 15.92550607]\n", " [28.36203329 13.47548358]\n", " [33.58164642 29.82672065]\n", " [18.24237517 34.30067476]]\n" ] } ], "source": [ "# Convertir le Zones en tableau numpy (toutes les lignes concaténées)\n", "import numpy as np\n", "if comp_zones.zones is not None:\n", " all_pts = [\n", " (vert.x, vert.y)\n", " for z in comp_zones.zones.myzones\n", " for v in z.myvectors\n", " for vert in v.myvertices\n", " ]\n", " if all_pts:\n", " arr = np.array(all_pts)\n", " print(f\"Shape : {arr.shape}\")\n", " print(arr)\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "77ce20bb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MultiPolylineZonesCompanion détruit.\n" ] } ], "source": [ "comp_zones.destroy()\n", "print('MultiPolylineZonesCompanion détruit.')\n" ] }, { "cell_type": "markdown", "id": "8d14b09c", "metadata": {}, "source": [ "---\n", "## E — `PolygonCompanion` : saisie de polygones fermés\n", "\n", "**Clic droit** → ajoute un sommet au polygone en cours. \n", "**Entrée** → ferme et finalise le polygone (≥ 3 sommets) ; une nouvelle saisie peut commencer. \n", "**Esc** → annule le polygone en cours (les polygones déjà validés sont conservés).\n", "\n", "Couleurs : polygones validés en **vert**, polygone en cours en **orange**.\n", "\n", "Données : `comp.polygons` (liste de listes ; le segment de fermeture dernier→premier est implicite).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "4d4da157", "metadata": {}, "outputs": [], "source": [ "comp_poly = polygon(viewer)\n", "print(comp_poly)" ] }, { "cell_type": "code", "execution_count": null, "id": "e528790c", "metadata": {}, "outputs": [], "source": [ "print(f\"{len(comp_poly.polygons)} polygone(s) finalisé(s)\")\n", "for j, poly in enumerate(comp_poly.polygons, 1):\n", " print(f\" Polygone {j} : {len(poly)} sommets\")\n", " for i, (x, y) in enumerate(poly, 1):\n", " print(f\" {i:3d}. X={x:.3f} Y={y:.3f}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "096c3f84", "metadata": {}, "outputs": [], "source": [ "# Calculer l'aire de chaque polygone (formule de Shoelace)\n", "import numpy as np\n", "for j, poly in enumerate(comp_poly.polygons, 1):\n", " v = np.array(poly)\n", " x, y = v[:, 0], v[:, 1]\n", " area = 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))\n", " print(f\"Polygone {j} : aire = {area:.3f} m²\")" ] }, { "cell_type": "code", "execution_count": null, "id": "e717130d", "metadata": {}, "outputs": [], "source": [ "comp_poly.destroy()\n", "print('PolygonCompanion détruit.')" ] }, { "cell_type": "markdown", "id": "ea685be2", "metadata": {}, "source": [ "---\n", "## F — Personnalisation : surcharger les attributs de classe\n", "\n", "Les couleurs et tailles sont des attributs de classe — on les modifie sans réécrire aucune méthode.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "3de34451", "metadata": {}, "outputs": [], "source": [ "class BluePicker(PointPickerCompanion):\n", " \"\"\"PointPickerCompanion avec des croix bleues et des marqueurs plus grands.\"\"\"\n", " COLOR_NORMAL = (0.1, 0.4, 1.0, 1.0) # bleu\n", " COLOR_SELECTED = (1.0, 1.0, 0.0, 1.0) # jaune\n", " CROSS_FRACTION = 0.018 # croix plus grandes\n", "\n", "\n", "class ThickPolyline(PolylineCompanion):\n", " \"\"\"PolylineCompanion en rouge épais.\"\"\"\n", " COLOR_LINE = (1.0, 0.1, 0.1, 1.0) # rouge\n", " LINE_WIDTH = 3.5\n", "\n", "\n", "# Utilisation identique aux classes de base :\n", "comp_blue = BluePicker(viewer)\n", "comp_blue.start()\n", "print(comp_blue)\n", "\n", "# Pour nettoyer avant la prochaine démonstration :\n", "comp_blue.destroy()" ] }, { "cell_type": "markdown", "id": "0d16c98e", "metadata": {}, "source": [ "## G — Extension : ajouter un comportement au clic\n", "\n", "Pour ajouter de la logique sans réécrire toute la classe, on surcharge uniquement le handler privé correspondant.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "e3d13fb0", "metadata": {}, "outputs": [], "source": [ "class AnnotatedPicker(PointPickerCompanion):\n", " \"\"\"Affiche un label numéroté dans la barre de statut après chaque clic.\"\"\"\n", "\n", " def _rdown(self, viewer, ctx):\n", " super()._rdown(viewer, ctx) # comportement de base (ajoute le point)\n", " n = len(self.points)\n", " self._set_status(f\"Point #{n} X={ctx.x_snap:.3f} Y={ctx.y_snap:.3f}\")\n", "\n", "\n", "comp_ann = AnnotatedPicker(viewer)\n", "comp_ann.start()\n", "print(\"Cliquez sur la carte — la barre de statut s'actualise à chaque point.\")\n", "# Nettoyage :\n", "# comp_ann.destroy()" ] }, { "cell_type": "code", "execution_count": null, "id": "d6772164", "metadata": {}, "outputs": [], "source": [ "comp_ann.destroy()\n", "print('Nettoyage terminé.')" ] }, { "cell_type": "markdown", "id": "264c02ff", "metadata": {}, "source": [ "---\n", "## Récapitulatif — référence rapide\n", "\n", "```python\n", "from wolfhece._companion_factory import (\n", " point_picker, # → comp.points : list[(x, y)]\n", " polyline, # → comp.vertices : list[(x, y)], comp.finished : bool\n", " multi_polyline, # → comp.polylines : list[list[(x, y)]]\n", " multi_polyline_zones, # → comp.zones : Zones (hiérarchie Zones→zone→vector)\n", " polygon, # → comp.polygons : list[list[(x, y)]]\n", ")\n", "\n", "# Démarrage\n", "comp = multi_polyline_zones(viewer, zones_id='mon_tracé', auto_attach=True)\n", "\n", "# Inspection\n", "print(comp.zones) # objet Zones (None avant la 1re ligne acceptée)\n", "print(comp.zones.nbzones) # nombre de zones enregistrées\n", "\n", "# Arrêt\n", "comp.stop() # désactive, données conservées\n", "comp.destroy() # désactive + désenregistre\n", "\n", "# Attachement manuel (quand auto_attach=False)\n", "comp2 = multi_polyline_zones(viewer, auto_attach=False)\n", "# … interaction …\n", "comp2.attach_zones() # ajoute le Zones au viewer\n", "\n", "# Remise à zéro\n", "comp.clear_zones() # vide le Zones et remet zones=None\n", "```\n", "\n", "### Personnalisation sans réécriture\n", "\n", "| Attribut de classe | Type | Rôle |\n", "|---|---|---|\n", "| `COLOR_NORMAL` | `tuple(r,g,b,a)` | Couleur des éléments non sélectionnés |\n", "| `COLOR_SELECTED` | `tuple` | Couleur de l'élément sélectionné |\n", "| `COLOR_LINE` / `COLOR_DONE` / `COLOR_CURRENT` | `tuple` | Couleurs de trait |\n", "| `CROSS_FRACTION` | `float` | Taille des marqueurs (fraction de la largeur visible) |\n", "| `LINE_WIDTH` | `float` | Épaisseur du trait OpenGL (pixels) |\n", "\n", "### Pour aller plus loin\n", "\n", "| Objectif | Ressource |\n", "|---|---|\n", "| Comprendre la structure de base | `plugin_companion_menuless.ipynb` |\n", "| Écrire un compagnon complet avec menu | `plugin_companion_example.ipynb` |\n", "| Tous les hooks (motion, key, paint) | `plugin_companion_complete.ipynb` |\n", "| Superposer deux compagnons | `plugin_companion_overload.ipynb` |\n" ] } ], "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 }