PlotCanvasLegacy.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  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. # Modified by Marius Stanciu 09/21/2019 #
  8. ############################################################
  9. from PyQt5 import QtGui, QtCore, QtWidgets
  10. from PyQt5.QtCore import pyqtSignal
  11. # Prevent conflict with Qt5 and above.
  12. from matplotlib import use as mpl_use
  13. mpl_use("Qt5Agg")
  14. from matplotlib.figure import Figure
  15. from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
  16. from matplotlib.backends.backend_agg import FigureCanvasAgg
  17. from matplotlib.widgets import Cursor
  18. # needed for legacy mode
  19. # Used for solid polygons in Matplotlib
  20. from descartes.patch import PolygonPatch
  21. from shapely.geometry import Polygon, LineString, LinearRing, Point, MultiPolygon, MultiLineString
  22. import FlatCAMApp
  23. from copy import deepcopy
  24. import logging
  25. import traceback
  26. import gettext
  27. import FlatCAMTranslation as fcTranslate
  28. import builtins
  29. fcTranslate.apply_language('strings')
  30. if '_' not in builtins.__dict__:
  31. _ = gettext.gettext
  32. log = logging.getLogger('base')
  33. class CanvasCache(QtCore.QObject):
  34. """
  35. Case story #1:
  36. 1) No objects in the project.
  37. 2) Object is created (new_object() emits object_created(obj)).
  38. on_object_created() adds (i) object to collection and emits
  39. (ii) new_object_available() then calls (iii) object.plot()
  40. 3) object.plot() creates axes if necessary on
  41. app.collection.figure. Then plots on it.
  42. 4) Plots on a cache-size canvas (in background).
  43. 5) Plot completes. Bitmap is generated.
  44. 6) Visible canvas is painted.
  45. """
  46. # Signals:
  47. # A bitmap is ready to be displayed.
  48. new_screen = QtCore.pyqtSignal()
  49. def __init__(self, plotcanvas, app, dpi=50):
  50. super(CanvasCache, self).__init__()
  51. self.app = app
  52. self.plotcanvas = plotcanvas
  53. self.dpi = dpi
  54. self.figure = Figure(dpi=dpi)
  55. self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
  56. self.axes.set_frame_on(False)
  57. self.axes.set_xticks([])
  58. self.axes.set_yticks([])
  59. self.canvas = FigureCanvasAgg(self.figure)
  60. self.cache = None
  61. def run(self):
  62. log.debug("CanvasCache Thread Started!")
  63. self.plotcanvas.update_screen_request.connect(self.on_update_req)
  64. def on_update_req(self, extents):
  65. """
  66. Event handler for an updated display request.
  67. :param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
  68. """
  69. # log.debug("Canvas update requested: %s" % str(extents))
  70. # Note: This information below might be out of date. Establish
  71. # a protocol regarding when to change the canvas in the main
  72. # thread and when to check these values here in the background,
  73. # or pass this data in the signal (safer).
  74. # log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
  75. # log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
  76. # Move the requested screen portion to the main thread
  77. # and inform about the update:
  78. self.new_screen.emit()
  79. # Continue to update the cache.
  80. # def on_new_object_available(self):
  81. #
  82. # log.debug("A new object is available. Should plot it!")
  83. class FigureCanvas(FigureCanvasQTAgg):
  84. """
  85. Reimplemented this so I can emit a signal when the idle drawing is finished and display the mouse shape
  86. """
  87. idle_drawing_finished = pyqtSignal()
  88. def __init__(self, figure):
  89. super().__init__(figure=figure)
  90. def _draw_idle(self):
  91. if self.height() < 0 or self.width() < 0:
  92. self._draw_pending = False
  93. if not self._draw_pending:
  94. return
  95. try:
  96. self.draw()
  97. except Exception:
  98. # Uncaught exceptions are fatal for PyQt5, so catch them instead.
  99. traceback.print_exc()
  100. finally:
  101. self._draw_pending = False
  102. # I reimplemented this class only to launch this signal
  103. self.idle_drawing_finished.emit()
  104. class PlotCanvasLegacy(QtCore.QObject):
  105. """
  106. Class handling the plotting area in the application.
  107. """
  108. # Signals:
  109. # Request for new bitmap to display. The parameter
  110. # is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
  111. update_screen_request = QtCore.pyqtSignal(list)
  112. double_click = QtCore.pyqtSignal(object)
  113. def __init__(self, container, app):
  114. """
  115. The constructor configures the Matplotlib figure that
  116. will contain all plots, creates the base axes and connects
  117. events to the plotting area.
  118. :param container: The parent container in which to draw plots.
  119. :rtype: PlotCanvas
  120. """
  121. super(PlotCanvasLegacy, self).__init__()
  122. self.app = app
  123. # Options
  124. self.x_margin = 15 # pixels
  125. self.y_margin = 25 # Pixels
  126. # Parent container
  127. self.container = container
  128. # Plots go onto a single matplotlib.figure
  129. self.figure = Figure(dpi=50) # TODO: dpi needed?
  130. self.figure.patch.set_visible(False)
  131. # These axes show the ticks and grid. No plotting done here.
  132. # New axes must have a label, otherwise mpl returns an existing one.
  133. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  134. self.axes.set_aspect(1)
  135. self.axes.grid(True)
  136. self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
  137. self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
  138. # The canvas is the top level container (FigureCanvasQTAgg)
  139. self.canvas = FigureCanvas(self.figure)
  140. self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
  141. self.canvas.setFocus()
  142. self.native = self.canvas
  143. self.adjust_axes(-10, -10, 100, 100)
  144. # self.canvas.set_can_focus(True) # For key press
  145. # Attach to parent
  146. # self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
  147. self.container.addWidget(self.canvas) # Qt
  148. # Copy a bitmap of the canvas for quick animation.
  149. # Update every time the canvas is re-drawn.
  150. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  151. # ## Bitmap Cache
  152. self.cache = CanvasCache(self, self.app)
  153. self.cache_thread = QtCore.QThread()
  154. self.cache.moveToThread(self.cache_thread)
  155. # super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
  156. self.cache_thread.started.connect(self.cache.run)
  157. self.cache_thread.start()
  158. self.cache.new_screen.connect(self.on_new_screen)
  159. # Events
  160. self.mp = self.graph_event_connect('button_press_event', self.on_mouse_press)
  161. self.mr = self.graph_event_connect('button_release_event', self.on_mouse_release)
  162. self.mm = self.graph_event_connect('motion_notify_event', self.on_mouse_move)
  163. # self.canvas.connect('configure-event', self.auto_adjust_axes)
  164. self.aaa = self.graph_event_connect('resize_event', self.auto_adjust_axes)
  165. # self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  166. # self.canvas.connect("scroll-event", self.on_scroll)
  167. self.osc = self.graph_event_connect('scroll_event', self.on_scroll)
  168. # self.graph_event_connect('key_press_event', self.on_key_down)
  169. # self.graph_event_connect('key_release_event', self.on_key_up)
  170. self.odr = self.graph_event_connect('draw_event', self.on_draw)
  171. self.mouse = [0, 0]
  172. self.key = None
  173. self.pan_axes = []
  174. self.panning = False
  175. # signal is the mouse is dragging
  176. self.is_dragging = False
  177. # signal if there is a doubleclick
  178. self.is_dblclk = False
  179. # pay attention, this signal should be connected only after the self.canvas and self.mouse is declared
  180. self.canvas.idle_drawing_finished.connect(lambda: self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1]))
  181. def graph_event_connect(self, event_name, callback):
  182. """
  183. Attach an event handler to the canvas through the Matplotlib interface.
  184. :param event_name: Name of the event
  185. :type event_name: str
  186. :param callback: Function to call
  187. :type callback: func
  188. :return: Connection id
  189. :rtype: int
  190. """
  191. if event_name == 'mouse_move':
  192. event_name = 'motion_notify_event'
  193. if event_name == 'mouse_press':
  194. event_name = 'button_press_event'
  195. if event_name == 'mouse_release':
  196. event_name = 'button_release_event'
  197. if event_name == 'mouse_double_click':
  198. return self.double_click.connect(callback)
  199. if event_name == 'key_press':
  200. event_name = 'key_press_event'
  201. return self.canvas.mpl_connect(event_name, callback)
  202. def graph_event_disconnect(self, cid):
  203. """
  204. Disconnect callback with the give id.
  205. :param cid: Callback id.
  206. :return: None
  207. """
  208. # self.double_click.disconnect(cid)
  209. self.canvas.mpl_disconnect(cid)
  210. def on_new_screen(self):
  211. pass
  212. # log.debug("Cache updated the screen!")
  213. def new_cursor(self, axes=None):
  214. # if axes is None:
  215. # c = MplCursor(axes=self.axes, color='black', linewidth=1)
  216. # else:
  217. # c = MplCursor(axes=axes, color='black', linewidth=1)
  218. c = FakeCursor()
  219. try:
  220. c.mouse_state_updated.connect(self.clear_cursor)
  221. except Exception as e:
  222. print(str(e))
  223. return c
  224. def clear_cursor(self, state):
  225. if state is True:
  226. self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
  227. else:
  228. self.canvas.restore_region(self.background)
  229. self.canvas.blit(self.axes.bbox)
  230. def on_key_down(self, event):
  231. """
  232. :param event:
  233. :return:
  234. """
  235. FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
  236. self.key = event.key
  237. def on_key_up(self, event):
  238. """
  239. :param event:
  240. :return:
  241. """
  242. self.key = None
  243. def connect(self, event_name, callback):
  244. """
  245. Attach an event handler to the canvas through the native Qt interface.
  246. :param event_name: Name of the event
  247. :type event_name: str
  248. :param callback: Function to call
  249. :type callback: function
  250. :return: Nothing
  251. """
  252. self.canvas.connect(event_name, callback)
  253. def clear(self):
  254. """
  255. Clears axes and figure.
  256. :return: None
  257. """
  258. # Clear
  259. self.axes.cla()
  260. try:
  261. self.figure.clf()
  262. except KeyError:
  263. FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
  264. # Re-build
  265. self.figure.add_axes(self.axes)
  266. self.axes.set_aspect(1)
  267. self.axes.grid(True)
  268. self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
  269. self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
  270. self.adjust_axes(-10, -10, 100, 100)
  271. # Re-draw
  272. self.canvas.draw_idle()
  273. def redraw(self):
  274. """
  275. Created only to serve for compatibility with the VisPy plotcanvas (the other graphic engine, 3D)
  276. :return:
  277. """
  278. self.clear()
  279. def adjust_axes(self, xmin, ymin, xmax, ymax):
  280. """
  281. Adjusts all axes while maintaining the use of the whole canvas
  282. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  283. request that will be modified to fit these restrictions.
  284. :param xmin: Requested minimum value for the X axis.
  285. :type xmin: float
  286. :param ymin: Requested minimum value for the Y axis.
  287. :type ymin: float
  288. :param xmax: Requested maximum value for the X axis.
  289. :type xmax: float
  290. :param ymax: Requested maximum value for the Y axis.
  291. :type ymax: float
  292. :return: None
  293. """
  294. # FlatCAMApp.App.log.debug("PC.adjust_axes()")
  295. if not self.app.collection.get_list():
  296. xmin = -10
  297. ymin = -10
  298. xmax = 100
  299. ymax = 100
  300. width = xmax - xmin
  301. height = ymax - ymin
  302. try:
  303. r = width / height
  304. except ZeroDivisionError:
  305. FlatCAMApp.App.log.error("Height is %f" % height)
  306. return
  307. canvas_w, canvas_h = self.canvas.get_width_height()
  308. canvas_r = float(canvas_w) / canvas_h
  309. x_ratio = float(self.x_margin) / canvas_w
  310. y_ratio = float(self.y_margin) / canvas_h
  311. if r > canvas_r:
  312. ycenter = (ymin + ymax) / 2.0
  313. newheight = height * r / canvas_r
  314. ymin = ycenter - newheight / 2.0
  315. ymax = ycenter + newheight / 2.0
  316. else:
  317. xcenter = (xmax + xmin) / 2.0
  318. newwidth = width * canvas_r / r
  319. xmin = xcenter - newwidth / 2.0
  320. xmax = xcenter + newwidth / 2.0
  321. # Adjust axes
  322. for ax in self.figure.get_axes():
  323. if ax._label != 'base':
  324. ax.set_frame_on(False) # No frame
  325. ax.set_xticks([]) # No tick
  326. ax.set_yticks([]) # No ticks
  327. ax.patch.set_visible(False) # No background
  328. ax.set_aspect(1)
  329. ax.set_xlim((xmin, xmax))
  330. ax.set_ylim((ymin, ymax))
  331. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  332. # Sync re-draw to proper paint on form resize
  333. self.canvas.draw()
  334. # #### Temporary place-holder for cached update #####
  335. self.update_screen_request.emit([0, 0, 0, 0, 0])
  336. def auto_adjust_axes(self, *args):
  337. """
  338. Calls ``adjust_axes()`` using the extents of the base axes.
  339. :rtype : None
  340. :return: None
  341. """
  342. xmin, xmax = self.axes.get_xlim()
  343. ymin, ymax = self.axes.get_ylim()
  344. self.adjust_axes(xmin, ymin, xmax, ymax)
  345. def fit_view(self):
  346. self.auto_adjust_axes()
  347. def zoom(self, factor, center=None):
  348. """
  349. Zooms the plot by factor around a given
  350. center point. Takes care of re-drawing.
  351. :param factor: Number by which to scale the plot.
  352. :type factor: float
  353. :param center: Coordinates [x, y] of the point around which to scale the plot.
  354. :type center: list
  355. :return: None
  356. """
  357. factor = 1 / factor
  358. xmin, xmax = self.axes.get_xlim()
  359. ymin, ymax = self.axes.get_ylim()
  360. width = xmax - xmin
  361. height = ymax - ymin
  362. if center is None or center == [None, None]:
  363. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  364. # For keeping the point at the pointer location
  365. relx = (xmax - center[0]) / width
  366. rely = (ymax - center[1]) / height
  367. new_width = width / factor
  368. new_height = height / factor
  369. xmin = center[0] - new_width * (1 - relx)
  370. xmax = center[0] + new_width * relx
  371. ymin = center[1] - new_height * (1 - rely)
  372. ymax = center[1] + new_height * rely
  373. # Adjust axes
  374. for ax in self.figure.get_axes():
  375. ax.set_xlim((xmin, xmax))
  376. ax.set_ylim((ymin, ymax))
  377. # Async re-draw
  378. self.canvas.draw_idle()
  379. # #### Temporary place-holder for cached update #####
  380. self.update_screen_request.emit([0, 0, 0, 0, 0])
  381. def pan(self, x, y):
  382. xmin, xmax = self.axes.get_xlim()
  383. ymin, ymax = self.axes.get_ylim()
  384. width = xmax - xmin
  385. height = ymax - ymin
  386. # Adjust axes
  387. for ax in self.figure.get_axes():
  388. ax.set_xlim((xmin + x * width, xmax + x * width))
  389. ax.set_ylim((ymin + y * height, ymax + y * height))
  390. # Re-draw
  391. self.canvas.draw_idle()
  392. # #### Temporary place-holder for cached update #####
  393. self.update_screen_request.emit([0, 0, 0, 0, 0])
  394. def new_axes(self, name):
  395. """
  396. Creates and returns an Axes object attached to this object's Figure.
  397. :param name: Unique label for the axes.
  398. :return: Axes attached to the figure.
  399. :rtype: Axes
  400. """
  401. return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  402. def remove_current_axes(self):
  403. """
  404. :return: The name of the deleted axes
  405. """
  406. axes_to_remove = self.figure.axes.gca()
  407. current_axes_name = deepcopy(axes_to_remove._label)
  408. self.figure.axes.remove(axes_to_remove)
  409. return current_axes_name
  410. def on_scroll(self, event):
  411. """
  412. Scroll event handler.
  413. :param event: Event object containing the event information.
  414. :return: None
  415. """
  416. # So it can receive key presses
  417. # self.canvas.grab_focus()
  418. self.canvas.setFocus()
  419. # Event info
  420. # z, direction = event.get_scroll_direction()
  421. if self.key is None:
  422. if event.button == 'up':
  423. self.zoom(1 / 1.5, self.mouse)
  424. else:
  425. self.zoom(1.5, self.mouse)
  426. return
  427. if self.key == 'shift':
  428. if event.button == 'up':
  429. self.pan(0.3, 0)
  430. else:
  431. self.pan(-0.3, 0)
  432. return
  433. if self.key == 'control':
  434. if event.button == 'up':
  435. self.pan(0, 0.3)
  436. else:
  437. self.pan(0, -0.3)
  438. return
  439. def on_mouse_press(self, event):
  440. self.is_dragging = True
  441. # Check for middle mouse button press
  442. if self.app.defaults["global_pan_button"] == '2':
  443. pan_button = 3 # right button for Matplotlib
  444. else:
  445. pan_button = 2 # middle button for Matplotlib
  446. if event.button == pan_button:
  447. # Prepare axes for pan (using 'matplotlib' pan function)
  448. self.pan_axes = []
  449. for a in self.figure.get_axes():
  450. if (event.x is not None and event.y is not None and a.in_axes(event) and
  451. a.get_navigate() and a.can_pan()):
  452. a.start_pan(event.x, event.y, 1)
  453. self.pan_axes.append(a)
  454. # Set pan view flag
  455. if len(self.pan_axes) > 0:
  456. self.panning = True
  457. if event.dblclick:
  458. self.double_click.emit(event)
  459. def on_mouse_release(self, event):
  460. self.is_dragging = False
  461. # Check for middle mouse button release to complete pan procedure
  462. # Check for middle mouse button press
  463. if self.app.defaults["global_pan_button"] == '2':
  464. pan_button = 3 # right button for Matplotlib
  465. else:
  466. pan_button = 2 # middle button for Matplotlib
  467. if event.button == pan_button:
  468. for a in self.pan_axes:
  469. a.end_pan()
  470. # Clear pan flag
  471. self.panning = False
  472. # And update the cursor
  473. self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
  474. def on_mouse_move(self, event):
  475. """
  476. Mouse movement event handler. Stores the coordinates. Updates view on pan.
  477. :param event: Contains information about the event.
  478. :return: None
  479. """
  480. try:
  481. x = float(event.xdata)
  482. y = float(event.ydata)
  483. except TypeError:
  484. return
  485. self.mouse = [event.xdata, event.ydata]
  486. self.canvas.restore_region(self.background)
  487. # Update pan view on mouse move
  488. if self.panning is True:
  489. for a in self.pan_axes:
  490. a.drag_pan(1, event.key, event.x, event.y)
  491. # x_pan, y_pan = self.app.geo_editor.snap(event.xdata, event.ydata)
  492. # self.draw_cursor(x_pos=x_pan, y_pos=y_pan)
  493. # Async re-draw (redraws only on thread idle state, uses timer on backend)
  494. self.canvas.draw_idle()
  495. # #### Temporary place-holder for cached update #####
  496. self.update_screen_request.emit([0, 0, 0, 0, 0])
  497. self.draw_cursor(x_pos=x, y_pos=y)
  498. # self.canvas.blit(self.axes.bbox)
  499. def draw_cursor(self, x_pos, y_pos):
  500. """
  501. Draw a cursor at the mouse grid snapped position
  502. :param x_pos: mouse x position
  503. :param y_pos: mouse y position
  504. :return:
  505. """
  506. # there is no point in drawing mouse cursor when panning as it jumps in a confusing way
  507. if self.app.app_cursor.enabled is True and self.panning is False:
  508. try:
  509. x, y = self.app.geo_editor.snap(x_pos, y_pos)
  510. # Pointer (snapped)
  511. elements = self.axes.plot(x, y, 'k+', ms=40, mew=2, animated=True)
  512. for el in elements:
  513. self.axes.draw_artist(el)
  514. except Exception as e:
  515. # this happen at app initialization since self.app.geo_editor does not exist yet
  516. # I could reshuffle the object instantiating order but what's the point? I could crash something else
  517. # and that's pythonic, too
  518. pass
  519. self.canvas.blit(self.axes.bbox)
  520. def translate_coords(self, position):
  521. """
  522. This does not do much. It's just for code compatibility
  523. :param position: Mouse event position
  524. :return: Tuple with mouse position
  525. """
  526. return position[0], position[1]
  527. def on_draw(self, renderer):
  528. # Store background on canvas redraw
  529. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  530. def get_axes_pixelsize(self):
  531. """
  532. Axes size in pixels.
  533. :return: Pixel width and height
  534. :rtype: tuple
  535. """
  536. bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
  537. width, height = bbox.width, bbox.height
  538. width *= self.figure.dpi
  539. height *= self.figure.dpi
  540. return width, height
  541. def get_density(self):
  542. """
  543. Returns unit length per pixel on horizontal
  544. and vertical axes.
  545. :return: X and Y density
  546. :rtype: tuple
  547. """
  548. xpx, ypx = self.get_axes_pixelsize()
  549. xmin, xmax = self.axes.get_xlim()
  550. ymin, ymax = self.axes.get_ylim()
  551. width = xmax - xmin
  552. height = ymax - ymin
  553. return width / xpx, height / ypx
  554. class FakeCursor(QtCore.QObject):
  555. """
  556. This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
  557. This way I don't have to chane (disable) things related to the cursor all over when
  558. using the low performance Matplotlib 2D graphic engine.
  559. """
  560. mouse_state_updated = pyqtSignal(bool)
  561. def __init__(self):
  562. super().__init__()
  563. self._enabled = True
  564. @property
  565. def enabled(self):
  566. return True if self._enabled else False
  567. @enabled.setter
  568. def enabled(self, value):
  569. self._enabled = value
  570. self.mouse_state_updated.emit(value)
  571. def set_data(self, pos, **kwargs):
  572. """Internal event handler to draw the cursor when the mouse moves."""
  573. pass
  574. class MplCursor(Cursor):
  575. """
  576. Unfortunately this gets attached to the current axes and if a new axes is added
  577. it will not be showed until that axes is deleted.
  578. Not the kind of behavior needed here so I don't use it anymore.
  579. """
  580. def __init__(self, axes, color='red', linewidth=1):
  581. super().__init__(ax=axes, useblit=True, color=color, linewidth=linewidth)
  582. self._enabled = True
  583. self.axes = axes
  584. self.color = color
  585. self.linewidth = linewidth
  586. self.x = None
  587. self.y = None
  588. @property
  589. def enabled(self):
  590. return True if self._enabled else False
  591. @enabled.setter
  592. def enabled(self, value):
  593. self._enabled = value
  594. self.visible = self._enabled
  595. self.canvas.draw()
  596. def onmove(self, event):
  597. pass
  598. def set_data(self, event, pos):
  599. """Internal event handler to draw the cursor when the mouse moves."""
  600. self.x = pos[0]
  601. self.y = pos[1]
  602. if self.ignore(event):
  603. return
  604. if not self.canvas.widgetlock.available(self):
  605. return
  606. if event.inaxes != self.ax:
  607. self.linev.set_visible(False)
  608. self.lineh.set_visible(False)
  609. if self.needclear:
  610. self.canvas.draw()
  611. self.needclear = False
  612. return
  613. self.needclear = True
  614. if not self.visible:
  615. return
  616. self.linev.set_xdata((self.x, self.x))
  617. self.lineh.set_ydata((self.y, self.y))
  618. self.linev.set_visible(self.visible and self.vertOn)
  619. self.lineh.set_visible(self.visible and self.horizOn)
  620. self._update()
  621. class ShapeCollectionLegacy:
  622. """
  623. This will create the axes for each collection of shapes and will also
  624. hold the collection of shapes into a dict self._shapes.
  625. This handles the shapes redraw on canvas.
  626. """
  627. def __init__(self, obj, app, name=None, annotation_job=None):
  628. """
  629. :param obj: this is the object to which the shapes collection is attached and for
  630. which it will have to draw shapes
  631. :param app: this is the FLatCAM.App usually, needed because we have to access attributes there
  632. :param name: this is the name given to the Matplotlib axes; it needs to be unique due of Matplotlib requurements
  633. :param annotation_job: make this True if the job needed is just for annotation
  634. """
  635. self.obj = obj
  636. self.app = app
  637. self.annotation_job = annotation_job
  638. self._shapes = dict()
  639. self.shape_dict = dict()
  640. self.shape_id = 0
  641. self._color = None
  642. self._face_color = None
  643. self._visible = True
  644. self._update = False
  645. self._alpha = None
  646. self._tool_tolerance = None
  647. self._tooldia = None
  648. self._obj = None
  649. self._gcode_parsed = None
  650. if name is None:
  651. axes_name = self.obj.options['name']
  652. else:
  653. axes_name = name
  654. # Axes must exist and be attached to canvas.
  655. if axes_name not in self.app.plotcanvas.figure.axes:
  656. self.axes = self.app.plotcanvas.new_axes(axes_name)
  657. def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
  658. update=False, layer=1, tolerance=0.01, obj=None, gcode_parsed=None, tool_tolerance=None, tooldia=None):
  659. """
  660. This function will add shapes to the shape collection
  661. :param shape: the Shapely shape to be added to the shape collection
  662. :param color: edge color of the shape, hex value
  663. :param face_color: the body color of the shape, hex value
  664. :param alpha: level of transparency of the shape [0.0 ... 1.0]; Float
  665. :param visible: if True will allow the shapes to be added
  666. :param update: not used; just for compatibility with VIsPy canvas
  667. :param layer: just for compatibility with VIsPy canvas
  668. :param tolerance: just for compatibility with VIsPy canvas
  669. :param obj: not used
  670. :param gcode_parsed: not used; just for compatibility with VIsPy canvas
  671. :param tool_tolerance: just for compatibility with VIsPy canvas
  672. :param tooldia:
  673. :return:
  674. """
  675. self._color = color[:-2] if color is not None else None
  676. self._face_color = face_color[:-2] if face_color is not None else None
  677. self._alpha = int(face_color[-2:], 16) / 255 if face_color is not None else 0.75
  678. if alpha is not None:
  679. self._alpha = alpha
  680. self._visible = visible
  681. self._update = update
  682. # CNCJob object related arguments
  683. self._obj = obj
  684. self._gcode_parsed = gcode_parsed
  685. self._tool_tolerance = tool_tolerance
  686. self._tooldia = tooldia
  687. # if self._update:
  688. # self.clear()
  689. try:
  690. for sh in shape:
  691. self.shape_id += 1
  692. self.shape_dict.update({
  693. 'color': self._color,
  694. 'face_color': self._face_color,
  695. 'alpha': self._alpha,
  696. 'shape': sh
  697. })
  698. self._shapes.update({
  699. self.shape_id: deepcopy(self.shape_dict)
  700. })
  701. except TypeError:
  702. self.shape_id += 1
  703. self.shape_dict.update({
  704. 'color': self._color,
  705. 'face_color': self._face_color,
  706. 'alpha': self._alpha,
  707. 'shape': shape
  708. })
  709. self._shapes.update({
  710. self.shape_id: deepcopy(self.shape_dict)
  711. })
  712. return self.shape_id
  713. def clear(self, update=None):
  714. """
  715. Clear the canvas of the shapes.
  716. :param update:
  717. :return: None
  718. """
  719. self._shapes.clear()
  720. self.shape_id = 0
  721. self.axes.cla()
  722. self.app.plotcanvas.auto_adjust_axes()
  723. if update is True:
  724. self.redraw()
  725. def redraw(self):
  726. """
  727. This draw the shapes in the shapes collection, on canvas
  728. :return: None
  729. """
  730. path_num = 0
  731. local_shapes = deepcopy(self._shapes)
  732. try:
  733. obj_type = self.obj.kind
  734. except AttributeError:
  735. obj_type = 'utility'
  736. if self._visible:
  737. for element in local_shapes:
  738. if obj_type == 'excellon':
  739. # Plot excellon (All polygons?)
  740. if self.obj.options["solid"] and isinstance(local_shapes[element]['shape'], Polygon):
  741. patch = PolygonPatch(local_shapes[element]['shape'],
  742. facecolor="#C40000",
  743. edgecolor="#750000",
  744. alpha=local_shapes[element]['alpha'],
  745. zorder=3)
  746. self.axes.add_patch(patch)
  747. else:
  748. x, y = local_shapes[element]['shape'].exterior.coords.xy
  749. self.axes.plot(x, y, 'r-')
  750. for ints in local_shapes[element]['shape'].interiors:
  751. x, y = ints.coords.xy
  752. self.axes.plot(x, y, 'o-')
  753. elif obj_type == 'geometry':
  754. if type(local_shapes[element]['shape']) == Polygon:
  755. x, y = local_shapes[element]['shape'].exterior.coords.xy
  756. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
  757. for ints in local_shapes[element]['shape'].interiors:
  758. x, y = ints.coords.xy
  759. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
  760. elif type(local_shapes[element]['shape']) == LineString or \
  761. type(local_shapes[element]['shape']) == LinearRing:
  762. x, y = local_shapes[element]['shape'].coords.xy
  763. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
  764. elif obj_type == 'gerber':
  765. if self.obj.options["multicolored"]:
  766. linespec = '-'
  767. else:
  768. linespec = 'k-'
  769. if self.obj.options["solid"]:
  770. try:
  771. patch = PolygonPatch(local_shapes[element]['shape'],
  772. facecolor=local_shapes[element]['face_color'],
  773. edgecolor=local_shapes[element]['color'],
  774. alpha=local_shapes[element]['alpha'],
  775. zorder=2)
  776. self.axes.add_patch(patch)
  777. except AssertionError:
  778. FlatCAMApp.App.log.warning("A geometry component was not a polygon:")
  779. FlatCAMApp.App.log.warning(str(element))
  780. else:
  781. x, y = local_shapes[element]['shape'].exterior.xy
  782. self.axes.plot(x, y, linespec)
  783. for ints in local_shapes[element]['shape'].interiors:
  784. x, y = ints.coords.xy
  785. self.axes.plot(x, y, linespec)
  786. elif obj_type == 'cncjob':
  787. if local_shapes[element]['face_color'] is None:
  788. linespec = '--'
  789. linecolor = local_shapes[element]['color']
  790. # if geo['kind'][0] == 'C':
  791. # linespec = 'k-'
  792. x, y = local_shapes[element]['shape'].coords.xy
  793. self.axes.plot(x, y, linespec, color=linecolor)
  794. else:
  795. path_num += 1
  796. if isinstance(local_shapes[element]['shape'], Polygon):
  797. self.axes.annotate(str(path_num), xy=local_shapes[element]['shape'].exterior.coords[0],
  798. xycoords='data', fontsize=20)
  799. else:
  800. self.axes.annotate(str(path_num), xy=local_shapes[element]['shape'].coords[0],
  801. xycoords='data', fontsize=20)
  802. patch = PolygonPatch(local_shapes[element]['shape'],
  803. facecolor=local_shapes[element]['face_color'],
  804. edgecolor=local_shapes[element]['color'],
  805. alpha=local_shapes[element]['alpha'], zorder=2)
  806. self.axes.add_patch(patch)
  807. elif obj_type == 'utility':
  808. # not a FlatCAM object, must be utility
  809. if local_shapes[element]['face_color']:
  810. try:
  811. patch = PolygonPatch(local_shapes[element]['shape'],
  812. facecolor=local_shapes[element]['face_color'],
  813. edgecolor=local_shapes[element]['color'],
  814. alpha=local_shapes[element]['alpha'],
  815. zorder=2)
  816. self.axes.add_patch(patch)
  817. except Exception as e:
  818. log.debug("ShapeCollectionLegacy.redraw() --> %s" % str(e))
  819. else:
  820. if isinstance(local_shapes[element]['shape'], Polygon):
  821. x, y = local_shapes[element]['shape'].exterior.xy
  822. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
  823. for ints in local_shapes[element]['shape'].interiors:
  824. x, y = ints.coords.xy
  825. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
  826. else:
  827. x, y = local_shapes[element]['shape'].coords.xy
  828. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
  829. self.app.plotcanvas.auto_adjust_axes()
  830. def set(self, text, pos, visible=True, font_size=16, color=None):
  831. """
  832. This will set annotations on the canvas.
  833. :param text: a list of text elements to be used as annotations
  834. :param pos: a list of positions for showing the text elements above
  835. :param visible: if True will display annotations, if False will clear them on canvas
  836. :param font_size: the font size or the annotations
  837. :param color: color of the annotations
  838. :return: None
  839. """
  840. if color is None:
  841. color = "#000000FF"
  842. if visible is not True:
  843. self.clear()
  844. return
  845. if len(text) != len(pos):
  846. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not annotate due of a difference between the number "
  847. "of text elements and the number of text positions."))
  848. return
  849. for idx in range(len(text)):
  850. try:
  851. self.axes.annotate(text[idx], xy=pos[idx], xycoords='data', fontsize=font_size, color=color)
  852. except Exception as e:
  853. log.debug("ShapeCollectionLegacy.set() --> %s" % str(e))
  854. self.app.plotcanvas.auto_adjust_axes()
  855. @property
  856. def visible(self):
  857. return self._visible
  858. @visible.setter
  859. def visible(self, value):
  860. if value is False:
  861. self.axes.cla()
  862. self.app.plotcanvas.auto_adjust_axes()
  863. else:
  864. if self._visible is False:
  865. self.redraw()
  866. self._visible = value
  867. @property
  868. def enabled(self):
  869. return self._visible
  870. @enabled.setter
  871. def enabled(self, value):
  872. if value is False:
  873. self.axes.cla()
  874. self.app.plotcanvas.auto_adjust_axes()
  875. else:
  876. if self._visible is False:
  877. self.redraw()
  878. self._visible = value