| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- ############################################################
- # FlatCAM: 2D Post-processing for Manufacturing #
- # http://caram.cl/software/flatcam #
- # Author: Juan Pablo Caram (c) #
- # Date: 2/5/2014 #
- # MIT Licence #
- ############################################################
- from PyQt5 import QtGui, QtCore, QtWidgets
- # Prevent conflict with Qt5 and above.
- from matplotlib import use as mpl_use
- from matplotlib.figure import Figure
- from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
- from matplotlib.backends.backend_agg import FigureCanvasAgg
- from matplotlib.widgets import Cursor
- import FlatCAMApp
- import logging
- mpl_use("Qt5Agg")
- log = logging.getLogger('base')
- class CanvasCache(QtCore.QObject):
- """
- Case story #1:
- 1) No objects in the project.
- 2) Object is created (new_object() emits object_created(obj)).
- on_object_created() adds (i) object to collection and emits
- (ii) new_object_available() then calls (iii) object.plot()
- 3) object.plot() creates axes if necessary on
- app.collection.figure. Then plots on it.
- 4) Plots on a cache-size canvas (in background).
- 5) Plot completes. Bitmap is generated.
- 6) Visible canvas is painted.
- """
- # Signals:
- # A bitmap is ready to be displayed.
- new_screen = QtCore.pyqtSignal()
- def __init__(self, plotcanvas, app, dpi=50):
- super(CanvasCache, self).__init__()
- self.app = app
- self.plotcanvas = plotcanvas
- self.dpi = dpi
- self.figure = Figure(dpi=dpi)
- self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
- self.axes.set_frame_on(False)
- self.axes.set_xticks([])
- self.axes.set_yticks([])
- self.canvas = FigureCanvasAgg(self.figure)
- self.cache = None
- def run(self):
- log.debug("CanvasCache Thread Started!")
- self.plotcanvas.update_screen_request.connect(self.on_update_req)
- def on_update_req(self, extents):
- """
- Event handler for an updated display request.
- :param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
- """
- # log.debug("Canvas update requested: %s" % str(extents))
- # Note: This information below might be out of date. Establish
- # a protocol regarding when to change the canvas in the main
- # thread and when to check these values here in the background,
- # or pass this data in the signal (safer).
- # log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
- # log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
- # Move the requested screen portion to the main thread
- # and inform about the update:
- self.new_screen.emit()
- # Continue to update the cache.
- # def on_new_object_available(self):
- #
- # log.debug("A new object is available. Should plot it!")
- class PlotCanvasLegacy(QtCore.QObject):
- """
- Class handling the plotting area in the application.
- """
- # Signals:
- # Request for new bitmap to display. The parameter
- # is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
- update_screen_request = QtCore.pyqtSignal(list)
- double_click = QtCore.pyqtSignal(object)
- def __init__(self, container, app):
- """
- 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
- """
- super(PlotCanvasLegacy, self).__init__()
- self.app = app
- # 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)
- self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
- self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
- # The canvas is the top level container (FigureCanvasQTAgg)
- self.canvas = FigureCanvas(self.figure)
- self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
- self.canvas.setFocus()
- self.native = self.canvas
- # 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??
- self.container.addWidget(self.canvas) # Qt
- # Copy a bitmap of the canvas for quick animation.
- # Update every time the canvas is re-drawn.
- self.background = self.canvas.copy_from_bbox(self.axes.bbox)
- # ## Bitmap Cache
- self.cache = CanvasCache(self, self.app)
- self.cache_thread = QtCore.QThread()
- self.cache.moveToThread(self.cache_thread)
- # super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
- self.cache_thread.started.connect(self.cache.run)
- self.cache_thread.start()
- self.cache.new_screen.connect(self.on_new_screen)
- # Events
- self.mp = self.graph_event_connect('button_press_event', self.on_mouse_press)
- self.mr = self.graph_event_connect('button_release_event', self.on_mouse_release)
- self.mm = self.graph_event_connect('motion_notify_event', self.on_mouse_move)
- # self.canvas.connect('configure-event', self.auto_adjust_axes)
- self.aaa = self.graph_event_connect('resize_event', self.auto_adjust_axes)
- # self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
- # self.canvas.connect("scroll-event", self.on_scroll)
- self.osc = self.graph_event_connect('scroll_event', self.on_scroll)
- # self.graph_event_connect('key_press_event', self.on_key_down)
- # self.graph_event_connect('key_release_event', self.on_key_up)
- self.odr = self.graph_event_connect('draw_event', self.on_draw)
- self.mouse = [0, 0]
- self.key = None
- self.pan_axes = []
- self.panning = False
- # signal is the mouse is dragging
- self.is_dragging = False
- # signal if there is a doubleclick
- self.is_dblclk = False
- def graph_event_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
- """
- if event_name == 'mouse_move':
- event_name = 'motion_notify_event'
- if event_name == 'mouse_press':
- event_name = 'button_press_event'
- if event_name == 'mouse_release':
- event_name = 'button_release_event'
- if event_name == 'mouse_double_click':
- return self.double_click.connect(callback)
- if event_name == 'key_press':
- event_name = 'key_press_event'
- return self.canvas.mpl_connect(event_name, callback)
- def graph_event_disconnect(self, cid):
- """
- Disconnect callback with the give id.
- :param cid: Callback id.
- :return: None
- """
- # self.double_click.disconnect(cid)
- self.canvas.mpl_disconnect(cid)
- def on_new_screen(self):
- pass
- # log.debug("Cache updated the screen!")
- def new_cursor(self, axes=None):
- # if axes is None:
- # c = MplCursor(axes=self.axes, color='black', linewidth=1)
- # else:
- # c = MplCursor(axes=axes, color='black', linewidth=1)
- c = FakeCursor()
- return c
- def on_key_down(self, event):
- """
- :param event:
- :return:
- """
- FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
- self.key = event.key
- def on_key_up(self, event):
- """
- :param event:
- :return:
- """
- self.key = None
- def connect(self, event_name, callback):
- """
- Attach an event handler to the canvas through the native Qt 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.draw_idle()
- 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])
- # Sync re-draw to proper paint on form resize
- self.canvas.draw()
- # #### Temporary place-holder for cached update #####
- self.update_screen_request.emit([0, 0, 0, 0, 0])
- 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))
- # Async re-draw
- self.canvas.draw_idle()
- # #### Temporary place-holder for cached update #####
- self.update_screen_request.emit([0, 0, 0, 0, 0])
- 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.draw_idle()
- # #### Temporary place-holder for cached update #####
- self.update_screen_request.emit([0, 0, 0, 0, 0])
- 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, event):
- """
- Scroll event handler.
- :param event: Event object containing the event information.
- :return: None
- """
- # So it can receive key presses
- # self.canvas.grab_focus()
- self.canvas.setFocus()
- # Event info
- # z, direction = event.get_scroll_direction()
- if self.key is None:
- if event.button == 'up':
- self.zoom(1.5, self.mouse)
- else:
- self.zoom(1 / 1.5, self.mouse)
- return
- if self.key == 'shift':
- if event.button == 'up':
- self.pan(0.3, 0)
- else:
- self.pan(-0.3, 0)
- return
- if self.key == 'control':
- if event.button == 'up':
- self.pan(0, 0.3)
- else:
- self.pan(0, -0.3)
- return
- def on_mouse_press(self, event):
- self.is_dragging = True
- # Check for middle mouse button press
- if self.app.defaults["global_pan_button"] == '2':
- pan_button = 3 # right button for Matplotlib
- else:
- pan_button = 2 # middle button for Matplotlib
- if event.button == pan_button:
- # Prepare axes for pan (using 'matplotlib' pan function)
- self.pan_axes = []
- for a in self.figure.get_axes():
- if (event.x is not None and event.y is not None and a.in_axes(event) and
- a.get_navigate() and a.can_pan()):
- a.start_pan(event.x, event.y, 1)
- self.pan_axes.append(a)
- # Set pan view flag
- if len(self.pan_axes) > 0:
- self.panning = True
- if event.dblclick:
- self.double_click.emit(event)
- def on_mouse_release(self, event):
- self.is_dragging = False
- # Check for middle mouse button release to complete pan procedure
- # Check for middle mouse button press
- if self.app.defaults["global_pan_button"] == '2':
- pan_button = 3 # right button for Matplotlib
- else:
- pan_button = 2 # middle button for Matplotlib
- if event.button == pan_button:
- for a in self.pan_axes:
- a.end_pan()
- # Clear pan flag
- self.panning = False
- def on_mouse_move(self, event):
- """
- Mouse movement event hadler. Stores the coordinates. Updates view on pan.
- :param event: Contains information about the event.
- :return: None
- """
- try:
- x = float(event.xdata)
- y = float(event.ydata)
- except TypeError:
- return
- self.mouse = [event.xdata, event.ydata]
- self.canvas.restore_region(self.background)
- # Update pan view on mouse move
- if self.panning is True:
- # x_pan, y_pan = self.app.geo_editor.snap(event.xdata, event.ydata)
- # self.app.app_cursor.set_data(event, (x_pan, y_pan))
- for a in self.pan_axes:
- a.drag_pan(1, event.key, event.x, event.y)
- # Async re-draw (redraws only on thread idle state, uses timer on backend)
- self.canvas.draw_idle()
- # #### Temporary place-holder for cached update #####
- self.update_screen_request.emit([0, 0, 0, 0, 0])
- x, y = self.app.geo_editor.snap(x, y)
- if self.app.app_cursor.enabled is True:
- # Pointer (snapped)
- elements = self.axes.plot(x, y, 'k+', ms=40, mew=2, animated=True)
- for el in elements:
- self.axes.draw_artist(el)
- self.canvas.blit(self.axes.bbox)
- def translate_coords(self, position):
- """
- This does not do much. It's just for code compatibility
- :param position: Mouse event position
- :return: Tuple with mouse position
- """
- return (position[0], position[1])
- def on_draw(self, renderer):
- # Store background on canvas redraw
- self.background = self.canvas.copy_from_bbox(self.axes.bbox)
- def get_axes_pixelsize(self):
- """
- Axes size in pixels.
- :return: Pixel width and height
- :rtype: tuple
- """
- bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
- width, height = bbox.width, bbox.height
- width *= self.figure.dpi
- height *= self.figure.dpi
- return width, height
- def get_density(self):
- """
- Returns unit length per pixel on horizontal
- and vertical axes.
- :return: X and Y density
- :rtype: tuple
- """
- xpx, ypx = self.get_axes_pixelsize()
- xmin, xmax = self.axes.get_xlim()
- ymin, ymax = self.axes.get_ylim()
- width = xmax - xmin
- height = ymax - ymin
- return width / xpx, height / ypx
- class FakeCursor():
- def __init__(self):
- self._enabled = True
- @property
- def enabled(self):
- return True if self._enabled else False
- @enabled.setter
- def enabled(self, value):
- self._enabled = value
- class MplCursor(Cursor):
- def __init__(self, axes, color='red', linewidth=1):
- super().__init__(ax=axes, useblit=True, color=color, linewidth=linewidth)
- self._enabled = True
- self.axes = axes
- self.color = color
- self.linewidth = linewidth
- self.x = None
- self.y = None
- @property
- def enabled(self):
- return True if self._enabled else False
- @enabled.setter
- def enabled(self, value):
- self._enabled = value
- self.visible = self._enabled
- self.canvas.draw()
- def onmove(self, event):
- pass
- def set_data(self, event, pos):
- """Internal event handler to draw the cursor when the mouse moves."""
- self.x = pos[0]
- self.y = pos[1]
- if self.ignore(event):
- return
- if not self.canvas.widgetlock.available(self):
- return
- if event.inaxes != self.ax:
- self.linev.set_visible(False)
- self.lineh.set_visible(False)
- if self.needclear:
- self.canvas.draw()
- self.needclear = False
- return
- self.needclear = True
- if not self.visible:
- return
- self.linev.set_xdata((self.x, self.x))
- self.lineh.set_ydata((self.y, self.y))
- self.linev.set_visible(self.visible and self.vertOn)
- self.lineh.set_visible(self.visible and self.horizOn)
- self._update()
- class ShapeCollectionLegacy():
- def __init__(self):
- self._shapes = []
- def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
- update=False, layer=1, tolerance=0.01):
- try:
- for sh in shape:
- self._shapes.append(sh)
- except TypeError:
- self._shapes.append(shape)
- return len(self._shapes) - 1
- def clear(self, update=None):
- self._shapes[:] = []
- if update is True:
- self.redraw()
- def redraw(self):
- pass
|