Companion factory — sélection interactive de géométries

Ce notebook présente cinq compagnons préconfigurés disponibles dans wolfhece._companion_factory. Chacun répond à un besoin courant de saisie interactive sur la carte.

Compagnon

Résultat

Cas d’usage typique

PointPickerCompanion

comp.points — liste de (x, y)

Saisie de points de contrôle, nœuds d’un maillage

PolylineCompanion

comp.vertices — liste ordonnée

Profil en long, tracé d’axe, chemin d’écoulement

MultiPolylineCompanion

comp.polylines — liste de listes

Réseau de drains, contours de sections

MultiPolylineZonesCompanion

comp.zones — objet Zones

Idem, mais stockage dans une couche vectorielle réutilisable

PolygonCompanion

comp.polygons — liste de polygones fermés

Zones d’inondation, bassins versants

Interaction commune à tous

Touche / action

Effet

Clic droit

Ajoute un sommet

Entrée

Valide la géométrie en cours (≥ 2 pts pour lignes, ≥ 3 pour polygones)

Esc

Annule / arrête

comp.stop()

Désactive l’action (données conservées)

comp.destroy()

Désactive + désenregistre tous les handlers

Usage minimal (one-liner)

from wolfhece._companion_factory import point_picker

comp = point_picker(viewer)   # crée, enregistre ET active en une ligne
# … clic droit sur la carte …
print(comp.points)
comp.destroy()

1 — Démarrage wx

[1]:
import sys
%gui wx

2 — Créer le viewer

[2]:
from wolfhece.PyDraw import WolfMapViewer

viewer = WolfMapViewer(None, title="Companion factory", w=1200, h=800)
viewer.Show()
INFO:root:Importing wolfhece modules
INFO:root:wolfhece modules imported
[2]:
False

3 — Importer la factory

[3]:
from wolfhece._companion_factory import (
    PointPickerCompanion,
    PolylineCompanion,
    MultiPolylineCompanion,
    MultiPolylineZonesCompanion,
    PolygonCompanion,
    point_picker,
    polyline,
    multi_polyline,
    multi_polyline_zones,
    polygon,
)
print('Factory importée.')

Factory importée.

A — PointPickerCompanion : sélection de points isolés

Clic droit → ajoute un point.
Clic gauche → sélectionne le point le plus proche (croix en or).
Ctrl+Z → annule le dernier point.
Esc → désactive.

Données disponibles après interaction : comp.points (liste de (x, y)).

[4]:
# One-liner : crée + enregistre + active
comp_pts = point_picker(viewer)
print(comp_pts)
INFO:root:ACTION : Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop
<PointPickerCompanion ns='pointpickercompanion' menu=not built actions=[pointpickercompanion.pick]>
[5]:
# Inspecter les points collectés (exécutez cette cellule à tout moment)
print(f"{len(comp_pts.points)} point(s)  |  sélectionné : {comp_pts.selected}")
for i, (x, y) in enumerate(comp_pts.points, 1):
    flag = ' ←' if i - 1 == comp_pts.selected else ''
    print(f"  {i:3d}.  X={x:.3f}   Y={y:.3f}{flag}")
5 point(s)  |  sélectionné : 4
    1.  X=4.767   Y=25.779
    2.  X=23.782   Y=27.803
    3.  X=11.531   Y=20.346
    4.  X=27.510   Y=15.446
    5.  X=33.795   Y=28.229 ←
[6]:
# Convertir en tableau numpy pour traitement
import numpy as np
if comp_pts.points:
    pts_array = np.array(comp_pts.points)   # shape (N, 2)
    print(f"Shape : {pts_array.shape}")
    print(pts_array)
Shape : (5, 2)
[[ 4.76725146 25.7788574 ]
 [23.78155646 27.80278902]
 [11.53144399 20.34619883]
 [27.50985155 15.44615385]
 [33.79469186 28.22887989]]
