PlotCanvasLegacy.py 30 KB

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