PlotCanvasLegacy.py 35 KB

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