[7]:
comp_pts.stop()
comp_pts.destroy()
print('PointPickerCompanion détruit.')
INFO:root:ACTION :
PointPickerCompanion détruit.

B — PolylineCompanion : saisie d’une polyligne

Clic droit → ajoute un sommet.
Entrée → valide (au moins 2 sommets requis).
Esc → annule et efface tous les sommets.

Données disponibles après Enter : comp.vertices (liste ordonnée de sommets), comp.finished (True).

[8]:
comp_line = polyline(viewer)
print(comp_line)
INFO:root:ACTION : Right-click: add vertex | Enter: finalise (≥2 pts) | Esc: cancel
<PolylineCompanion ns='polylinecompanion' menu=not built actions=[polylinecompanion.line]>
[9]:
print(f"finished={comp_line.finished}  |  {len(comp_line.vertices)} sommet(s)")
for i, (x, y) in enumerate(comp_line.vertices, 1):
    print(f"  {i:3d}.  X={x:.3f}   Y={y:.3f}")
finished=True  |  10 sommet(s)
    1.  X=-3.009   Y=15.606
    2.  X=-3.435   Y=30.093
    3.  X=9.188   Y=31.212
    4.  X=22.397   Y=30.945
    5.  X=22.823   Y=23.861
    6.  X=7.910   Y=14.115
    7.  X=19.574   Y=10.546
    8.  X=31.291   Y=17.257
    9.  X=34.008   Y=35.472
   10.  X=10.573   Y=37.177
[10]:
# Calculer la longueur totale de la polyligne
if comp_line.finished and len(comp_line.vertices) >= 2:
    import numpy as np
    v = np.array(comp_line.vertices)
    segments = np.hypot(np.diff(v[:, 0]), np.diff(v[:, 1]))
    print(f"Longueur totale : {segments.sum():.3f} m")
    print(f"Nb segments     : {len(segments)}")
Longueur totale : 132.904 m
Nb segments     : 9
[11]:
comp_line.destroy()
print('PolylineCompanion détruit.')
PolylineCompanion détruit.

C — MultiPolylineCompanion : plusieurs polylignes en une session

Clic droit → ajoute un sommet à la ligne en cours.
Entrée → finalise la ligne courante (≥ 2 pts) et commence une nouvelle.
Esc → abandonne la ligne en cours et désactive l’action.

Couleurs : lignes finalisées en bleu, ligne en cours en orange.

Données : comp.polylines (liste de listes de sommets).

[ ]:
comp_lines = multi_polyline(viewer)
print(comp_lines)
[ ]:
print(f"{len(comp_lines.polylines)} polyligne(s) finalisée(s)")
for j, line in enumerate(comp_lines.polylines, 1):
    print(f"  Ligne {j} : {len(line)} sommets")
    for i, (x, y) in enumerate(line, 1):
        print(f"    {i:3d}.  X={x:.3f}   Y={y:.3f}")
[ ]:
comp_lines.destroy()
print('MultiPolylineCompanion détruit.')

D — MultiPolylineZonesCompanion : polylignes dans un objet Zones

Identique à MultiPolylineCompanion pour l’interaction, mais chaque polyligne acceptée est immédiatement stockée dans un objet `Zones <../../api/wolfhece.pyvertexvectors.rst>`__ — hiérarchie Zoneszonevector — et, par défaut, ajoutée automatiquement au viewer comme couche vectorielle.

Clic droit → ajoute un sommet à la ligne en cours.
Entrée → finalise la ligne courante (≥ 2 pts) et l’enregistre dans comp.zones.
Esc → abandonne la ligne en cours ; les lignes déjà validées sont conservées.

Données : comp.zones (objet Zones, None avant la première acceptation).

Avantage clé : l’objet Zones peut être exporté, manipulé, ou transmis à d’autres outils wolfhece directement sans conversion.

[4]:
# One-liner : crée, enregistre et active
# Par défaut auto_attach=True → le Zones est ajouté au viewer à la 1re ligne acceptée
comp_zones = multi_polyline_zones(viewer, zones_id='demo_zones')
print(comp_zones)

