PlotCanvasLegacy.py 39 KB

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