PlotCanvas.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://caram.cl/software/flatcam #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. from PyQt4 import QtGui, QtCore
  9. # Prevent conflict with Qt5 and above.
  10. from matplotlib import use as mpl_use
  11. mpl_use("Qt4Agg")
  12. from matplotlib.figure import Figure
  13. from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
  14. from matplotlib.backends.backend_agg import FigureCanvasAgg
  15. import FlatCAMApp
  16. class CanvasCache(QtCore.QObject):
  17. # Signals
  18. new_screen = QtCore.pyqtSignal()
  19. def __init__(self, plotcanvas, dpi=50):
  20. super(CanvasCache, self).__init__()
  21. self.plotcanvas = plotcanvas
  22. self.dpi = dpi
  23. self.figure = Figure(dpi=dpi)
  24. self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
  25. self.axes.set_frame_on(False)
  26. self.axes.set_xticks([])
  27. self.axes.set_yticks([])
  28. self.canvas = FigureCanvasAgg(self.figure)
  29. self.cache = None
  30. def run(self):
  31. self.plotcanvas.update_screen_request.connect(self.on_update_req)
  32. def on_update_req(self, extents):
  33. # Move the requested screen portion to the main thread
  34. # and inform about the update:
  35. self.new_screen.emit()
  36. # Continue to update the cache.
  37. class PlotCanvas(QtCore.QObject):
  38. """
  39. Class handling the plotting area in the application.
  40. """
  41. # Signals
  42. update_screen_request = QtCore.pyqtSignal(list)
  43. def __init__(self, container):
  44. """
  45. The constructor configures the Matplotlib figure that
  46. will contain all plots, creates the base axes and connects
  47. events to the plotting area.
  48. :param container: The parent container in which to draw plots.
  49. :rtype: PlotCanvas
  50. """
  51. # Options
  52. self.x_margin = 15 # pixels
  53. self.y_margin = 25 # Pixels
  54. # Parent container
  55. self.container = container
  56. # Plots go onto a single matplotlib.figure
  57. self.figure = Figure(dpi=50) # TODO: dpi needed?
  58. self.figure.patch.set_visible(False)
  59. # These axes show the ticks and grid. No plotting done here.
  60. # New axes must have a label, otherwise mpl returns an existing one.
  61. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  62. self.axes.set_aspect(1)
  63. self.axes.grid(True)
  64. # The canvas is the top level container (Gtk.DrawingArea)
  65. self.canvas = FigureCanvas(self.figure)
  66. # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
  67. # self.canvas.setFocus()
  68. #self.canvas.set_hexpand(1)
  69. #self.canvas.set_vexpand(1)
  70. #self.canvas.set_can_focus(True) # For key press
  71. # Attach to parent
  72. #self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
  73. self.container.addWidget(self.canvas) # Qt
  74. # Copy a bitmap of the canvas for quick animation.
  75. # Update every time the canvas is re-drawn.
  76. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  77. # Bitmap Cache
  78. self.cache = CanvasCache(self)
  79. self.cache_thread = QtCore.QThread()
  80. self.cache.moveToThread(self.cache_thread)
  81. super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
  82. # self.connect()
  83. self.cache_thread.start()
  84. # Events
  85. self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
  86. self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
  87. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
  88. #self.canvas.connect('configure-event', self.auto_adjust_axes)
  89. self.canvas.mpl_connect('resize_event', self.auto_adjust_axes)
  90. #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  91. #self.canvas.connect("scroll-event", self.on_scroll)
  92. self.canvas.mpl_connect('scroll_event', self.on_scroll)
  93. self.canvas.mpl_connect('key_press_event', self.on_key_down)
  94. self.canvas.mpl_connect('key_release_event', self.on_key_up)
  95. self.canvas.mpl_connect('draw_event', self.on_draw)
  96. self.mouse = [0, 0]
  97. self.key = None
  98. self.pan_axes = []
  99. self.panning = False
  100. def on_key_down(self, event):
  101. """
  102. :param event:
  103. :return:
  104. """
  105. FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
  106. self.key = event.key
  107. def on_key_up(self, event):
  108. """
  109. :param event:
  110. :return:
  111. """
  112. self.key = None
  113. def mpl_connect(self, event_name, callback):
  114. """
  115. Attach an event handler to the canvas through the Matplotlib interface.
  116. :param event_name: Name of the event
  117. :type event_name: str
  118. :param callback: Function to call
  119. :type callback: func
  120. :return: Connection id
  121. :rtype: int
  122. """
  123. return self.canvas.mpl_connect(event_name, callback)
  124. def mpl_disconnect(self, cid):
  125. """
  126. Disconnect callback with the give id.
  127. :param cid: Callback id.
  128. :return: None
  129. """
  130. self.canvas.mpl_disconnect(cid)
  131. def connect(self, event_name, callback):
  132. """
  133. Attach an event handler to the canvas through the native Qt interface.
  134. :param event_name: Name of the event
  135. :type event_name: str
  136. :param callback: Function to call
  137. :type callback: function
  138. :return: Nothing
  139. """
  140. self.canvas.connect(event_name, callback)
  141. def clear(self):
  142. """
  143. Clears axes and figure.
  144. :return: None
  145. """
  146. # Clear
  147. self.axes.cla()
  148. try:
  149. self.figure.clf()
  150. except KeyError:
  151. FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
  152. # Re-build
  153. self.figure.add_axes(self.axes)
  154. self.axes.set_aspect(1)
  155. self.axes.grid(True)
  156. # Re-draw
  157. self.canvas.draw_idle()
  158. def adjust_axes(self, xmin, ymin, xmax, ymax):
  159. """
  160. Adjusts all axes while maintaining the use of the whole canvas
  161. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  162. request that will be modified to fit these restrictions.
  163. :param xmin: Requested minimum value for the X axis.
  164. :type xmin: float
  165. :param ymin: Requested minimum value for the Y axis.
  166. :type ymin: float
  167. :param xmax: Requested maximum value for the X axis.
  168. :type xmax: float
  169. :param ymax: Requested maximum value for the Y axis.
  170. :type ymax: float
  171. :return: None
  172. """
  173. # FlatCAMApp.App.log.debug("PC.adjust_axes()")
  174. width = xmax - xmin
  175. height = ymax - ymin
  176. try:
  177. r = width / height
  178. except ZeroDivisionError:
  179. FlatCAMApp.App.log.error("Height is %f" % height)
  180. return
  181. canvas_w, canvas_h = self.canvas.get_width_height()
  182. canvas_r = float(canvas_w) / canvas_h
  183. x_ratio = float(self.x_margin) / canvas_w
  184. y_ratio = float(self.y_margin) / canvas_h
  185. if r > canvas_r:
  186. ycenter = (ymin + ymax) / 2.0
  187. newheight = height * r / canvas_r
  188. ymin = ycenter - newheight / 2.0
  189. ymax = ycenter + newheight / 2.0
  190. else:
  191. xcenter = (xmax + xmin) / 2.0
  192. newwidth = width * canvas_r / r
  193. xmin = xcenter - newwidth / 2.0
  194. xmax = xcenter + newwidth / 2.0
  195. # Adjust axes
  196. for ax in self.figure.get_axes():
  197. if ax._label != 'base':
  198. ax.set_frame_on(False) # No frame
  199. ax.set_xticks([]) # No tick
  200. ax.set_yticks([]) # No ticks
  201. ax.patch.set_visible(False) # No background
  202. ax.set_aspect(1)
  203. ax.set_xlim((xmin, xmax))
  204. ax.set_ylim((ymin, ymax))
  205. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  206. # Sync re-draw to proper paint on form resize
  207. self.canvas.draw()
  208. def auto_adjust_axes(self, *args):
  209. """
  210. Calls ``adjust_axes()`` using the extents of the base axes.
  211. :rtype : None
  212. :return: None
  213. """
  214. xmin, xmax = self.axes.get_xlim()
  215. ymin, ymax = self.axes.get_ylim()
  216. self.adjust_axes(xmin, ymin, xmax, ymax)
  217. def zoom(self, factor, center=None):
  218. """
  219. Zooms the plot by factor around a given
  220. center point. Takes care of re-drawing.
  221. :param factor: Number by which to scale the plot.
  222. :type factor: float
  223. :param center: Coordinates [x, y] of the point around which to scale the plot.
  224. :type center: list
  225. :return: None
  226. """
  227. xmin, xmax = self.axes.get_xlim()
  228. ymin, ymax = self.axes.get_ylim()
  229. width = xmax - xmin
  230. height = ymax - ymin
  231. if center is None or center == [None, None]:
  232. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  233. # For keeping the point at the pointer location
  234. relx = (xmax - center[0]) / width
  235. rely = (ymax - center[1]) / height
  236. new_width = width / factor
  237. new_height = height / factor
  238. xmin = center[0] - new_width * (1 - relx)
  239. xmax = center[0] + new_width * relx
  240. ymin = center[1] - new_height * (1 - rely)
  241. ymax = center[1] + new_height * rely
  242. # Adjust axes
  243. for ax in self.figure.get_axes():
  244. ax.set_xlim((xmin, xmax))
  245. ax.set_ylim((ymin, ymax))
  246. # Async re-draw
  247. self.canvas.draw_idle()
  248. def pan(self, x, y):
  249. xmin, xmax = self.axes.get_xlim()
  250. ymin, ymax = self.axes.get_ylim()
  251. width = xmax - xmin
  252. height = ymax - ymin
  253. # Adjust axes
  254. for ax in self.figure.get_axes():
  255. ax.set_xlim((xmin + x * width, xmax + x * width))
  256. ax.set_ylim((ymin + y * height, ymax + y * height))
  257. # Re-draw
  258. self.canvas.draw_idle()
  259. def new_axes(self, name):
  260. """
  261. Creates and returns an Axes object attached to this object's Figure.
  262. :param name: Unique label for the axes.
  263. :return: Axes attached to the figure.
  264. :rtype: Axes
  265. """
  266. return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  267. def on_scroll(self, event):
  268. """
  269. Scroll event handler.
  270. :param event: Event object containing the event information.
  271. :return: None
  272. """
  273. # So it can receive key presses
  274. # self.canvas.grab_focus()
  275. self.canvas.setFocus()
  276. # Event info
  277. # z, direction = event.get_scroll_direction()
  278. if self.key is None:
  279. if event.button == 'up':
  280. self.zoom(1.5, self.mouse)
  281. else:
  282. self.zoom(1 / 1.5, self.mouse)
  283. return
  284. if self.key == 'shift':
  285. if event.button == 'up':
  286. self.pan(0.3, 0)
  287. else:
  288. self.pan(-0.3, 0)
  289. return
  290. if self.key == 'control':
  291. if event.button == 'up':
  292. self.pan(0, 0.3)
  293. else:
  294. self.pan(0, -0.3)
  295. return
  296. def on_mouse_press(self, event):
  297. # Check for middle mouse button press
  298. if event.button == 2:
  299. # Prepare axes for pan (using 'matplotlib' pan function)
  300. self.pan_axes = []
  301. for a in self.figure.get_axes():
  302. if (event.x is not None and event.y is not None and a.in_axes(event) and
  303. a.get_navigate() and a.can_pan()):
  304. a.start_pan(event.x, event.y, 1)
  305. self.pan_axes.append(a)
  306. # Set pan view flag
  307. if len(self.pan_axes) > 0: self.panning = True;
  308. def on_mouse_release(self, event):
  309. # Check for middle mouse button release to complete pan procedure
  310. if event.button == 2:
  311. for a in self.pan_axes:
  312. a.end_pan()
  313. # Clear pan flag
  314. self.panning = False
  315. def on_mouse_move(self, event):
  316. """
  317. Mouse movement event hadler. Stores the coordinates. Updates view on pan.
  318. :param event: Contains information about the event.
  319. :return: None
  320. """
  321. self.mouse = [event.xdata, event.ydata]
  322. # Update pan view on mouse move
  323. if self.panning is True:
  324. for a in self.pan_axes:
  325. a.drag_pan(1, event.key, event.x, event.y)
  326. # Async re-draw (redraws only on thread idle state, uses timer on backend)
  327. self.canvas.draw_idle()
  328. def on_draw(self, renderer):
  329. # Store background on canvas redraw
  330. self.background = self.canvas.copy_from_bbox(self.axes.bbox)