INFO:root:ACTION : Right-click: vertex | Enter: accept line (≥2 pts) | Esc: stop
<MultiPolylineZonesCompanion ns='multipolylinezonescompanion' menu=not built actions=[multipolylinezonescompanion.mplz]>
[5]:
# Inspecter les lignes dans le Zones (exécutez à tout moment)
if comp_zones.zones is not None:
    z_obj = comp_zones.zones
    print(f"{z_obj.nbzones} zone(s) enregistrée(s)")
    for j, z in enumerate(z_obj.myzones, 1):
        for v in z.myvectors:
            pts = [(vert.x, vert.y) for vert in v.myvertices]
            print(f"  Zone {j} '{z.myname}' — {len(pts)} sommet(s)")
            for i, (x, y) in enumerate(pts, 1):
                print(f"    {i:3d}.  X={x:.3f}   Y={y:.3f}")
else:
    print("Aucune ligne acceptée pour l'instant (appuyez sur Entrée après ≥ 2 clics).")

1 zone(s) enregistrée(s)
  Zone 1 'zone_001' — 10 sommet(s)
      1.  X=1.625   Y=24.820
      2.  X=9.614   Y=28.122
      3.  X=18.935   Y=28.442
      4.  X=25.113   Y=24.501
      5.  X=23.995   Y=21.571
      6.  X=12.756   Y=20.666
      7.  X=16.964   Y=15.926
      8.  X=28.362   Y=13.475
      9.  X=33.582   Y=29.827
     10.  X=18.242   Y=34.301
[6]:
# Convertir le Zones en tableau numpy (toutes les lignes concaténées)
import numpy as np
if comp_zones.zones is not None:
    all_pts = [
        (vert.x, vert.y)
        for z in comp_zones.zones.myzones
        for v in z.myvectors
        for vert in v.myvertices
    ]
    if all_pts:
        arr = np.array(all_pts)
        print(f"Shape : {arr.shape}")
        print(arr)

Shape : (10, 2)
[[ 1.62483131 24.82015295]
 [ 9.61403509 28.12235717]
 [18.93477283 28.44192533]
 [25.11309042 24.5005848 ]
 [23.99460189 21.57121008]
 [12.75645524 20.66576698]
 [16.96410256 15.92550607]
 [28.36203329 13.47548358]
 [33.58164642 29.82672065]
 [18.24237517 34.30067476]]
[7]:
comp_zones.destroy()
print('MultiPolylineZonesCompanion détruit.')

MultiPolylineZonesCompanion détruit.

E — PolygonCompanion : saisie de polygones fermés

Clic droit → ajoute un sommet au polygone en cours.
Entrée → ferme et finalise le polygone (≥ 3 sommets) ; une nouvelle saisie peut commencer.
Esc → annule le polygone en cours (les polygones déjà validés sont conservés).

Couleurs : polygones validés en vert, polygone en cours en orange.

Données : comp.polygons (liste de listes ; le segment de fermeture dernier→premier est implicite).

[ ]:
comp_poly = polygon(viewer)
print(comp_poly)
[ ]:
print(f"{len(comp_poly.polygons)} polygone(s) finalisé(s)")
for j, poly in enumerate(comp_poly.polygons, 1):
    print(f"  Polygone {j} : {len(poly)} sommets")
    for i, (x, y) in enumerate(poly, 1):
        print(f"    {i:3d}.  X={x:.3f}   Y={y:.3f}")
[ ]:
# Calculer l'aire de chaque polygone (formule de Shoelace)
import numpy as np
for j, poly in enumerate(comp_poly.polygons, 1):
    v = np.array(poly)
    x, y = v[:, 0], v[:, 1]
    area = 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))
    print(f"Polygone {j} : aire = {area:.3f} m²")
[ ]:
comp_poly.destroy()
print('PolygonCompanion détruit.')

