PlotCanvasLegacy.py 40 KB

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