Source code for plotpy.tools.shape
# -*- coding: utf-8 -*-# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see plotpy/LICENSE for details)
"""Shape tools"""
from __future__ import annotations
import warnings
from typing import Callable
import numpy as np
from qtpy import QtCore as QC
from plotpy.config import _
from plotpy.constants import SHAPE_Z_OFFSET
from plotpy.events import (
MultilineSelectionHandler,
PointSelectionHandler,
StatefulEventFilter,
setup_standard_tool_filter,
)
from plotpy.items import (
EllipseShape,
ObliqueRectangleShape,
PointShape,
PolygonShape,
SegmentShape,
)
from plotpy.plot import BasePlot
from plotpy.tools.base import DefaultToolbarID, InteractiveTool, RectangularActionTool
[docs]
class MultiLineTool(InteractiveTool):
"""
A tool for drawing multi-line shapes (polylines) on a plot.
This tool allows users to create polyline shapes by clicking on the plot
to add points. The shape can be finalized using the Enter or Space key.
Args:
manager: The plot manager.
handle_final_shape_cb: Callback function to handle the final shape.
shape_style: Tuple containing the style section and key for the shape.
toolbar_id: ID of the toolbar to which this tool belongs.
title: Title of the tool.
icon: Icon for the tool.
tip: Tooltip for the tool.
switch_to_default_tool: Whether to switch to the default tool after use.
"""
TITLE: str = _("Polyline")
ICON: str = "polyline.png"
CLOSED: bool = False
CURSOR: QC.Qt.CursorShape = QC.Qt.CursorShape.PointingHandCursor
def __init__(
self,
manager,
setup_shape_cb: Callable | None = None,
handle_final_shape_cb: Callable | None = None,
shape_style: tuple[str, str] | None = None,
toolbar_id: str = DefaultToolbarID,
title: str | None = None,
icon: str | None = None,
tip: str | None = None,
switch_to_default_tool: bool | None = None,
):
super().__init__(
manager,
toolbar_id,
title=title,
icon=icon,
tip=tip,
switch_to_default_tool=switch_to_default_tool,
)
self.handler: MultilineSelectionHandler | None = None
self.switch_to_default_tool = switch_to_default_tool
self.setup_shape_cb = setup_shape_cb
self.handle_final_shape_cb = handle_final_shape_cb
if shape_style is not None:
self.shape_style_sect, self.shape_style_key = shape_style
else:
self.shape_style_sect = "plot"
self.shape_style_key = "shape/drag"
[docs]
def set_shape_style(self, shape: PolygonShape) -> None:
"""Set shape style
Args:
shape: shape
"""
shape.set_style(self.shape_style_sect, self.shape_style_key)
[docs]
def create_shape(self) -> PolygonShape:
"""Create shape"""
shape = PolygonShape(closed=False)
self.set_shape_style(shape)
return shape
[docs]
def setup_shape(self, shape: PolygonShape) -> None:
"""Setup shape"""
shape.setTitle(self.TITLE)
if self.setup_shape_cb is not None:
self.setup_shape_cb(shape)
[docs]
def get_shape(self) -> PolygonShape:
"""
Get shape
Returns:
shape
"""
shape = self.create_shape()
self.setup_shape(shape)
return shape
[docs]
def setup_filter(self, baseplot: BasePlot) -> StatefulEventFilter:
"""
Set up the event filter for the tool.
Args:
baseplot: The base plot object.
Returns:
The configured filter.
"""
filter = baseplot.filter
start_state = filter.new_state()
self.handler = MultilineSelectionHandler(
filter, QC.Qt.LeftButton, start_state=start_state, closed=self.CLOSED
)
self.handler.SIG_END_POLYLINE.connect(self.end_polyline)
shape = self.get_shape()
self.handler.set_shape(shape, self.setup_shape)
return setup_standard_tool_filter(filter, start_state)
[docs]
def handle_final_shape(self, shape) -> None:
"""
Handle the final shape after it's been created.
Args:
shape: The final shape object.
"""
if self.handle_final_shape_cb is not None:
self.handle_final_shape_cb(shape)
[docs]
def end_polyline(self, filter: StatefulEventFilter, points: np.ndarray) -> None:
"""
End the polyline and reset the tool.
Args:
filter: The plot filter.
points: The points of the polyline.
"""
plot = filter.plot
shape = self.get_shape()
shape.set_points(points)
shape.set_closed(self.CLOSED)
plot.add_item_with_z_offset(shape, SHAPE_Z_OFFSET)
self.handle_final_shape(shape)
self.SIG_TOOL_JOB_FINISHED.emit()
if self.switch_to_default_tool:
plot.set_active_item(shape)
[docs]
class PolygonTool(MultiLineTool):
"""
A tool for drawing free-form shapes on a plot.
This tool extends the MultiLineTool to create closed shapes when
there are more than 2 points.
"""
TITLE: str = _("Polygon")
ICON: str = "polygon.png"
CLOSED: bool = True
# The old name of the class was FreeFormTool, but the class is now PolygonTool
# The old name is kept for backward compatibility, but a warning is issued when
# the class is instantiated using the old name.
class FreeFormTool(PolygonTool):
def __init__(self, *args, **kwargs):
warnings.warn(
"FreeFormTool is deprecated, use PolygonTool instead.",
DeprecationWarning,
stacklevel=2,
)
super().__init__(*args, **kwargs)
[docs]
class RectangularShapeTool(RectangularActionTool):
"""
Base class for tools that create rectangular shapes.
Args:
manager: The plot manager.
setup_shape_cb: Callback function to set up the shape.
handle_final_shape_cb: Callback function to handle the final shape.
shape_style: Tuple containing the style section and key for the shape.
toolbar_id: ID of the toolbar to which this tool belongs.
title: Title of the tool.
icon: Icon for the tool.
tip: Tooltip for the tool.
switch_to_default_tool: Whether to switch to the default tool after use.
"""
TITLE: str | None = None
ICON: str | None = None
def __init__(
self,
manager,
setup_shape_cb: Callable | None = None,
handle_final_shape_cb: Callable | None = None,
shape_style: tuple[str, str] | None = None,
toolbar_id: str = DefaultToolbarID,
title: str | None = None,
icon: str | None = None,
tip: str | None = None,
switch_to_default_tool: bool | None = None,
):
super().__init__(
manager,
self.add_shape_to_plot,
shape_style,
toolbar_id=toolbar_id,
title=title,
icon=icon,
tip=tip,
switch_to_default_tool=switch_to_default_tool,
)
self.setup_shape_cb = setup_shape_cb
self.handle_final_shape_cb = handle_final_shape_cb
[docs]
def add_shape_to_plot(self, plot, p0: QC.QPointF, p1: QC.QPointF):
"""
Add the final shape to the plot.
Args:
plot: The plot object.
p0: The first point of the shape.
p1: The second point of the shape.
"""
shape = self.get_final_shape(plot, p0, p1)
self.handle_final_shape(shape)
plot.replot()
[docs]
def setup_shape(self, shape) -> None:
"""
Set up the shape properties.
Args:
shape: The shape object to set up.
"""
shape.setTitle(self.TITLE)
if self.setup_shape_cb is not None:
self.setup_shape_cb(shape)
[docs]
def handle_final_shape(self, shape) -> None:
"""
Handle the final shape after it's been created.
Args:
shape: The final shape object.
"""
if self.handle_final_shape_cb is not None:
self.handle_final_shape_cb(shape)
[docs]
class RectangleTool(RectangularShapeTool):
"""Tool for creating rectangle shapes."""
TITLE: str = _("Rectangle")
ICON: str = "rectangle.png"
class ObliqueRectangleTool(RectangularShapeTool):
"""Tool for creating oblique rectangle shapes."""
TITLE: str = _("Oblique rectangle")
ICON: str = "oblique_rectangle.png"
AVOID_NULL_SHAPE: bool = True
def create_shape(self):
"""
Create an oblique rectangle shape.
Returns:
A tuple containing the shape object and its handle indices.
"""
shape = ObliqueRectangleShape(1, 1, 2, 1, 2, 2, 1, 2)
self.set_shape_style(shape)
return shape, 0, 2
[docs]
class PointTool(RectangularShapeTool):
"""Tool for creating point shapes."""
TITLE: str = _("Point")
ICON: str = "point_shape.png"
SHAPE_STYLE_KEY: str = "shape/point"
[docs]
def create_shape(self):
"""
Create a point shape.
Returns:
A tuple containing the shape object and its handle indices.
"""
shape = PointShape(0, 0)
self.set_shape_style(shape)
return shape, 0, 0
[docs]
def get_selection_handler(self, filter, start_state):
"""
Get the selection handler for the point tool.
Args:
filter: The plot filter.
start_state: The initial state.
Returns:
A PointSelectionHandler object.
"""
return PointSelectionHandler(filter, QC.Qt.LeftButton, start_state=start_state)
[docs]
class SegmentTool(RectangularShapeTool):
"""Tool for creating segment shapes."""
TITLE: str = _("Segment")
ICON: str = "segment.png"
SHAPE_STYLE_KEY: str = "shape/segment"
[docs]
def create_shape(self):
"""
Create a segment shape.
Returns:
A tuple containing the shape object and its handle indices.
"""
shape = SegmentShape(0, 0, 1, 1)
self.set_shape_style(shape)
return shape, 0, 1
[docs]
class CircleTool(RectangularShapeTool):
"""Tool for creating circle shapes."""
TITLE: str = _("Circle")
ICON: str = "circle.png"
[docs]
def create_shape(self):
"""
Create a circle shape.
Returns:
A tuple containing the shape object and its handle indices.
"""
shape = EllipseShape(0, 0, 1, 1)
self.set_shape_style(shape)
return shape, 0, 1
[docs]
class EllipseTool(RectangularShapeTool):
"""Tool for creating ellipse shapes."""
TITLE: str = _("Ellipse")
ICON: str = "ellipse_shape.png"
[docs]
def create_shape(self):
"""
Create an ellipse shape.
Returns:
A tuple containing the shape object and its handle indices.
"""
shape = EllipseShape(0, 0, 1, 1)
self.set_shape_style(shape)
return shape, 0, 1
[docs]
def handle_final_shape(self, shape) -> None:
"""
Handle the final ellipse shape after it's been created.
Args:
shape: The final ellipse shape object.
"""
shape.switch_to_ellipse()
super().handle_final_shape(shape)