PlotCanvasLegacy.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  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 PyQt5 import QtGui, QtCore, QtWidgets
  9. # Prevent conflict with Qt5 and above.
  10. from matplotlib import use as mpl_use
  11. from matplotlib.figure import Figure
  12. from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
  13. from matplotlib.backends.backend_agg import FigureCanvasAgg
  14. from matplotlib.widgets import Cursor
  15. import FlatCAMApp
  16. import logging
  17. mpl_use("Qt5Agg")
  18. log = logging.getLogger('base')
  19. class CanvasCache(QtCore.QObject):
  20. """
  21. Case story #1:
  22. 1) No objects in the project.
  23. 2) Object is created (new_object() emits object_created(obj)).
  24. on_object_created() adds (i) object to collection and emits
  25. (ii) new_object_available() then calls (iii) object.plot()
  26. 3) object.plot() creates axes if necessary on
  27. app.collection.figure. Then plots on it.
  28. 4) Plots on a cache-size canvas (in background).
  29. 5) Plot completes. Bitmap is generated.
  30. 6) Visible canvas is painted.
  31. """
  32. # Signals:
  33. # A bitmap is ready to be displayed.
  34. new_screen = QtCore.pyqtSignal()
  35. def __init__(self, plotcanvas, app, dpi=50):
  36. super(CanvasCache, self).__init__()
  37. self.app = app
  38. self.plotcanvas = plotcanvas
  39. self.dpi = dpi
  40. self.figure = Figure(dpi=dpi)
  41. self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
  42. self.axes.set_frame_on(False)
  43. self.axes.set_xticks([])
  44. self.axes.set_yticks([])
  45. self.canvas = FigureCanvasAgg(self.figure)
  46. self.cache = None
  47. def run(self):
  48. log.debug("CanvasCache Thread Started!")
  49. self.plotcanvas.update_screen_request.connect(self.on_update_req)
  50. def on_update_req(self, extents):
  51. """
  52. Event handler for an updated display request.
  53. :param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
  54. """
  55. # log.debug("Canvas update requested: %s" % str(extents))
  56. # Note: This information below might be out of date. Establish
  57. # a protocol regarding when to change the canvas in the main
  58. # thread and when to check these values here in the background,
  59. # or pass this data in the signal (safer).
  60. # log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
  61. # log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
  62. # Move the requested screen portion to the main thread
  63. # and inform about the update:
  64. self.new_screen.emit()
  65. # Continue to update the cache.
  66. # def on_new_object_available(self):
  67. #
  68. # log.debug("A new object is available. Should plot it!")
  69. class PlotCanvasLegacy(QtCore.QObject):
  70. """
  71. Class handling the plotting area in the application.
  72. """
  73. # Signals:
  74. # Request for new bitmap to display. The parameter
  75. # is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
  76. update_screen_request = QtCore.pyqtSignal(list)
  77. double_click = QtCore.pyqtSignal(object)
  78. def __init__(self, container, app):
  79. """
  80. The constructor configures the Matplotlib figure that
  81. will contain all plots, creates the base axes and connects
  82. events to the plotting area.
  83. :param container: The parent container in which to draw plots.
  84. :rtype: PlotCanvas
  85. """
  86. super(PlotCanvasLegacy, self).__init__()
  87. self.app = app
  88. # Options
  89. self.x_margin = 15 # pixels
  90. self.y_margin = 25 # Pixels
  91. # Parent container
  92. self.container = container
  93. # Plots go onto a single matplotlib.figure
  94. self.figure = Figure(dpi=50) # TODO: dpi needed?
  95. self.figure.patch.set_visible(False)
  96. # These axes show the ticks and grid. No plotting done here.
  97. # New axes must have a label, otherwise mpl returns an existing one.
  98. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  99. self.axes.set_aspect(1)
  100. self.axes.grid(True)
  101. self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
  102. self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
  103. # The canvas is the top level container (FigureCanvasQTAgg)
  104. self.canvas = FigureCanvas(self.figure)
  105. self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
  106. self.canvas.setFocus()
  107. self.native = self.canvas
  108. # self.canvas.set_can_focus(True) # For key press
  109. # Attach to parent
  110. # self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
  111. self.container.addWidget(self.canvas) # Qt
  112. # Copy a bitmap of the canvas for quick animation.
  113. # Update every time the canvas is re-drawn.
  114. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  115. # ## Bitmap Cache
  116. self.cache = CanvasCache(self, self.app)
  117. self.cache_thread = QtCore.QThread()
  118. self.cache.moveToThread(self.cache_thread)
  119. # super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
  120. self.cache_thread.started.connect(self.cache.run)
  121. self.cache_thread.start()
  122. self.cache.new_screen.connect(self.on_new_screen)
  123. # Events
  124. self.mp = self.graph_event_connect('button_press_event', self.on_mouse_press)
  125. self.mr = self.graph_event_connect('button_release_event', self.on_mouse_release)
  126. self.mm = self.graph_event_connect('motion_notify_event', self.on_mouse_move)
  127. # self.canvas.connect('configure-event', self.auto_adjust_axes)
  128. self.aaa = self.graph_event_connect('resize_event', self.auto_adjust_axes)
  129. # self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  130. # self.canvas.connect("scroll-event", self.on_scroll)
  131. self.osc = self.graph_event_connect('scroll_event', self.on_scroll)
  132. # self.graph_event_connect('key_press_event', self.on_key_down)
  133. # self.graph_event_connect('key_release_event', self.on_key_up)
  134. self.odr = self.graph_event_connect('draw_event', self.on_draw)
  135. self.mouse = [0, 0]
  136. self.key = None
  137. self.pan_axes = []
  138. self.panning = False
  139. # signal is the mouse is dragging
  140. self.is_dragging = False
  141. # signal if there is a doubleclick
  142. self.is_dblclk = False
  143. def graph_event_connect(self, event_name, callback):
  144. """
  145. Attach an event handler to the canvas through the Matplotlib interface.
  146. :param event_name: Name of the event
  147. :type event_name: str
  148. :param callback: Function to call
  149. :type callback: func
  150. :return: Connection id
  151. :rtype: int
  152. """
  153. if event_name == 'mouse_move':
  154. event_name = 'motion_notify_event'
  155. if event_name == 'mouse_press':
  156. event_name = 'button_press_event'
  157. if event_name == 'mouse_release':
  158. event_name = 'button_release_event'
  159. if event_name == 'mouse_double_click':
  160. return self.double_click.connect(callback)
  161. if event_name == 'key_press':
  162. event_name = 'key_press_event'
  163. return self.canvas.mpl_connect(event_name, callback)
  164. def graph_event_disconnect(self, cid):
  165. """
  166. Disconnect callback with the give id.
  167. :param cid: Callback id.
  168. :return: None
  169. """
  170. # self.double_click.disconnect(cid)
  171. self.canvas.mpl_disconnect(cid)
  172. def on_new_screen(self):
  173. pass
  174. # log.debug("Cache updated the screen!")
  175. def new_cursor(self, axes=None):
  176. # if axes is None:
  177. # c = MplCursor(axes=self.axes, color='black', linewidth=1)
  178. # else:
  179. # c = MplCursor(axes=axes, color='black', linewidth=1)
  180. c = FakeCursor()
  181. return c
  182. def on_key_down(self, event):
  183. """
  184. :param event:
  185. :return:
  186. """
  187. FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
  188. self.key = event.key
  189. def on_key_up(self, event):
  190. """
  191. :param event:
  192. :return:
  193. """
  194. self.key = None
  195. def connect(self, event_name, callback):
  196. """
  197. Attach an event handler to the canvas through the native Qt interface.
  198. :param event_name: Name of the event
  199. :type event_name: str
  200. :param callback: Function to call
  201. :type callback: function
  202. :return: Nothing
  203. """
  204. self.canvas.connect(event_name, callback)
  205. def clear(self):
  206. """
  207. Clears axes and figure.
  208. :return: None
  209. """
  210. # Clear
  211. self.axes.cla()
  212. try:
  213. self.figure.clf()
  214. except KeyError:
  215. FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
  216. # Re-build
  217. self.figure.add_axes(self.axes)
  218. self.axes.set_aspect(1)
  219. self.axes.grid(True)
  220. # Re-draw
  221. self.canvas.draw_idle()
  222. def adjust_axes(self, xmin, ymin, xmax, ymax):
  223. """
  224. Adjusts all axes while maintaining the use of the whole canvas
  225. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  226. request that will be modified to fit these restrictions.
  227. :param xmin: Requested minimum value for the X axis.
  228. :type xmin: float
  229. :param ymin: Requested minimum value for the Y axis.
  230. :type ymin: float
  231. :param xmax: Requested maximum value for the X axis.
  232. :type xmax: float
  233. :param ymax: Requested maximum value for the Y axis.
  234. :type ymax: float
  235. :return: None
  236. """
  237. # FlatCAMApp.App.log.debug("PC.adjust_axes()")
  238. width = xmax - xmin
  239. height = ymax - ymin
  240. try:
  241. r = width / height
  242. except ZeroDivisionError:
  243. FlatCAMApp.App.log.error("Height is %f" % height)
  244. return
  245. canvas_w, canvas_h = self.canvas.get_width_height()
  246. canvas_r = float(canvas_w) / canvas_h
  247. x_ratio = float(self.x_margin) / canvas_w
  248. y_ratio = float(self.y_margin) / canvas_h
  249. if r > canvas_r:
  250. ycenter = (ymin + ymax) / 2.0
  251. newheight = height * r / canvas_r
  252. ymin = ycenter - newheight / 2.0
  253. ymax = ycenter + newheight / 2.0
  254. else:
  255. xcenter = (xmax + xmin) / 2.0
  256. newwidth = width * canvas_r / r
  257. xmin = xcenter - newwidth / 2.0
  258. xmax = xcenter + newwidth / 2.0
  259. # Adjust axes
  260. for ax in self.figure.get_axes():
  261. if ax._label != 'base':
  262. ax.set_frame_on(False) # No frame
  263. ax.set_xticks([]) # No tick
  264. ax.set_yticks([]) # No ticks
  265. ax.patch.set_visible(False) # No background
  266. ax.set_aspect(1)
  267. ax.set_xlim((xmin, xmax))
  268. ax.set_ylim((ymin, ymax))
  269. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  270. # Sync re-draw to proper paint on form resize
  271. self.canvas.draw()
  272. # #### Temporary place-holder for cached update #####
  273. self.update_screen_request.emit([0, 0, 0, 0, 0])
  274. def auto_adjust_axes(self, *args):
  275. """
  276. Calls ``adjust_axes()`` using the extents of the base axes.
  277. :rtype : None
  278. :return: None
  279. """
  280. xmin, xmax = self.axes.get_xlim()
  281. ymin, ymax = self.axes.get_ylim()
  282. self.adjust_axes(xmin, ymin, xmax, ymax)
  283. def zoom(self, factor, center=None):
  284. """
  285. Zooms the plot by factor around a given
  286. center point. Takes care of re-drawing.
  287. :param factor: Number by which to scale the plot.
  288. :type factor: float
  289. :param center: Coordinates [x, y] of the point around which to scale the plot.
  290. :type center: list
  291. :return: None
  292. """
  293. xmin, xmax = self.axes.get_xlim()
  294. ymin, ymax = self.axes.get_ylim()
  295. width = xmax - xmin
  296. height = ymax - ymin
  297. if center is None or center == [None, None]:
  298. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  299. # For keeping the point at the pointer location
  300. relx = (xmax - center[0]) / width
  301. rely = (ymax - center[1]) / height
  302. new_width = width / factor
  303. new_height = height / factor
  304. xmin = center[0] - new_width * (1 - relx)
  305. xmax = center[0] + new_width * relx
  306. ymin = center[1] - new_height * (1 - rely)
  307. ymax = center[1] + new_height * rely
  308. # Adjust axes
  309. for ax in self.figure.get_axes():
  310. ax.set_xlim((xmin, xmax))
  311. ax.set_ylim((ymin, ymax))
  312. # Async re-draw
  313. self.canvas.draw_idle()
  314. # #### Temporary place-holder for cached update #####
  315. self.update_screen_request.emit([0, 0, 0, 0, 0])
  316. def pan(self, x, y):
  317. xmin, xmax = self.axes.get_xlim()
  318. ymin, ymax = self.axes.get_ylim()
  319. width = xmax - xmin
  320. height = ymax - ymin
  321. # Adjust axes
  322. for ax in self.figure.get_axes():
  323. ax.set_xlim((xmin + x * width, xmax + x * width))
  324. ax.set_ylim((ymin + y * height, ymax + y * height))
  325. # Re-draw
  326. self.canvas.draw_idle()
  327. # #### Temporary place-holder for cached update #####
  328. self.update_screen_request.emit([0, 0, 0, 0, 0])
  329. def new_axes(self, name):
  330. """
  331. Creates and returns an Axes object attached to this object's Figure.
  332. :param name: Unique label for the axes.
  333. :return: Axes attached to the figure.
  334. :rtype: Axes
  335. """
  336. return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  337. def on_scroll(self, event):
  338. """
  339. Scroll event handler.
  340. :param event: Event object containing the event information.
  341. :return: None
  342. """
  343. # So it can receive key presses
  344. # self.canvas.grab_focus()
  345. self.canvas.setFocus()
  346. # Event info
  347. # z, direction = event.get_scroll_direction()
  348. if self.key is None:
  349. if event.button == 'up':
  350. self.zoom(1.5, self.mouse)
  351. else:
  352. self.zoom(1 / 1.5, self.mouse)
  353. return
  354. if self.key == 'shift':
  355. if event.button == 'up':
  356. self.pan(0.3, 0)
  357. else:
  358. self.pan(-0.3, 0)
  359. return
  360. if self.key == 'control':
  361. if event.button == 'up':
  362. self.pan(0, 0.3)
  363. else:
  364. self.pan(0, -0.3)
  365. return
  366. def on_mouse_press(self, event):
  367. self.is_dragging = True
  368. # Check for middle mouse button press
  369. if self.app.defaults["global_pan_button"] == '2':
  370. pan_button = 3 # right button for Matplotlib
  371. else:
  372. pan_button = 2 # middle button for Matplotlib
  373. if event.button == pan_button:
  374. # Prepare axes for pan (using 'matplotlib' pan function)
  375. self.pan_axes = []
  376. for a in self.figure.get_axes():
  377. if (event.x is not None and event.y is not None and a.in_axes(event) and
  378. a.get_navigate() and a.can_pan()):
  379. a.start_pan(event.x, event.y, 1)
  380. self.pan_axes.append(a)
  381. # Set pan view flag
  382. if len(self.pan_axes) > 0:
  383. self.panning = True
  384. if event.dblclick:
  385. self.double_click.emit(event)
  386. def on_mouse_release(self, event):
  387. self.is_dragging = False
  388. # Check for middle mouse button release to complete pan procedure
  389. # Check for middle mouse button press
  390. if self.app.defaults["global_pan_button"] == '2':
  391. pan_button = 3 # right button for Matplotlib
  392. else:
  393. pan_button = 2 # middle button for Matplotlib
  394. if event.button == pan_button:
  395. for a in self.pan_axes:
  396. a.end_pan()
  397. # Clear pan flag
  398. self.panning = False
  399. def on_mouse_move(self, event):
  400. """
  401. Mouse movement event hadler. Stores the coordinates. Updates view on pan.
  402. :param event: Contains information about the event.
  403. :return: None
  404. """
  405. try:
  406. x = float(event.xdata)
  407. y = float(event.ydata)
  408. except TypeError:
  409. return
  410. self.mouse = [event.xdata, event.ydata]
  411. self.canvas.restore_region(self.background)
  412. # Update pan view on mouse move
  413. if self.panning is True:
  414. # x_pan, y_pan = self.app.geo_editor.snap(event.xdata, event.ydata)
  415. # self.app.app_cursor.set_data(event, (x_pan, y_pan))
  416. for a in self.pan_axes:
  417. a.drag_pan(1, event.key, event.x, event.y)
  418. # Async re-draw (redraws only on thread idle state, uses timer on backend)
  419. self.canvas.draw_idle()
  420. # #### Temporary place-holder for cached update #####
  421. self.update_screen_request.emit([0, 0, 0, 0, 0])
  422. x, y = self.app.geo_editor.snap(x, y)
  423. if self.app.app_cursor.enabled is True:
  424. # Pointer (snapped)
  425. elements = self.axes.plot(x, y, 'k+', ms=40, mew=2, animated=True)
  426. for el in elements:
  427. self.axes.draw_artist(el)
  428. self.canvas.blit(self.axes.bbox)
  429. def translate_coords(self, position):
  430. """
  431. This does not do much. It's just for code compatibility
  432. :param position: Mouse event position
  433. :return: Tuple with mouse position
  434. """
  435. return (position[0], position[1])
  436. def on_draw(self, renderer):
  437. # Store background on canvas redraw
  438. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  439. def get_axes_pixelsize(self):
  440. """
  441. Axes size in pixels.
  442. :return: Pixel width and height
  443. :rtype: tuple
  444. """
  445. bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
  446. width, height = bbox.width, bbox.height
  447. width *= self.figure.dpi
  448. height *= self.figure.dpi
  449. return width, height
  450. def get_density(self):
  451. """
  452. Returns unit length per pixel on horizontal
  453. and vertical axes.
  454. :return: X and Y density
  455. :rtype: tuple
  456. """
  457. xpx, ypx = self.get_axes_pixelsize()
  458. xmin, xmax = self.axes.get_xlim()
  459. ymin, ymax = self.axes.get_ylim()
  460. width = xmax - xmin
  461. height = ymax - ymin
  462. return width / xpx, height / ypx
  463. class FakeCursor():
  464. def __init__(self):
  465. self._enabled = True
  466. @property
  467. def enabled(self):
  468. return True if self._enabled else False
  469. @enabled.setter
  470. def enabled(self, value):
  471. self._enabled = value
  472. class MplCursor(Cursor):
  473. def __init__(self, axes, color='red', linewidth=1):
  474. super().__init__(ax=axes, useblit=True, color=color, linewidth=linewidth)
  475. self._enabled = True
  476. self.axes = axes
  477. self.color = color
  478. self.linewidth = linewidth
  479. self.x = None
  480. self.y = None
  481. @property
  482. def enabled(self):
  483. return True if self._enabled else False
  484. @enabled.setter
  485. def enabled(self, value):
  486. self._enabled = value
  487. self.visible = self._enabled
  488. self.canvas.draw()
  489. def onmove(self, event):
  490. pass
  491. def set_data(self, event, pos):
  492. """Internal event handler to draw the cursor when the mouse moves."""
  493. self.x = pos[0]
  494. self.y = pos[1]
  495. if self.ignore(event):
  496. return
  497. if not self.canvas.widgetlock.available(self):
  498. return
  499. if event.inaxes != self.ax:
  500. self.linev.set_visible(False)
  501. self.lineh.set_visible(False)
  502. if self.needclear:
  503. self.canvas.draw()
  504. self.needclear = False
  505. return
  506. self.needclear = True
  507. if not self.visible:
  508. return
  509. self.linev.set_xdata((self.x, self.x))
  510. self.lineh.set_ydata((self.y, self.y))
  511. self.linev.set_visible(self.visible and self.vertOn)
  512. self.lineh.set_visible(self.visible and self.horizOn)
  513. self._update()
  514. class ShapeCollectionLegacy():
  515. def __init__(self):
  516. self._shapes = []
  517. def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
  518. update=False, layer=1, tolerance=0.01):
  519. try:
  520. for sh in shape:
  521. self._shapes.append(sh)
  522. except TypeError:
  523. self._shapes.append(shape)
  524. return len(self._shapes) - 1
  525. def clear(self, update=None):
  526. self._shapes[:] = []
  527. if update is True:
  528. self.redraw()
  529. def redraw(self):
  530. pass