F — Personnalisation : surcharger les attributs de classe

Les couleurs et tailles sont des attributs de classe — on les modifie sans réécrire aucune méthode.

[ ]:
class BluePicker(PointPickerCompanion):
    """PointPickerCompanion avec des croix bleues et des marqueurs plus grands."""
    COLOR_NORMAL   = (0.1, 0.4, 1.0, 1.0)   # bleu
    COLOR_SELECTED = (1.0, 1.0, 0.0, 1.0)   # jaune
    CROSS_FRACTION = 0.018                   # croix plus grandes


class ThickPolyline(PolylineCompanion):
    """PolylineCompanion en rouge épais."""
    COLOR_LINE  = (1.0, 0.1, 0.1, 1.0)   # rouge
    LINE_WIDTH  = 3.5


# Utilisation identique aux classes de base :
comp_blue = BluePicker(viewer)
comp_blue.start()
print(comp_blue)

# Pour nettoyer avant la prochaine démonstration :
comp_blue.destroy()

G — Extension : ajouter un comportement au clic

Pour ajouter de la logique sans réécrire toute la classe, on surcharge uniquement le handler privé correspondant.

[ ]:
class AnnotatedPicker(PointPickerCompanion):
    """Affiche un label numéroté dans la barre de statut après chaque clic."""

    def _rdown(self, viewer, ctx):
        super()._rdown(viewer, ctx)   # comportement de base (ajoute le point)
        n = len(self.points)
        self._set_status(f"Point #{n}  X={ctx.x_snap:.3f}  Y={ctx.y_snap:.3f}")


comp_ann = AnnotatedPicker(viewer)
comp_ann.start()
print("Cliquez sur la carte — la barre de statut s'actualise à chaque point.")
# Nettoyage :
# comp_ann.destroy()
[ ]:
comp_ann.destroy()
print('Nettoyage terminé.')

Récapitulatif — référence rapide

from wolfhece._companion_factory import (
    point_picker,          # → comp.points        : list[(x, y)]
    polyline,              # → comp.vertices       : list[(x, y)],  comp.finished : bool
    multi_polyline,        # → comp.polylines      : list[list[(x, y)]]
    multi_polyline_zones,  # → comp.zones          : Zones  (hiérarchie Zones→zone→vector)
    polygon,               # → comp.polygons       : list[list[(x, y)]]
)

# Démarrage
comp = multi_polyline_zones(viewer, zones_id='mon_tracé', auto_attach=True)

# Inspection
print(comp.zones)              # objet Zones (None avant la 1re ligne acceptée)
print(comp.zones.nbzones)      # nombre de zones enregistrées

# Arrêt
comp.stop()                    # désactive, données conservées
comp.destroy()                 # désactive + désenregistre

# Attachement manuel (quand auto_attach=False)
comp2 = multi_polyline_zones(viewer, auto_attach=False)
# … interaction …
comp2.attach_zones()           # ajoute le Zones au viewer

# Remise à zéro
comp.clear_zones()             # vide le Zones et remet zones=None

Personnalisation sans réécriture

Attribut de classe

Type

Rôle

COLOR_NORMAL

tuple(r,g,b,a)

Couleur des éléments non sélectionnés

COLOR_SELECTED

tuple

Couleur de l’élément sélectionné

COLOR_LINE / COLOR_DONE / COLOR_CURRENT

tuple

Couleurs de trait

CROSS_FRACTION

float

Taille des marqueurs (fraction de la largeur visible)

LINE_WIDTH

float

Épaisseur du trait OpenGL (pixels)

Pour aller plus loin

Objectif

Ressource

Comprendre la structure de base

plugin_companion_menuless.ipynb

Écrire un compagnon complet avec menu

plugin_companion_example.ipynb

Tous les hooks (motion, key, paint)

plugin_companion_complete.ipynb

Superposer deux compagnons

plugin_companion_overload.ipynb