”;
Poly Editor is short for Polygon Editor is an application that allows users to interactively edit and manipulate vertices of a polygon in a graphical environment.
In the context of Matplotlib, a Poly Editor typically refers to a cross-GUI application that allows users to interactively modify polygons displayed on a canvas. This application provides features such as adding, deleting, and moving vertices of a polygon, as well as adjusting its shape and position using mouse clicks and keybindings.
This tutorial will demonstrate how to create a polygon editor using Matplotlib”s event handling capabilities.
Creating the Polygon Interactor Class
To create the Poly Editor, define a Python class called PolygonInteractor, which handles interactions with the polygon vertices. This class implements event handling methods to respond to user interactions −
-
on_draw − Handles the drawing of the polygon and its vertices.
-
on_button_press − Responds to mouse button presses to select vertices.
-
on_button_release − Handles mouse button releases.
-
on_key_press − Handles key presses to toggle vertex markers(using the ”t” key), delete vertices(using the ‘d’ key), or insert new vertices(using the ”i” key).
-
on_mouse_move − Handles mouse movements to drag vertices and update the polygon.
Below is the implementation of the PolygonInteractor class −
class PolygonInteractor: showverts = True epsilon = 3 def __init__(self, ax, poly): if poly.figure is None: raise RuntimeError(''You must first add the polygon to a figure '' ''or canvas before defining the interactor'') self.ax = ax canvas = poly.figure.canvas self.poly = poly x, y = zip(*self.poly.xy) self.line = Line2D(x, y, marker=''o'', markerfacecolor=''r'', animated=True) self.ax.add_line(self.line) self.cid = self.poly.add_callback(self.poly_changed) self._ind = None # the active vert canvas.mpl_connect(''draw_event'', self.on_draw) canvas.mpl_connect(''button_press_event'', self.on_button_press) canvas.mpl_connect(''key_press_event'', self.on_key_press) canvas.mpl_connect(''button_release_event'', self.on_button_release) canvas.mpl_connect(''motion_notify_event'', self.on_mouse_move) self.canvas = canvas def on_draw(self, event): self.background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self.poly) self.ax.draw_artist(self.line) def poly_changed(self, poly): vis = self.line.get_visible() Artist.update_from(self.line, poly) self.line.set_visible(vis) # don''t use the poly visibility state def get_ind_under_point(self, event): xy = np.asarray(self.poly.xy) xyt = self.poly.get_transform().transform(xy) xt, yt = xyt[:, 0], xyt[:, 1] d = np.hypot(xt - event.x, yt - event.y) indseq, = np.nonzero(d == d.min()) ind = indseq[0] if d[ind] >= self.epsilon: ind = None return ind def on_button_press(self, event): if not self.showverts: return if event.inaxes is None: return if event.button != 1: return self._ind = self.get_ind_under_point(event) def on_button_release(self, event): if not self.showverts: return if event.button != 1: return self._ind = None def on_key_press(self, event): if not event.inaxes: return if event.key == ''t'': self.showverts = not self.showverts self.line.set_visible(self.showverts) if not self.showverts: self._ind = None elif event.key == ''d'': ind = self.get_ind_under_point(event) if ind is not None: self.poly.xy = np.delete(self.poly.xy, ind, axis=0) self.line.set_data(zip(*self.poly.xy)) elif event.key == ''i'': xys = self.poly.get_transform().transform(self.poly.xy) p = event.x, event.y # display coords for i in range(len(xys) - 1): s0 = xys[i] s1 = xys[i + 1] d = dist_point_to_segment(p, s0, s1) if d <= self.epsilon: self.poly.xy = np.insert( self.poly.xy, i+1, [event.xdata, event.ydata], axis=0) self.line.set_data(zip(*self.poly.xy)) break if self.line.stale: self.canvas.draw_idle() def on_mouse_move(self, event): if not self.showverts: return if self._ind is None: return if event.inaxes is None: return if event.button != 1: return x, y = event.xdata, event.ydata self.poly.xy[self._ind] = x, y if self._ind == 0: self.poly.xy[-1] = x, y elif self._ind == len(self.poly.xy) - 1: self.poly.xy[0] = x, y self.line.set_data(zip(*self.poly.xy)) self.canvas.restore_region(self.background) self.ax.draw_artist(self.poly) self.ax.draw_artist(self.line) self.canvas.blit(self.ax.bbox)
Defining Utility Function
Define a utility function dist_point_to_segment to calculate the distance between a point and a line segment. This function is used to determine which vertex is closest to the mouse cursor during interaction.
def dist_point_to_segment(p, s0, s1): s01 = s1 - s0 s0p = p - s0 if (s01 == 0).all(): return np.hypot(*s0p) p1 = s0 + np.clip((s0p @ s01) / (s01 @ s01), 0, 1) * s01 return np.hypot(*(p - p1))
Initializing the Polygon Editor
To initialize the polygon editor, we need to create an instance of the PolygonInteractor class and pass it the axis object and the polygon object:
if __name__ == ''__main__'': import matplotlib.pyplot as plt from matplotlib.patches import Polygon theta = np.arange(0, 2*np.pi, 0.2) r = 1.5 xs = r * np.cos(theta) ys = r * np.sin(theta) poly = Polygon(np.column_stack([xs, ys]), animated=True) fig, ax = plt.subplots() ax.add_patch(poly) p = PolygonInteractor(ax, poly) ax.set_title(''Click and drag a point to move it'') ax.set_xlim((-2, 2)) ax.set_ylim((-2, 2)) plt.show()
Running the Poly Editor
By executing the complete code provided below, we will get a Matplotlib window displaying a plot with a polygon. We can interact with the polygon by clicking and dragging its vertices, toggling vertex markers by pressing the ‘t’ key, pressing the ”d” key to delete vertices, and pressing the ”i” key to insert new vertices.
Example
import matplotlib.pyplot as plt import numpy as np from matplotlib.backend_bases import MouseButton from matplotlib.patches import PathPatch from matplotlib.path import Path class PathInteractor: showverts = True # max pixel distance to count as a vertex hit epsilon = 5 def __init__(self, pathpatch): # Initialization and event connections self.ax = pathpatch.axes canvas = self.ax.figure.canvas self.pathpatch = pathpatch self.pathpatch.set_animated(True) x, y = zip(*self.pathpatch.get_path().vertices) self.line, = ax.plot( x, y, marker=''o'', markerfacecolor=''r'', animated=True) self._ind = None # the active vertex canvas.mpl_connect(''draw_event'', self.on_draw) canvas.mpl_connect(''button_press_event'', self.on_button_press) canvas.mpl_connect(''key_press_event'', self.on_key_press) canvas.mpl_connect(''button_release_event'', self.on_button_release) canvas.mpl_connect(''motion_notify_event'', self.on_mouse_move) self.canvas = canvas def get_ind_under_point(self, event): # Return the index of the point closest to the event position or None xy = self.pathpatch.get_path().vertices xyt = self.pathpatch.get_transform().transform(xy) # to display coords xt, yt = xyt[:, 0], xyt[:, 1] d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) ind = d.argmin() return ind if d[ind] < self.epsilon else None def on_draw(self, event): # Callback for draws. self.background = self.canvas.copy_from_bbox(self.ax.bbox) self.ax.draw_artist(self.pathpatch) self.ax.draw_artist(self.line) self.canvas.blit(self.ax.bbox) def on_button_press(self, event): # Callback for mouse button presses if (event.inaxes is None or event.button != MouseButton.LEFT or not self.showverts): return self._ind = self.get_ind_under_point(event) def on_button_release(self, event): # Callback for mouse button releases if (event.button != MouseButton.LEFT or not self.showverts): return self._ind = None def on_key_press(self, event): # Callback for key presses if not event.inaxes: return if event.key == ''t'': self.showverts = not self.showverts self.line.set_visible(self.showverts) if not self.showverts: self._ind = None self.canvas.draw() def on_mouse_move(self, event): # Callback for mouse movements if (self._ind is None or event.inaxes is None or event.button != MouseButton.LEFT or not self.showverts): return vertices = self.pathpatch.get_path().vertices vertices[self._ind] = event.xdata, event.ydata self.line.set_data(zip(*vertices)) self.canvas.restore_region(self.background) self.ax.draw_artist(self.pathpatch) self.ax.draw_artist(self.line) self.canvas.blit(self.ax.bbox) fig, ax = plt.subplots() pathdata = [ (Path.MOVETO, (1.58, -2.57)), (Path.CURVE4, (0.35, -1.1)), (Path.CURVE4, (-1.75, 2.0)), (Path.CURVE4, (0.375, 2.0)), (Path.LINETO, (0.85, 1.15)), (Path.CURVE4, (2.2, 3.2)), (Path.CURVE4, (3, 0.05)), (Path.CURVE4, (2.0, -0.5)), (Path.CLOSEPOLY, (1.58, -2.57)), ] codes, verts = zip(*pathdata) path = Path(verts, codes) patch = PathPatch( path, facecolor=''green'', edgecolor=''yellow'', alpha=0.5) ax.add_patch(patch) interactor = PathInteractor(patch) ax.set_title(''drag vertices to update path'') ax.set_xlim(-3, 4) ax.set_ylim(-3, 4) plt.show()
Output
On executing the above code we will get the following output −
Watch the video below to observe the works of this application −
”;