PlotCanvasLegacy.py 30 KB

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