PlotCanvasLegacy.py 38 KB

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