|
|
@@ -0,0 +1,310 @@
|
|
|
+from gi.repository import Gdk
|
|
|
+from matplotlib.figure import Figure
|
|
|
+from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
|
|
|
+#from FlatCAMApp import *
|
|
|
+import FlatCAMApp
|
|
|
+
|
|
|
+
|
|
|
+class PlotCanvas:
|
|
|
+ """
|
|
|
+ Class handling the plotting area in the application.
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, container):
|
|
|
+ """
|
|
|
+ The constructor configures the Matplotlib figure that
|
|
|
+ will contain all plots, creates the base axes and connects
|
|
|
+ events to the plotting area.
|
|
|
+
|
|
|
+ :param container: The parent container in which to draw plots.
|
|
|
+ :rtype: PlotCanvas
|
|
|
+ """
|
|
|
+ # Options
|
|
|
+ self.x_margin = 15 # pixels
|
|
|
+ self.y_margin = 25 # Pixels
|
|
|
+
|
|
|
+ # Parent container
|
|
|
+ self.container = container
|
|
|
+
|
|
|
+ # Plots go onto a single matplotlib.figure
|
|
|
+ self.figure = Figure(dpi=50) # TODO: dpi needed?
|
|
|
+ self.figure.patch.set_visible(False)
|
|
|
+
|
|
|
+ # These axes show the ticks and grid. No plotting done here.
|
|
|
+ # New axes must have a label, otherwise mpl returns an existing one.
|
|
|
+ self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
|
|
|
+ self.axes.set_aspect(1)
|
|
|
+ self.axes.grid(True)
|
|
|
+
|
|
|
+ # The canvas is the top level container (Gtk.DrawingArea)
|
|
|
+ self.canvas = FigureCanvas(self.figure)
|
|
|
+ self.canvas.set_hexpand(1)
|
|
|
+ self.canvas.set_vexpand(1)
|
|
|
+ self.canvas.set_can_focus(True) # For key press
|
|
|
+
|
|
|
+ # Attach to parent
|
|
|
+ self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
|
|
|
+
|
|
|
+ # Events
|
|
|
+ self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
|
|
|
+ self.canvas.connect('configure-event', self.auto_adjust_axes)
|
|
|
+ self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
|
|
|
+ self.canvas.connect("scroll-event", self.on_scroll)
|
|
|
+ self.canvas.mpl_connect('key_press_event', self.on_key_down)
|
|
|
+ self.canvas.mpl_connect('key_release_event', self.on_key_up)
|
|
|
+
|
|
|
+ self.mouse = [0, 0]
|
|
|
+ self.key = None
|
|
|
+
|
|
|
+ def on_key_down(self, event):
|
|
|
+ """
|
|
|
+
|
|
|
+ :param event:
|
|
|
+ :return:
|
|
|
+ """
|
|
|
+ self.key = event.key
|
|
|
+
|
|
|
+ def on_key_up(self, event):
|
|
|
+ """
|
|
|
+
|
|
|
+ :param event:
|
|
|
+ :return:
|
|
|
+ """
|
|
|
+ self.key = None
|
|
|
+
|
|
|
+ def mpl_connect(self, event_name, callback):
|
|
|
+ """
|
|
|
+ Attach an event handler to the canvas through the Matplotlib interface.
|
|
|
+
|
|
|
+ :param event_name: Name of the event
|
|
|
+ :type event_name: str
|
|
|
+ :param callback: Function to call
|
|
|
+ :type callback: func
|
|
|
+ :return: Connection id
|
|
|
+ :rtype: int
|
|
|
+ """
|
|
|
+ return self.canvas.mpl_connect(event_name, callback)
|
|
|
+
|
|
|
+ def mpl_disconnect(self, cid):
|
|
|
+ """
|
|
|
+ Disconnect callback with the give id.
|
|
|
+ :param cid: Callback id.
|
|
|
+ :return: None
|
|
|
+ """
|
|
|
+ self.canvas.mpl_disconnect(cid)
|
|
|
+
|
|
|
+ def connect(self, event_name, callback):
|
|
|
+ """
|
|
|
+ Attach an event handler to the canvas through the native GTK interface.
|
|
|
+
|
|
|
+ :param event_name: Name of the event
|
|
|
+ :type event_name: str
|
|
|
+ :param callback: Function to call
|
|
|
+ :type callback: function
|
|
|
+ :return: Nothing
|
|
|
+ """
|
|
|
+ self.canvas.connect(event_name, callback)
|
|
|
+
|
|
|
+ def clear(self):
|
|
|
+ """
|
|
|
+ Clears axes and figure.
|
|
|
+
|
|
|
+ :return: None
|
|
|
+ """
|
|
|
+
|
|
|
+ # Clear
|
|
|
+ self.axes.cla()
|
|
|
+ try:
|
|
|
+ self.figure.clf()
|
|
|
+ except KeyError:
|
|
|
+ FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
|
|
|
+
|
|
|
+ # Re-build
|
|
|
+ self.figure.add_axes(self.axes)
|
|
|
+ self.axes.set_aspect(1)
|
|
|
+ self.axes.grid(True)
|
|
|
+
|
|
|
+ # Re-draw
|
|
|
+ self.canvas.queue_draw()
|
|
|
+
|
|
|
+ def adjust_axes(self, xmin, ymin, xmax, ymax):
|
|
|
+ """
|
|
|
+ Adjusts all axes while maintaining the use of the whole canvas
|
|
|
+ and an aspect ratio to 1:1 between x and y axes. The parameters are an original
|
|
|
+ request that will be modified to fit these restrictions.
|
|
|
+
|
|
|
+ :param xmin: Requested minimum value for the X axis.
|
|
|
+ :type xmin: float
|
|
|
+ :param ymin: Requested minimum value for the Y axis.
|
|
|
+ :type ymin: float
|
|
|
+ :param xmax: Requested maximum value for the X axis.
|
|
|
+ :type xmax: float
|
|
|
+ :param ymax: Requested maximum value for the Y axis.
|
|
|
+ :type ymax: float
|
|
|
+ :return: None
|
|
|
+ """
|
|
|
+
|
|
|
+ FlatCAMApp.App.log.debug("PC.adjust_axes()")
|
|
|
+
|
|
|
+ width = xmax - xmin
|
|
|
+ height = ymax - ymin
|
|
|
+ try:
|
|
|
+ r = width / height
|
|
|
+ except ZeroDivisionError:
|
|
|
+ FlatCAMApp.App.log.error("Height is %f" % height)
|
|
|
+ return
|
|
|
+ canvas_w, canvas_h = self.canvas.get_width_height()
|
|
|
+ canvas_r = float(canvas_w) / canvas_h
|
|
|
+ x_ratio = float(self.x_margin) / canvas_w
|
|
|
+ y_ratio = float(self.y_margin) / canvas_h
|
|
|
+
|
|
|
+ if r > canvas_r:
|
|
|
+ ycenter = (ymin + ymax) / 2.0
|
|
|
+ newheight = height * r / canvas_r
|
|
|
+ ymin = ycenter - newheight / 2.0
|
|
|
+ ymax = ycenter + newheight / 2.0
|
|
|
+ else:
|
|
|
+ xcenter = (xmax + xmin) / 2.0
|
|
|
+ newwidth = width * canvas_r / r
|
|
|
+ xmin = xcenter - newwidth / 2.0
|
|
|
+ xmax = xcenter + newwidth / 2.0
|
|
|
+
|
|
|
+ # Adjust axes
|
|
|
+ for ax in self.figure.get_axes():
|
|
|
+ if ax._label != 'base':
|
|
|
+ ax.set_frame_on(False) # No frame
|
|
|
+ ax.set_xticks([]) # No tick
|
|
|
+ ax.set_yticks([]) # No ticks
|
|
|
+ ax.patch.set_visible(False) # No background
|
|
|
+ ax.set_aspect(1)
|
|
|
+ ax.set_xlim((xmin, xmax))
|
|
|
+ ax.set_ylim((ymin, ymax))
|
|
|
+ ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
|
|
|
+
|
|
|
+ # Re-draw
|
|
|
+ self.canvas.queue_draw()
|
|
|
+
|
|
|
+ def auto_adjust_axes(self, *args):
|
|
|
+ """
|
|
|
+ Calls ``adjust_axes()`` using the extents of the base axes.
|
|
|
+
|
|
|
+ :rtype : None
|
|
|
+ :return: None
|
|
|
+ """
|
|
|
+
|
|
|
+ xmin, xmax = self.axes.get_xlim()
|
|
|
+ ymin, ymax = self.axes.get_ylim()
|
|
|
+ self.adjust_axes(xmin, ymin, xmax, ymax)
|
|
|
+
|
|
|
+ def zoom(self, factor, center=None):
|
|
|
+ """
|
|
|
+ Zooms the plot by factor around a given
|
|
|
+ center point. Takes care of re-drawing.
|
|
|
+
|
|
|
+ :param factor: Number by which to scale the plot.
|
|
|
+ :type factor: float
|
|
|
+ :param center: Coordinates [x, y] of the point around which to scale the plot.
|
|
|
+ :type center: list
|
|
|
+ :return: None
|
|
|
+ """
|
|
|
+
|
|
|
+ xmin, xmax = self.axes.get_xlim()
|
|
|
+ ymin, ymax = self.axes.get_ylim()
|
|
|
+ width = xmax - xmin
|
|
|
+ height = ymax - ymin
|
|
|
+
|
|
|
+ if center is None or center == [None, None]:
|
|
|
+ center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
|
|
|
+
|
|
|
+ # For keeping the point at the pointer location
|
|
|
+ relx = (xmax - center[0]) / width
|
|
|
+ rely = (ymax - center[1]) / height
|
|
|
+
|
|
|
+ new_width = width / factor
|
|
|
+ new_height = height / factor
|
|
|
+
|
|
|
+ xmin = center[0] - new_width * (1 - relx)
|
|
|
+ xmax = center[0] + new_width * relx
|
|
|
+ ymin = center[1] - new_height * (1 - rely)
|
|
|
+ ymax = center[1] + new_height * rely
|
|
|
+
|
|
|
+ # Adjust axes
|
|
|
+ for ax in self.figure.get_axes():
|
|
|
+ ax.set_xlim((xmin, xmax))
|
|
|
+ ax.set_ylim((ymin, ymax))
|
|
|
+
|
|
|
+ # Re-draw
|
|
|
+ self.canvas.queue_draw()
|
|
|
+
|
|
|
+ def pan(self, x, y):
|
|
|
+ xmin, xmax = self.axes.get_xlim()
|
|
|
+ ymin, ymax = self.axes.get_ylim()
|
|
|
+ width = xmax - xmin
|
|
|
+ height = ymax - ymin
|
|
|
+
|
|
|
+ # Adjust axes
|
|
|
+ for ax in self.figure.get_axes():
|
|
|
+ ax.set_xlim((xmin + x*width, xmax + x*width))
|
|
|
+ ax.set_ylim((ymin + y*height, ymax + y*height))
|
|
|
+
|
|
|
+ # Re-draw
|
|
|
+ self.canvas.queue_draw()
|
|
|
+
|
|
|
+ def new_axes(self, name):
|
|
|
+ """
|
|
|
+ Creates and returns an Axes object attached to this object's Figure.
|
|
|
+
|
|
|
+ :param name: Unique label for the axes.
|
|
|
+ :return: Axes attached to the figure.
|
|
|
+ :rtype: Axes
|
|
|
+ """
|
|
|
+
|
|
|
+ return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
|
|
|
+
|
|
|
+ def on_scroll(self, canvas, event):
|
|
|
+ """
|
|
|
+ Scroll event handler.
|
|
|
+
|
|
|
+ :param canvas: The widget generating the event. Ignored.
|
|
|
+ :param event: Event object containing the event information.
|
|
|
+ :return: None
|
|
|
+ """
|
|
|
+
|
|
|
+ # So it can receive key presses
|
|
|
+ self.canvas.grab_focus()
|
|
|
+
|
|
|
+ # Event info
|
|
|
+ z, direction = event.get_scroll_direction()
|
|
|
+
|
|
|
+ if self.key is None:
|
|
|
+
|
|
|
+ if direction is Gdk.ScrollDirection.UP:
|
|
|
+ self.zoom(1.5, self.mouse)
|
|
|
+ else:
|
|
|
+ self.zoom(1/1.5, self.mouse)
|
|
|
+ return
|
|
|
+
|
|
|
+ if self.key == 'shift':
|
|
|
+
|
|
|
+ if direction is Gdk.ScrollDirection.UP:
|
|
|
+ self.pan(0.3, 0)
|
|
|
+ else:
|
|
|
+ self.pan(-0.3, 0)
|
|
|
+ return
|
|
|
+
|
|
|
+ if self.key == 'ctrl+control':
|
|
|
+
|
|
|
+ if direction is Gdk.ScrollDirection.UP:
|
|
|
+ self.pan(0, 0.3)
|
|
|
+ else:
|
|
|
+ self.pan(0, -0.3)
|
|
|
+ return
|
|
|
+
|
|
|
+ def on_mouse_move(self, event):
|
|
|
+ """
|
|
|
+ Mouse movement event hadler. Stores the coordinates.
|
|
|
+
|
|
|
+ :param event: Contains information about the event.
|
|
|
+ :return: None
|
|
|
+ """
|
|
|
+ self.mouse = [event.xdata, event.ydata]
|