"""Pre-built companion classes for common interactive-selection patterns.
All four classes are ready to use as-is or to subclass. Override the
``COLOR_*`` / ``*_FRACTION`` class attributes to change the visual style
without writing any method code.
Quick-start
-----------
::
from wolfhece._companion_factory import point_picker, polyline, multi_polyline, polygon
# One-liner: create + register + activate
comp = point_picker(viewer)
# … right-click on the map …
print(comp.points)
comp.destroy()
Interaction summary
-------------------
+-------------------------+------------------+-------------------+------------------+
| Companion | Add vertex | Finish / accept | Cancel / stop |
+=========================+==================+===================+==================+
| PointPickerCompanion | Right-click | (already done) | Esc or stop() |
+-------------------------+------------------+-------------------+------------------+
| PolylineCompanion | Right-click | Enter (≥ 2 pts) | Esc (discard) |
+-------------------------+------------------+-------------------+------------------+
| MultiPolylineCompanion | Right-click | Enter (≥ 2 pts) | Esc stops action |
+-------------------------+------------------+-------------------+------------------+
| PolygonCompanion | Right-click | Enter (≥ 3 pts) | Esc (discard) |
+-------------------------+------------------+-------------------+------------------+
All companions:
* ``left-click`` selects the nearest already-placed vertex (PointPickerCompanion only)
* ``Ctrl+Z`` undoes the last vertex (PointPickerCompanion only)
* ``comp.stop()`` deactivates the action; collected data is preserved
* ``comp.destroy()`` deactivates + unregisters all handlers
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
_logger = logging.getLogger(__name__)
import numpy as np
from wolfhece._menu_companion_abc import AbstractCompanion
from wolfhece._viewer_plugin_handlers import MouseContext, KeyboardSnapshot
if TYPE_CHECKING:
from wolfhece.PyDraw import WolfMapViewer
__all__ = [
'PointPickerCompanion',
'PolylineCompanion',
'MultiPolylineCompanion',
'MultiPolylineZonesCompanion',
'PolygonCompanion',
'point_picker',
'polyline',
'multi_polyline',
'multi_polyline_zones',
'polygon',
]
# wx key codes — numeric values avoid importing wx at module load time.
_WXK_RETURN = 13
_WXK_ESCAPE = 27
# ---------------------------------------------------------------------------
# PointPickerCompanion
# ---------------------------------------------------------------------------
[docs]
class PointPickerCompanion(AbstractCompanion):
"""Collect isolated (x, y) points via right-click.
Interaction
-----------
* **Right-click** — add a point at the snapped cursor position
* **Left-click** — select the nearest point (highlighted in gold)
* **Ctrl+Z** — remove the last point
* **Esc** — deactivate the action; collected points are preserved
Attributes
----------
points : list[tuple[float, float]]
Collected world-coordinate pairs (snapped).
selected : int
Index of the highlighted point (-1 = none).
Customisation
-------------
Override the class attributes to change colours/size without subclassing::
class MyPicker(PointPickerCompanion):
COLOR_NORMAL = (0.0, 0.6, 1.0, 1.0) # blue
COLOR_SELECTED = (1.0, 1.0, 0.0, 1.0) # yellow
CROSS_FRACTION = 0.015
"""
#: RGBA colour for unselected points.
[docs]
COLOR_NORMAL: tuple = (1.0, 0.0, 0.0, 1.0)
#: RGBA colour for the selected point.
[docs]
COLOR_SELECTED: tuple = (1.0, 0.8, 0.0, 1.0)
#: Cross arm-length as a fraction of viewport width.
[docs]
CROSS_FRACTION: float = 0.01
def __init__(self, viewer: 'WolfMapViewer') -> None:
super().__init__(viewer)
#: Collected world-coordinate pairs.
[docs]
self.points: list[tuple[float, float]] = []
#: Index of the currently highlighted point (-1 = none).
[docs]
self.selected: int = -1
[docs]
def start(self) -> None:
"""Activate the point-picker action."""
if self._action_id('pick') not in self._registered_action_ids:
self._register_action(
self._action_id('pick'),
rdown=self._rdown,
ldown=self._ldown,
key=self._key,
paint=self._paint,
)
self._start_action(
self._action_id('pick'),
'Right-click: add | Left-click: select nearest | Ctrl+Z: undo | Esc: stop',
)
# -- handlers ------------------------------------------------------------
[docs]
def _rdown(self, viewer, ctx: MouseContext) -> None:
self.points.append((ctx.x_snap, ctx.y_snap))
self.selected = len(self.points) - 1
self._force_redraw()
[docs]
def _ldown(self, viewer, ctx: MouseContext) -> None:
if not self.points:
return
pts = np.array(self.points)
dists = np.hypot(pts[:, 0] - ctx.x, pts[:, 1] - ctx.y)
self.selected = int(np.argmin(dists))
self._force_redraw()
[docs]
def _key(self, viewer, kb: KeyboardSnapshot) -> bool:
if kb.key_code == _WXK_ESCAPE:
self.stop()
return True
if kb.ctrl and kb.key_code == ord('Z'):
if self.points:
self.points.pop()
if self.selected >= len(self.points):
self.selected = len(self.points) - 1
self._force_redraw()
return True
return False
[docs]
def _paint(self, viewer) -> None:
self._draw_crosses(
self.points,
self._viewport_fraction(self.CROSS_FRACTION),
color=self.COLOR_NORMAL,
selected_idx=self.selected,
selected_color=self.COLOR_SELECTED,
)
# ---------------------------------------------------------------------------
# PolylineCompanion
# ---------------------------------------------------------------------------
[docs]
class PolylineCompanion(AbstractCompanion):
"""Record a single polyline vertex by vertex.
Interaction
-----------
* **Right-click** — append a vertex
* **Enter / Return** — finalise the polyline (requires ≥ 2 vertices)
* **Esc** — discard all vertices and deactivate
Attributes
----------
vertices : list[tuple[float, float]]
Ordered vertices of the recorded polyline. Empty after a cancelled
interaction, populated after a successful Enter.
finished : bool
``True`` once Enter has been pressed with ≥ 2 vertices.
Customisation
-------------
Override ``COLOR_LINE``, ``COLOR_VERTEX``, ``CROSS_FRACTION``,
``LINE_WIDTH`` at the class level.
"""
#: RGBA colour for the line segments.
[docs]
COLOR_LINE: tuple = (0.0, 0.5, 1.0, 1.0)
#: RGBA colour for the vertex markers.
[docs]
COLOR_VERTEX: tuple = (1.0, 1.0, 1.0, 1.0)
#: Vertex cross size as a fraction of viewport width.
[docs]
CROSS_FRACTION: float = 0.008
#: OpenGL line width in pixels.
[docs]
LINE_WIDTH: float = 2.0
def __init__(self, viewer: 'WolfMapViewer') -> None:
super().__init__(viewer)
#: Ordered vertices of the polyline being collected.
[docs]
self.vertices: list[tuple[float, float]] = []
#: ``True`` after the user has pressed Enter to finalise.
[docs]
self.finished: bool = False
[docs]
def start(self) -> None:
"""Reset state and activate the polyline-recording action."""
self.vertices.clear()
self.finished = False
if self._action_id('line') not in self._registered_action_ids:
self._register_action(
self._action_id('line'),
rdown=self._rdown,
key=self._key,
paint=self._paint,
)
self._start_action(
self._action_id('line'),
'Right-click: add vertex | Enter: finalise (≥2 pts) | Esc: cancel',
)
# -- handlers ------------------------------------------------------------
[docs]
def _rdown(self, viewer, ctx: MouseContext) -> None:
self.vertices.append((ctx.x_snap, ctx.y_snap))
self._force_redraw()
[docs]
def _key(self, viewer, kb: KeyboardSnapshot) -> bool:
if kb.key_code == _WXK_RETURN:
if len(self.vertices) >= 2:
self.finished = True
else:
self._set_status('Need at least 2 vertices to finalise.')
self.stop()
return True
if kb.key_code == _WXK_ESCAPE:
self.vertices.clear()
self.finished = False
self.stop()
return True
return False
[docs]
def _paint(self, viewer) -> None:
if len(self.vertices) >= 2:
self._draw_polyline(
self.vertices, self.COLOR_LINE, line_width=self.LINE_WIDTH,
)
half = self._viewport_fraction(self.CROSS_FRACTION)
self._draw_crosses(self.vertices, half, color=self.COLOR_VERTEX)
# ---------------------------------------------------------------------------
# MultiPolylineCompanion
# ---------------------------------------------------------------------------
[docs]
class MultiPolylineCompanion(AbstractCompanion):
"""Record multiple polylines in a single session.
Interaction
-----------
* **Right-click** — append a vertex to the current in-progress line
* **Enter / Return** — finalise the current line (≥ 2 vertices) and start a new one
* **Esc** — discard the current in-progress line and deactivate;
already-finalised lines are preserved
Attributes
----------
polylines : list[list[tuple[float, float]]]
All finalised polylines.
current : list[tuple[float, float]]
Vertices of the line currently being drawn.
"""
#: RGBA colour for finalised lines.
[docs]
COLOR_DONE: tuple = (0.2, 0.6, 1.0, 1.0)
#: RGBA colour for the in-progress line.
[docs]
COLOR_CURRENT: tuple = (1.0, 0.5, 0.0, 1.0)
#: RGBA colour for vertex markers.
[docs]
COLOR_VERTEX: tuple = (1.0, 1.0, 1.0, 0.8)
#: Vertex cross size as a fraction of viewport width.
[docs]
CROSS_FRACTION: float = 0.007
#: OpenGL line width in pixels.
[docs]
LINE_WIDTH: float = 1.5
def __init__(self, viewer: 'WolfMapViewer') -> None:
super().__init__(viewer)
#: All finalised polylines.
[docs]
self.polylines: list[list[tuple[float, float]]] = []
#: Vertices of the line currently being built.
[docs]
self.current: list[tuple[float, float]] = []
[docs]
def start(self) -> None:
"""Reset state and activate the multi-polyline recording action."""
self.polylines.clear()
self.current.clear()
if self._action_id('lines') not in self._registered_action_ids:
self._register_action(
self._action_id('lines'),
rdown=self._rdown,
key=self._key,
paint=self._paint,
)
self._start_action(
self._action_id('lines'),
'Right-click: vertex | Enter: next line | Esc: finish session',
)
# -- handlers ------------------------------------------------------------
[docs]
def _rdown(self, viewer, ctx: MouseContext) -> None:
self.current.append((ctx.x_snap, ctx.y_snap))
self._force_redraw()
[docs]
def _key(self, viewer, kb: KeyboardSnapshot) -> bool:
if kb.key_code == _WXK_RETURN:
if len(self.current) >= 2:
self.polylines.append(list(self.current))
self.current.clear()
self._force_redraw()
n = len(self.polylines)
self._set_status(
f'Line {n} recorded — right-click to start line {n + 1} | Esc: finish'
)
else:
self._set_status('Need at least 2 vertices — keep clicking.')
return True
if kb.key_code == _WXK_ESCAPE:
self.current.clear()
self._force_redraw()
self.stop()
return True
return False
[docs]
def _paint(self, viewer) -> None:
for line in self.polylines:
if len(line) >= 2:
self._draw_polyline(line, self.COLOR_DONE, line_width=self.LINE_WIDTH)
if len(self.current) >= 2:
self._draw_polyline(self.current, self.COLOR_CURRENT, line_width=self.LINE_WIDTH)
half = self._viewport_fraction(self.CROSS_FRACTION)
all_pts = [p for line in self.polylines for p in line] + self.current
if all_pts:
self._draw_crosses(all_pts, half, color=self.COLOR_VERTEX)
# ---------------------------------------------------------------------------
# MultiPolylineZonesCompanion
# ---------------------------------------------------------------------------
[docs]
class MultiPolylineZonesCompanion(AbstractCompanion):
"""Record multiple polylines and store them in a
:class:`~wolfhece.pyvertexvectors.Zones` object.
Behaves like :class:`MultiPolylineCompanion` for the interaction, but
instead of keeping the results in a plain Python list each accepted
polyline is immediately written into a ``Zones`` → ``zone`` → ``vector``
hierarchy and — when *auto_attach* is enabled — added to the viewer so
that it appears in the layers panel and can be exported.
Interaction
-----------
* **Right-click** — append a vertex to the current in-progress line
* **Enter / Return** — finalise the current line (≥ 2 vertices); it is
added to the :attr:`zones` object as a new ``zone``
* **Esc** — discard the current in-progress vertices and stop the action;
already-finalised lines in :attr:`zones` are preserved
Attributes
----------
zones : Zones | None
The backing :class:`~wolfhece.pyvertexvectors.Zones` object.
``None`` until the first line is accepted. Once created it is the
same object that is attached to the viewer (when *auto_attach* is
``True``).
current : list[tuple[float, float]]
Vertices of the line currently being digitised.
Parameters
----------
viewer :
The map viewer.
zones_id : str
Identifier string for the :class:`~wolfhece.pyvertexvectors.Zones`
object. Defaults to ``'multi_polyline'``.
auto_attach : bool
When ``True`` (default) the :class:`~wolfhece.pyvertexvectors.Zones`
object is added to the viewer via ``viewer.add_object('vector', …)``
on the first accepted polyline.
zone_name_prefix : str
Prefix for the generated zone names (``zone_001``, ``zone_002``, …).
Customisation
-------------
Override the class attributes to change colours/line width::
class MyZonesCompanion(MultiPolylineZonesCompanion):
COLOR_DONE = (0.9, 0.2, 0.2, 1.0)
COLOR_CURRENT = (1.0, 0.8, 0.0, 1.0)
"""
#: RGBA colour for lines that have already been accepted into the Zones.
[docs]
COLOR_DONE: tuple = (0.2, 0.6, 1.0, 1.0)
#: RGBA colour for the line currently being digitised.
[docs]
COLOR_CURRENT: tuple = (1.0, 0.5, 0.0, 1.0)
#: RGBA colour for vertex cross markers.
[docs]
COLOR_VERTEX: tuple = (1.0, 1.0, 1.0, 0.8)
#: Vertex cross size as a fraction of viewport width.
[docs]
CROSS_FRACTION: float = 0.007
#: OpenGL line width in pixels.
[docs]
LINE_WIDTH: float = 1.5
def __init__(
self,
viewer: 'WolfMapViewer',
*,
zones_id: str = 'multi_polyline',
auto_attach: bool = True,
zone_name_prefix: str = 'zone',
) -> None:
super().__init__(viewer)
[docs]
self._zones_id = zones_id
[docs]
self._auto_attach = auto_attach
[docs]
self._zone_name_prefix = zone_name_prefix
#: Backing Zones object (None until the first line is accepted).
[docs]
self.zones: 'Zones | None' = None # type: ignore[name-defined]
#: Vertices of the line currently being digitised.
[docs]
self.current: list[tuple[float, float]] = []
# -- AbstractCompanion interface -----------------------------------------
[docs]
def start(self) -> None:
"""Activate the multi-polyline action.
Only *current* is cleared; any already-accepted lines in :attr:`zones`
are preserved so that the session can be resumed.
"""
self.current.clear()
if self._action_id('mplz') not in self._registered_action_ids:
self._register_action(
self._action_id('mplz'),
rdown=self._rdown,
key=self._key,
paint=self._paint,
)
self._start_action(
self._action_id('mplz'),
'Right-click: vertex | Enter: accept line (≥2 pts) | Esc: stop',
)
# -- public API ----------------------------------------------------------
[docs]
def attach_zones(self, id: str | None = None) -> 'Zones | None': # type: ignore[name-defined]
"""Manually add :attr:`zones` to the viewer.
This is a no-op when *auto_attach* is ``True`` (attachment already
happened) or when :attr:`zones` is ``None`` (no line accepted yet).
:param id: Override the id used when registering with the viewer
(defaults to the *zones_id* given at construction time).
:return: The :class:`~wolfhece.pyvertexvectors.Zones` object, or
``None`` if nothing has been digitised yet.
"""
if self.zones is None:
return None
oid = id if id is not None else self._zones_id
try:
self._viewer.add_object('vector', newobj=self.zones,
ToCheck=True, id=oid)
except Exception as exc:
_logger.warning("attach_zones: could not add object to viewer: %s", exc)
return self.zones
[docs]
def clear_zones(self) -> None:
"""Discard all accepted lines and reset :attr:`zones` to ``None``.
This does *not* remove the object from the viewer if it was already
attached — call ``viewer.get_obj_from_id(…)`` and remove it manually
if needed.
"""
self.zones = None
self.current.clear()
self._force_redraw()
# -- private helpers -----------------------------------------------------
[docs]
def _ensure_zones(self) -> 'Zones': # type: ignore[name-defined]
"""Return the Zones object, creating (and optionally attaching) it
on the first call."""
if self.zones is None:
from wolfhece.pyvertexvectors import Zones
self.zones = Zones(idx=self._zones_id, mapviewer=self._viewer)
if self._auto_attach:
try:
self._viewer.add_object(
'vector', newobj=self.zones,
ToCheck=True, id=self._zones_id,
)
except Exception as exc:
_logger.warning(
"_ensure_zones: could not attach Zones to viewer: %s", exc
)
return self.zones
[docs]
def _finalise_current(self) -> None:
"""Move *current* vertices into a new zone+vector inside :attr:`zones`."""
if len(self.current) < 2:
return
zones_obj = self._ensure_zones()
n = zones_obj.nbzones + 1
zone_name = f'{self._zone_name_prefix}_{n:03d}'
z = zones_obj._make_zone(name=zone_name, parent=zones_obj)
v = zones_obj._make_vector(
name=f'line_{n:03d}',
parentzone=z,
fromlist=list(self.current),
)
z.add_vector(v, forceparent=True)
zones_obj.add_zone(z, forceparent=True)
try:
zones_obj.find_minmax(update=True)
except Exception:
pass
self.current.clear()
self._force_redraw()
# -- event handlers ------------------------------------------------------
[docs]
def _rdown(self, viewer, ctx: MouseContext) -> None:
self.current.append((ctx.x_snap, ctx.y_snap))
self._force_redraw()
[docs]
def _key(self, viewer, kb: KeyboardSnapshot) -> bool:
if kb.key_code == _WXK_RETURN:
if len(self.current) >= 2:
self._finalise_current()
n = self.zones.nbzones if self.zones is not None else 0
self._set_status(
f'Line {n} accepted — right-click to start line {n + 1} | Esc: stop'
)
else:
self._set_status('Need at least 2 vertices — keep clicking.')
return True
if kb.key_code == _WXK_ESCAPE:
self.current.clear()
self._force_redraw()
self.stop()
return True
return False
[docs]
def _paint(self, viewer) -> None:
# Already-accepted lines — draw from the Zones object so the overlay
# stays in sync even if the viewer hasn't refreshed its own layer yet.
if self.zones is not None:
for z in self.zones.myzones:
for v in z.myvectors:
pts = [(vert.x, vert.y) for vert in v.myvertices]
if len(pts) >= 2:
self._draw_polyline(
pts, self.COLOR_DONE, line_width=self.LINE_WIDTH,
)
# In-progress line
if len(self.current) >= 2:
self._draw_polyline(
self.current, self.COLOR_CURRENT, line_width=self.LINE_WIDTH,
)
# Vertex markers
half = self._viewport_fraction(self.CROSS_FRACTION)
accepted_pts = [
(vert.x, vert.y)
for z in (self.zones.myzones if self.zones is not None else [])
for v in z.myvectors
for vert in v.myvertices
]
all_pts = accepted_pts + self.current
if all_pts:
self._draw_crosses(all_pts, half, color=self.COLOR_VERTEX)
# ---------------------------------------------------------------------------
# PolygonCompanion
# ---------------------------------------------------------------------------
[docs]
class PolygonCompanion(AbstractCompanion):
"""Record one or more closed polygons.
Interaction
-----------
* **Right-click** — append a vertex to the current polygon
* **Enter / Return** — close and finalise the current polygon (≥ 3 vertices)
* **Esc** — discard the current in-progress polygon; finalised polygons are kept
Attributes
----------
polygons : list[list[tuple[float, float]]]
All finalised polygons. The closing segment (last → first vertex) is
implicit — it is drawn but *not* stored as a duplicate vertex.
current : list[tuple[float, float]]
Vertices of the polygon currently being drawn.
"""
#: RGBA colour for finalised (closed) polygons.
[docs]
COLOR_DONE: tuple = (0.0, 0.8, 0.2, 1.0)
#: RGBA colour for the in-progress polygon.
[docs]
COLOR_CURRENT: tuple = (1.0, 0.5, 0.0, 1.0)
#: RGBA colour for vertex markers.
[docs]
COLOR_VERTEX: tuple = (1.0, 1.0, 1.0, 0.8)
#: Vertex cross size as a fraction of viewport width.
[docs]
CROSS_FRACTION: float = 0.007
#: OpenGL line width in pixels.
[docs]
LINE_WIDTH: float = 1.5
def __init__(self, viewer: 'WolfMapViewer') -> None:
super().__init__(viewer)
#: All finalised polygons (closing segment is implicit).
[docs]
self.polygons: list[list[tuple[float, float]]] = []
#: Vertices of the polygon currently being built.
[docs]
self.current: list[tuple[float, float]] = []
[docs]
def start(self) -> None:
"""Reset state and activate the polygon-recording action."""
self.polygons.clear()
self.current.clear()
if self._action_id('poly') not in self._registered_action_ids:
self._register_action(
self._action_id('poly'),
rdown=self._rdown,
key=self._key,
paint=self._paint,
)
self._start_action(
self._action_id('poly'),
'Right-click: vertex | Enter: close polygon (≥3 pts) | Esc: cancel current',
)
# -- handlers ------------------------------------------------------------
[docs]
def _rdown(self, viewer, ctx: MouseContext) -> None:
self.current.append((ctx.x_snap, ctx.y_snap))
self._force_redraw()
[docs]
def _key(self, viewer, kb: KeyboardSnapshot) -> bool:
if kb.key_code == _WXK_RETURN:
if len(self.current) >= 3:
self.polygons.append(list(self.current))
self.current.clear()
self._force_redraw()
n = len(self.polygons)
self._set_status(
f'Polygon {n} closed — right-click to start polygon {n + 1} | Esc: stop'
)
else:
self._set_status('Need at least 3 vertices to close a polygon.')
return True
if kb.key_code == _WXK_ESCAPE:
self.current.clear()
self._force_redraw()
return True
return False
[docs]
def _paint(self, viewer) -> None:
for poly in self.polygons:
if len(poly) >= 3:
self._draw_polyline(
poly, self.COLOR_DONE, closed=True, line_width=self.LINE_WIDTH,
)
if len(self.current) >= 2:
self._draw_polyline(self.current, self.COLOR_CURRENT, line_width=self.LINE_WIDTH)
half = self._viewport_fraction(self.CROSS_FRACTION)
all_pts = [p for poly in self.polygons for p in poly] + self.current
if all_pts:
self._draw_crosses(all_pts, half, color=self.COLOR_VERTEX)
# ---------------------------------------------------------------------------
# Convenience factory functions
# ---------------------------------------------------------------------------
[docs]
def point_picker(viewer: 'WolfMapViewer', **kwargs) -> PointPickerCompanion:
"""Create, register and activate a :class:`PointPickerCompanion`.
:param viewer: The :class:`~wolfhece.PyDraw.WolfMapViewer` instance.
:param kwargs: Forwarded to :class:`PointPickerCompanion` constructor
(e.g. ``namespace=``, ``dialogs=``).
:return: The activated companion.
"""
c = PointPickerCompanion(viewer, **kwargs)
c.start()
return c
[docs]
def polyline(viewer: 'WolfMapViewer', **kwargs) -> PolylineCompanion:
"""Create, register and activate a :class:`PolylineCompanion`.
:param viewer: The :class:`~wolfhece.PyDraw.WolfMapViewer` instance.
:param kwargs: Forwarded to the constructor.
:return: The activated companion.
"""
c = PolylineCompanion(viewer, **kwargs)
c.start()
return c
[docs]
def multi_polyline(viewer: 'WolfMapViewer', **kwargs) -> MultiPolylineCompanion:
"""Create, register and activate a :class:`MultiPolylineCompanion`.
:param viewer: The :class:`~wolfhece.PyDraw.WolfMapViewer` instance.
:param kwargs: Forwarded to the constructor.
:return: The activated companion.
"""
c = MultiPolylineCompanion(viewer, **kwargs)
c.start()
return c
[docs]
def multi_polyline_zones(
viewer: 'WolfMapViewer',
zones_id: str = 'multi_polyline',
auto_attach: bool = True,
**kwargs,
) -> 'MultiPolylineZonesCompanion':
"""Create, register and activate a :class:`MultiPolylineZonesCompanion`.
:param viewer: The :class:`~wolfhece.PyDraw.WolfMapViewer` instance.
:param zones_id: Identifier of the :class:`~wolfhece.pyvertexvectors.Zones`
object that will be added to the viewer.
:param auto_attach: When ``True`` (default) the :class:`~wolfhece.pyvertexvectors.Zones`
object is added to the viewer automatically on the first accepted polyline.
:param kwargs: Forwarded to the constructor.
:return: The activated companion.
"""
c = MultiPolylineZonesCompanion(viewer, zones_id=zones_id,
auto_attach=auto_attach, **kwargs)
c.start()
return c
[docs]
def polygon(viewer: 'WolfMapViewer', **kwargs) -> PolygonCompanion:
"""Create, register and activate a :class:`PolygonCompanion`.
:param viewer: The :class:`~wolfhece.PyDraw.WolfMapViewer` instance.
:param kwargs: Forwarded to the constructor.
:return: The activated companion.
"""
c = PolygonCompanion(viewer, **kwargs)
c.start()
return c