Companion factory — interactive geometry selection

This notebook presents five preconfigured companions available in wolfhece.plugins.factory. Each one addresses a common need for interactive input on the map.

Companion

Result

Typical use case

PointPickerCompanion

comp.points — list of (x, y)

Control point input, mesh nodes

PolylineCompanion

comp.vertices — ordered list

Longitudinal profile, axis tracing, flow path

MultiPolylineCompanion

comp.polylines — list of lists

Drain network, section outlines

MultiPolylineZonesCompanion

comp.zonesZones object

Same idea, but stored in a reusable vector layer

PolygonCompanion

comp.polygons — list of closed polygons

Flood zones, catchments

Common interaction for all companions

Key / action

Effect

Right click

Adds a vertex

Enter

Validates the geometry in progress (>= 2 points for lines, >= 3 for polygons)

Esc

Cancels / stops

comp.stop()

Deactivates the action (data preserved)

comp.destroy()

Deactivates and unregisters all handlers

Minimal usage (one-liner)

from wolfhece.plugins.factory import point_picker

comp = point_picker(viewer)   # creates, registers, AND activates in one line
# ... right click on the map ...
print(comp.points)
comp.destroy()

1 — wx startup

[1]:
import sys
%gui wx

2 — Create the 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
INFO:root:Plugin manager init: skipping auto-discovery because no global configuration is available yet.
[2]:
False

3 — Import the factory

[3]:
from wolfhece.plugins.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: selecting individual points

Right click → adds a point.
Left click → selects the nearest point (gold cross).
Ctrl+Z → undoes the last point.
Esc → deactivates.

Available data after interaction: comp.points (list of (x, y)).

[4]:
# One-liner: create + register + activate
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]:
# Inspect collected points (run this cell at any time)
print(f"{len(comp_pts.points)} point(s)  |  selected: {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}")
4 point(s)  |  selected: 3
    1.  X=11.052   Y=26.525
    2.  X=28.788   Y=26.578
    3.  X=16.645   Y=18.642
    4.  X=26.871   Y=13.635 ←
[6]:
# Convert to a NumPy array for processing
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: (4, 2)
[[11.05209177 26.52451642]
 [28.78812416 26.57777778]
 [16.64453441 18.64183536]
 [26.87071525 13.63526766]]
[7]:
comp_pts.stop()
comp_pts.destroy()
print('PointPickerCompanion destroyed.')
INFO:root:ACTION : End of action
PointPickerCompanion destroyed.

B — PolylineCompanion: polyline input

Right click → adds a vertex.
Enter → validates (at least 2 vertices required).
Esc → cancels and clears all vertices.

Data available after Enter: comp.vertices (ordered list of vertices), comp.finished (True).

[5]:
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]>
[6]:
print(f"finished={comp_line.finished}  |  {len(comp_line.vertices)} vertex(s)")
for i, (x, y) in enumerate(comp_line.vertices, 1):
    print(f"  {i:3d}.  X={x:.3f}   Y={y:.3f}")
finished=True  |  6 vertex(s)
    1.  X=3.276   Y=29.507
    2.  X=20.000   Y=24.341
    3.  X=4.607   Y=15.819
    4.  X=21.971   Y=12.357
    5.  X=29.374   Y=22.317
    6.  X=22.716   Y=32.117
[7]:
# Compute the total polyline length
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"Total length: {segments.sum():.3f} m")
    print(f"Number of segments: {len(segments)}")
Total length: 77.061 m
Number of segments: 5
[8]:
comp_line.destroy()
print('PolylineCompanion destroyed.')
PolylineCompanion destroyed.

C — MultiPolylineCompanion: multiple polylines in one session

Right click → adds a vertex to the current line.
Enter → finalizes the current line (>= 2 points) and starts a new one.
Esc → abandons the current line and deactivates the action.

Colors: finalized lines in blue, current line in orange.

Data: comp.polylines (list of lists of vertices).

[9]:
comp_lines = multi_polyline(viewer)
print(comp_lines)
INFO:root:ACTION : Right-click: vertex | Enter: next line | Esc: finish session
<MultiPolylineCompanion ns='multipolylinecompanion' menu=not built actions=[multipolylinecompanion.lines]>
[10]:
print(f"{len(comp_lines.polylines)} finalized polyline(s)")
for j, line in enumerate(comp_lines.polylines, 1):
    print(f"  Line {j}: {len(line)} vertices")
    for i, (x, y) in enumerate(line, 1):
        print(f"    {i:3d}.  X={x:.3f}   Y={y:.3f}")
0 finalized polyline(s)
[11]:
comp_lines.destroy()
print('MultiPolylineCompanion destroyed.')
MultiPolylineCompanion destroyed.

D — MultiPolylineZonesCompanion: polylines in a Zones object

Same interaction as MultiPolylineCompanion, but each accepted polyline is stored immediately in a `Zones <../../api/wolfhece.pyvertexvectors.rst>`__ object

  • Zoneszonevector hierarchy - and, by default, automatically added to the viewer as a vector layer.

Right click → adds a vertex to the current line.
Enter → finalizes the current line (>= 2 points) and stores it in comp.zones.
Esc → abandons the current line; already validated lines are kept.

Data: comp.zones (Zones object, None before the first accepted line).

Key advantage: the Zones object can be exported, manipulated, or passed to other wolfhece tools directly without conversion.

[4]:
# One-liner: create, register, and activate
# By default auto_attach=True, so the Zones is added to the viewer when the first line is accepted
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]:
# Inspect the lines in the Zones object (run at any time)
if comp_zones.zones is not None:
    z_obj = comp_zones.zones
    print(f"{z_obj.nbzones} registered zone(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)} vertex(s)")
            for i, (x, y) in enumerate(pts, 1):
                print(f"    {i:3d}.  X={x:.3f}   Y={y:.3f}")
else:
    print("No line has been accepted yet (press Enter after >= 2 clicks).")

No line has been accepted yet (press Enter after >= 2 clicks).
[6]:
# Convert the Zones object to a NumPy array (all lines concatenated)
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)

[7]:
comp_zones.destroy()
print('MultiPolylineZonesCompanion destroyed.')

MultiPolylineZonesCompanion destroyed.

E — PolygonCompanion: closed polygon input

Right click → adds a vertex to the current polygon.
Enter → closes and finalizes the polygon (>= 3 vertices); a new input can begin.
Esc → cancels the current polygon (already validated polygons are kept).

Colors: validated polygons in green, current polygon in orange.

Data: comp.polygons (list of lists; the closing segment last→first is implicit).

[4]:
comp_poly = polygon(viewer)
print(comp_poly)
INFO:root:ACTION : Right-click: vertex | Enter: close polygon (≥3 pts) | Esc: cancel current
<PolygonCompanion ns='polygoncompanion' menu=not built actions=[polygoncompanion.poly]>
[ ]:
print(f"{len(comp_poly.polygons)} finalized polygon(s)")
for j, poly in enumerate(comp_poly.polygons, 1):
    print(f"  Polygon {j}: {len(poly)} vertices")
    for i, (x, y) in enumerate(poly, 1):
        print(f"    {i:3d}.  X={x:.3f}   Y={y:.3f}")
[ ]:
# Compute the area of each polygon (Shoelace formula)
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"Polygon {j}: area = {area:.3f} m²")
[5]:
comp_poly.destroy()
print('PolygonCompanion destroyed.')
PolygonCompanion destroyed.

F — Customization: override class attributes

Colors and sizes are class attributes, so you can change them without rewriting any method.

[8]:
class BluePicker(PointPickerCompanion):
    """PointPickerCompanion with blue crosses and larger markers."""
    COLOR_NORMAL   = (0.1, 0.4, 1.0, 1.0)   # blue
    COLOR_SELECTED = (1.0, 1.0, 0.0, 1.0)   # yellow
    CROSS_FRACTION = 0.018                   # larger crosses


class ThickPolyline(PolylineCompanion):
    """Thick red PolylineCompanion."""
    COLOR_LINE  = (1.0, 0.1, 0.1, 1.0)   # red
    LINE_WIDTH  = 3.5


comp_blue = BluePicker()
viewer.attach_companion(comp_blue)
comp_blue.start()
print(comp_blue)
INFO:root:ACTION : Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop
<BluePicker ns='bluepicker' menu=not built actions=[bluepicker.pick]>
[9]:
# Cleanup:
comp_blue.destroy()

G — Extension: add click behavior

To add logic without rewriting the whole class, override only the corresponding private handler.

[10]:
class AnnotatedPicker(PointPickerCompanion):
    """Displays a numbered label in the status bar after each click."""

    def _rdown(self, ctx):
        super()._rdown(ctx)   # add the point
        n = len(self.points)
        self.proxy.set_status(f'Point #{n}  X={ctx.x_snap:.3f}  Y={ctx.y_snap:.3f}')


comp_ann = AnnotatedPicker()
viewer.attach_companion(comp_ann)
comp_ann.start()
print("Click on the map — the status bar updates after each point.")
# comp_ann.destroy()
INFO:root:ACTION : Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop
Click on the map — the status bar updates after each point.
[ ]:
comp_ann.destroy()
print('Cleanup complete.')
Cleanup complete.
The Kernel crashed while executing code in the current cell or a previous cell.

Please review the code in the cell(s) to identify a possible cause of the failure.

Click <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info.

View Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details.

Quick reference summary

from wolfhece.plugins.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  (Zones→zone→vector hierarchy)
    polygon,               # → comp.polygons       : list[list[(x, y)]]
)

# Startup
comp = multi_polyline_zones(viewer, zones_id='my_trace', auto_attach=True)

# Inspection
print(comp.zones)              # Zones object (None before the first accepted line)
print(comp.zones.nbzones)      # number of registered zones

# Stop
comp.stop()                    # deactivates, data preserved
comp.destroy()                 # deactivates + unregisters

# Manual attachment (when auto_attach=False)
comp2 = multi_polyline_zones(viewer, auto_attach=False)
# ... interaction ...
comp2.attach_zones()           # adds the Zones to the viewer

# Reset
comp.clear_zones()             # clears the Zones and sets zones=None

Customize without rewriting

Class attribute

Type

Role

COLOR_NORMAL

tuple(r,g,b,a)

Color of unselected elements

COLOR_SELECTED

tuple

Color of the selected element