PlotCanvasLegacy.py 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662
  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. # ##########################################################
  10. # File Modified : Marcos Dumay de Medeiros #
  11. # Modifications under GPLv3 #
  12. # ##########################################################
  13. from PyQt5 import QtCore
  14. from PyQt5.QtCore import pyqtSignal
  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
  19. from copy import deepcopy
  20. import logging
  21. import numpy as np
  22. import gettext
  23. import appTranslation as fcTranslate
  24. import builtins
  25. # Prevent conflict with Qt5 and above.
  26. from matplotlib import use as mpl_use
  27. mpl_use("Qt5Agg")
  28. from matplotlib.figure import Figure
  29. from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
  30. from matplotlib.lines import Line2D
  31. from matplotlib.offsetbox import AnchoredText
  32. # from matplotlib.widgets import Cursor
  33. fcTranslate.apply_language('strings')
  34. if '_' not in builtins.__dict__:
  35. _ = gettext.gettext
  36. log = logging.getLogger('base')
  37. class CanvasCache(QtCore.QObject):
  38. """
  39. Case story #1:
  40. 1) No objects in the project.
  41. 2) Object is created (app_obj.new_object() emits object_created(obj)).
  42. on_object_created() adds (i) object to collection and emits
  43. (ii) app_obj.new_object_available() then calls (iii) object.plot()
  44. 3) object.plot() creates axes if necessary on
  45. app.collection.figure. Then plots on it.
  46. 4) Plots on a cache-size canvas (in background).
  47. 5) Plot completes. Bitmap is generated.
  48. 6) Visible canvas is painted.
  49. """
  50. # Signals:
  51. # A bitmap is ready to be displayed.
  52. new_screen = QtCore.pyqtSignal()
  53. def __init__(self, plotcanvas, app, dpi=50):
  54. super(CanvasCache, self).__init__()
  55. self.app = app
  56. self.plotcanvas = plotcanvas
  57. self.dpi = dpi
  58. self.figure = Figure(dpi=dpi)
  59. self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
  60. self.axes.set_frame_on(False)
  61. self.axes.set_xticks([])
  62. self.axes.set_yticks([])
  63. if self.app.defaults['global_theme'] == 'white':
  64. self.axes.set_facecolor('#FFFFFF')
  65. else:
  66. self.axes.set_facecolor('#000000')
  67. self.canvas = FigureCanvas(self.figure)
  68. self.cache = None
  69. def run(self):
  70. log.debug("CanvasCache Thread Started!")
  71. self.plotcanvas.update_screen_request.connect(self.on_update_req)
  72. def on_update_req(self, extents):
  73. """
  74. Event handler for an updated display request.
  75. :param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
  76. """
  77. # log.debug("Canvas update requested: %s" % str(extents))
  78. # Note: This information below might be out of date. Establish
  79. # a protocol regarding when to change the canvas in the main
  80. # thread and when to check these values here in the background,
  81. # or pass this data in the signal (safer).
  82. # log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
  83. # log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
  84. # Move the requested screen portion to the main thread
  85. # and inform about the update:
  86. self.new_screen.emit()
  87. # Continue to update the cache.
  88. # def on_app_obj.new_object_available(self):
  89. #
  90. # log.debug("A new object is available. Should plot it!")
  91. class PlotCanvasLegacy(QtCore.QObject):
  92. """
  93. Class handling the plotting area in the application.
  94. """
  95. # Signals:
  96. # Request for new bitmap to display. The parameter
  97. # is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
  98. update_screen_request = QtCore.pyqtSignal(list)
  99. double_click = QtCore.pyqtSignal(object)
  100. def __init__(self, container, app):
  101. """
  102. The constructor configures the Matplotlib figure that
  103. will contain all plots, creates the base axes and connects
  104. events to the plotting area.
  105. :param container: The parent container in which to draw plots.
  106. :rtype: PlotCanvas
  107. """
  108. super(PlotCanvasLegacy, self).__init__()
  109. self.app = app
  110. if self.app.defaults['global_theme'] == 'white':
  111. theme_color = '#FFFFFF'
  112. tick_color = '#000000'
  113. self.rect_hud_color = '#0000FF10'
  114. self.text_hud_color = '#000000'
  115. else:
  116. theme_color = '#000000'
  117. tick_color = '#FFFFFF'
  118. self.rect_hud_color = '#80808040'
  119. self.text_hud_color = '#FFFFFF'
  120. # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
  121. # which might decrease performance
  122. # self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
  123. self.workspace_line = None
  124. self.pagesize_dict = {}
  125. self.pagesize_dict.update(
  126. {
  127. 'A0': (841, 1189),
  128. 'A1': (594, 841),
  129. 'A2': (420, 594),
  130. 'A3': (297, 420),
  131. 'A4': (210, 297),
  132. 'A5': (148, 210),
  133. 'A6': (105, 148),
  134. 'A7': (74, 105),
  135. 'A8': (52, 74),
  136. 'A9': (37, 52),
  137. 'A10': (26, 37),
  138. 'B0': (1000, 1414),
  139. 'B1': (707, 1000),
  140. 'B2': (500, 707),
  141. 'B3': (353, 500),
  142. 'B4': (250, 353),
  143. 'B5': (176, 250),
  144. 'B6': (125, 176),
  145. 'B7': (88, 125),
  146. 'B8': (62, 88),
  147. 'B9': (44, 62),
  148. 'B10': (31, 44),
  149. 'C0': (917, 1297),
  150. 'C1': (648, 917),
  151. 'C2': (458, 648),
  152. 'C3': (324, 458),
  153. 'C4': (229, 324),
  154. 'C5': (162, 229),
  155. 'C6': (114, 162),
  156. 'C7': (81, 114),
  157. 'C8': (57, 81),
  158. 'C9': (40, 57),
  159. 'C10': (28, 40),
  160. # American paper sizes
  161. 'LETTER': (8.5*25.4, 11*25.4),
  162. 'LEGAL': (8.5*25.4, 14*25.4),
  163. 'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
  164. # From https://en.wikipedia.org/wiki/Paper_size
  165. 'JUNIOR_LEGAL': (5*25.4, 8*25.4),
  166. 'HALF_LETTER': (5.5*25.4, 8*25.4),
  167. 'GOV_LETTER': (8*25.4, 10.5*25.4),
  168. 'GOV_LEGAL': (8.5*25.4, 13*25.4),
  169. 'LEDGER': (17*25.4, 11*25.4),
  170. }
  171. )
  172. # Options
  173. self.x_margin = 15 # pixels
  174. self.y_margin = 25 # Pixels
  175. # Parent container
  176. self.container = container
  177. # Plots go onto a single matplotlib.figure
  178. self.figure = Figure(dpi=50)
  179. self.figure.patch.set_visible(True)
  180. self.figure.set_facecolor(theme_color)
  181. # These axes show the ticks and grid. No plotting done here.
  182. # New axes must have a label, otherwise mpl returns an existing one.
  183. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  184. self.axes.set_aspect(1)
  185. self.axes.grid(True, color='gray')
  186. self.h_line = self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
  187. self.v_line = self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
  188. self.axes.tick_params(axis='x', color=tick_color, labelcolor=tick_color)
  189. self.axes.tick_params(axis='y', color=tick_color, labelcolor=tick_color)
  190. self.axes.spines['bottom'].set_color(tick_color)
  191. self.axes.spines['top'].set_color(tick_color)
  192. self.axes.spines['right'].set_color(tick_color)
  193. self.axes.spines['left'].set_color(tick_color)
  194. self.axes.set_facecolor(theme_color)
  195. self.ch_line = None
  196. self.cv_line = None
  197. # The canvas is the top level container (FigureCanvasQTAgg)
  198. self.canvas = FigureCanvas(self.figure)
  199. self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
  200. self.canvas.setFocus()
  201. self.native = self.canvas
  202. self.adjust_axes(-10, -10, 100, 100)
  203. # self.canvas.set_can_focus(True) # For key press
  204. # Attach to parent
  205. # self.container.attach(self.canvas, 0, 0, 600, 400)
  206. self.container.addWidget(self.canvas) # Qt
  207. # Copy a bitmap of the canvas for quick animation.
  208. # Update every time the canvas is re-drawn.
  209. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  210. # ################### NOT IMPLEMENTED YET - EXPERIMENTAL #######################
  211. # ## Bitmap Cache
  212. # self.cache = CanvasCache(self, self.app)
  213. # self.cache_thread = QtCore.QThread()
  214. # self.cache.moveToThread(self.cache_thread)
  215. # # super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
  216. # self.cache_thread.started.connect(self.cache.run)
  217. #
  218. # self.cache_thread.start()
  219. # self.cache.new_screen.connect(self.on_new_screen)
  220. # ##############################################################################
  221. # Events
  222. self.mp = self.graph_event_connect('button_press_event', self.on_mouse_press)
  223. self.mr = self.graph_event_connect('button_release_event', self.on_mouse_release)
  224. self.mm = self.graph_event_connect('motion_notify_event', self.on_mouse_move)
  225. # self.canvas.connect('configure-event', self.auto_adjust_axes)
  226. self.aaa = self.graph_event_connect('resize_event', self.auto_adjust_axes)
  227. # self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  228. # self.canvas.connect("scroll-event", self.on_scroll)
  229. self.osc = self.graph_event_connect('scroll_event', self.on_scroll)
  230. # self.graph_event_connect('key_press_event', self.on_key_down)
  231. # self.graph_event_connect('key_release_event', self.on_key_up)
  232. self.odr = self.graph_event_connect('draw_event', self.on_draw)
  233. self.key = None
  234. self.pan_axes = []
  235. self.panning = False
  236. self.mouse = [0, 0]
  237. self.big_cursor = False
  238. self.big_cursor_isdisabled = None
  239. # signal is the mouse is dragging
  240. self.is_dragging = False
  241. self.mouse_press_pos = None
  242. # signal if there is a doubleclick
  243. self.is_dblclk = False
  244. # HUD Display
  245. self.hud_enabled = False
  246. self.text_hud = self.Thud(plotcanvas=self)
  247. if self.app.defaults['global_hud'] is True:
  248. self.on_toggle_hud(state=True, silent=None)
  249. # enable Grid lines
  250. self.grid_lines_enabled = True
  251. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  252. # all CNC have a limited workspace
  253. if self.app.defaults['global_workspace'] is True:
  254. self.draw_workspace(workspace_size=self.app.defaults["global_workspaceT"])
  255. # Axis Display
  256. self.axis_enabled = True
  257. # enable Axis
  258. self.on_toggle_axis(state=True, silent=True)
  259. self.app.ui.axis_status_label.setStyleSheet("""
  260. QLabel
  261. {
  262. color: black;
  263. background-color: orange;
  264. }
  265. """)
  266. def on_toggle_axis(self, signal=None, state=None, silent=None):
  267. if not state:
  268. state = not self.axis_enabled
  269. if state:
  270. self.axis_enabled = True
  271. self.app.defaults['global_axis'] = True
  272. if self.h_line not in self.axes.lines and self.v_line not in self.axes.lines:
  273. self.h_line = self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
  274. self.v_line = self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
  275. self.app.ui.axis_status_label.setStyleSheet("""
  276. QLabel
  277. {
  278. color: black;
  279. background-color: orange;
  280. }
  281. """)
  282. if silent is None:
  283. self.app.inform[str, bool].emit(_("Axis enabled."), False)
  284. else:
  285. self.axis_enabled = False
  286. self.app.defaults['global_axis'] = False
  287. if self.h_line in self.axes.lines and self.v_line in self.axes.lines:
  288. self.axes.lines.remove(self.h_line)
  289. self.axes.lines.remove(self.v_line)
  290. self.app.ui.axis_status_label.setStyleSheet("")
  291. if silent is None:
  292. self.app.inform[str, bool].emit(_("Axis disabled."), False)
  293. self.canvas.draw()
  294. def on_toggle_hud(self, signal=None, state=None, silent=None):
  295. if state is None:
  296. state = not self.hud_enabled
  297. if state:
  298. self.hud_enabled = True
  299. self.text_hud.add_artist()
  300. self.app.defaults['global_hud'] = True
  301. self.app.ui.hud_label.setStyleSheet("""
  302. QLabel
  303. {
  304. color: black;
  305. background-color: mediumpurple;
  306. }
  307. """)
  308. if silent is None:
  309. self.app.inform[str, bool].emit(_("HUD enabled."), False)
  310. else:
  311. self.hud_enabled = False
  312. self.text_hud.remove_artist()
  313. self.app.defaults['global_hud'] = False
  314. self.app.ui.hud_label.setStyleSheet("")
  315. if silent is None:
  316. self.app.inform[str, bool].emit(_("HUD disabled."), False)
  317. self.canvas.draw()
  318. class Thud(QtCore.QObject):
  319. text_changed = QtCore.pyqtSignal(str)
  320. def __init__(self, plotcanvas):
  321. super().__init__()
  322. self.p = plotcanvas
  323. units = self.p.app.defaults['units']
  324. self._text = 'Dx: %s [%s]\nDy: %s [%s]\n\nX: %s [%s]\nY: %s [%s]' % \
  325. ('0.0000', units, '0.0000', units, '0.0000', units, '0.0000', units)
  326. # set font size
  327. qsettings = QtCore.QSettings("Open Source", "FlatCAM")
  328. if qsettings.contains("hud_font_size"):
  329. # I multiply with 2.5 because this seems to be the difference between the value taken by the VisPy (3D)
  330. # and Matplotlib (Legacy2D FlatCAM graphic engine)
  331. fsize = int(qsettings.value('hud_font_size', type=int) * 2.5)
  332. else:
  333. fsize = 20
  334. self.hud_holder = AnchoredText(self._text, prop=dict(size=fsize), frameon=True, loc='upper left')
  335. self.hud_holder.patch.set_boxstyle("round,pad=0.,rounding_size=0.2")
  336. fc_color = self.p.rect_hud_color[:-2]
  337. fc_alpha = int(self.p.rect_hud_color[-2:], 16) / 255
  338. text_color = self.p.text_hud_color
  339. self.hud_holder.patch.set_facecolor(fc_color)
  340. self.hud_holder.patch.set_alpha(fc_alpha)
  341. self.hud_holder.patch.set_edgecolor((0, 0, 0, 0))
  342. self. hud_holder.txt._text.set_color(color=text_color)
  343. self.text_changed.connect(self.on_text_changed)
  344. @property
  345. def text(self):
  346. return self._text
  347. @text.setter
  348. def text(self, val):
  349. self.text_changed.emit(val)
  350. self._text = val
  351. def on_text_changed(self, txt):
  352. try:
  353. txt = txt.replace('\t', ' ')
  354. self.hud_holder.txt.set_text(txt)
  355. self.p.canvas.draw()
  356. except Exception:
  357. pass
  358. def add_artist(self):
  359. if self.hud_holder not in self.p.axes.artists:
  360. self.p.axes.add_artist(self.hud_holder)
  361. def remove_artist(self):
  362. if self.hud_holder in self.p.axes.artists:
  363. self.p.axes.artists.remove(self.hud_holder)
  364. def on_toggle_grid_lines(self, signal=None, silent=None):
  365. state = not self.grid_lines_enabled
  366. if state:
  367. self.app.defaults['global_grid_lines'] = True
  368. self.grid_lines_enabled = True
  369. self.axes.grid(True)
  370. try:
  371. self.canvas.draw()
  372. except IndexError:
  373. pass
  374. if silent is None:
  375. self.app.inform[str, bool].emit(_("Grid enabled."), False)
  376. else:
  377. self.app.defaults['global_grid_lines'] = False
  378. self.grid_lines_enabled = False
  379. self.axes.grid(False)
  380. try:
  381. self.canvas.draw()
  382. except IndexError:
  383. pass
  384. if silent is None:
  385. self.app.inform[str, bool].emit(_("Grid disabled."), False)
  386. def draw_workspace(self, workspace_size):
  387. """
  388. Draw a rectangular shape on canvas to specify our valid workspace.
  389. :param workspace_size: the workspace size; tuple
  390. :return:
  391. """
  392. try:
  393. if self.app.defaults['units'].upper() == 'MM':
  394. dims = self.pagesize_dict[workspace_size]
  395. else:
  396. dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
  397. except Exception as e:
  398. log.debug("PlotCanvasLegacy.draw_workspace() --> %s" % str(e))
  399. return
  400. if self.app.defaults['global_workspace_orientation'] == 'l':
  401. dims = (dims[1], dims[0])
  402. xdata = [0, dims[0], dims[0], 0, 0]
  403. ydata = [0, 0, dims[1], dims[1], 0]
  404. if self.workspace_line not in self.axes.lines:
  405. self.workspace_line = Line2D(xdata=xdata, ydata=ydata, linewidth=2, antialiased=True, color='#b34d4d')
  406. self.axes.add_line(self.workspace_line)
  407. self.canvas.draw()
  408. self.app.ui.wplace_label.set_value(workspace_size[:3])
  409. self.app.ui.wplace_label.setToolTip(workspace_size)
  410. self.fcapp.ui.wplace_label.setStyleSheet("""
  411. QLabel
  412. {
  413. color: black;
  414. background-color: olivedrab;
  415. }
  416. """)
  417. def delete_workspace(self):
  418. try:
  419. self.axes.lines.remove(self.workspace_line)
  420. self.canvas.draw()
  421. except Exception:
  422. pass
  423. self.fcapp.ui.wplace_label.setStyleSheet("")
  424. def graph_event_connect(self, event_name, callback):
  425. """
  426. Attach an event handler to the canvas through the Matplotlib interface.
  427. :param event_name: Name of the event
  428. :type event_name: str
  429. :param callback: Function to call
  430. :type callback: func
  431. :return: Connection id
  432. :rtype: int
  433. """
  434. if event_name == 'mouse_move':
  435. event_name = 'motion_notify_event'
  436. if event_name == 'mouse_press':
  437. event_name = 'button_press_event'
  438. if event_name == 'mouse_release':
  439. event_name = 'button_release_event'
  440. if event_name == 'mouse_double_click':
  441. return self.double_click.connect(callback)
  442. if event_name == 'key_press':
  443. event_name = 'key_press_event'
  444. return self.canvas.mpl_connect(event_name, callback)
  445. def graph_event_disconnect(self, cid):
  446. """
  447. Disconnect callback with the give id.
  448. :param cid: Callback id.
  449. :return: None
  450. """
  451. self.canvas.mpl_disconnect(cid)
  452. def on_new_screen(self):
  453. pass
  454. # log.debug("Cache updated the screen!")
  455. def new_cursor(self, axes=None, big=None):
  456. # if axes is None:
  457. # c = MplCursor(axes=self.axes, color='black', linewidth=1)
  458. # else:
  459. # c = MplCursor(axes=axes, color='black', linewidth=1)
  460. if self.app.defaults["global_cursor_color_enabled"]:
  461. color = self.app.defaults["global_cursor_color"]
  462. else:
  463. if self.app.defaults['global_theme'] == 'white':
  464. color = '#000000'
  465. else:
  466. color = '#FFFFFF'
  467. if big is True:
  468. self.big_cursor = True
  469. self.ch_line = self.axes.axhline(color=color, linewidth=self.app.defaults["global_cursor_width"])
  470. self.cv_line = self.axes.axvline(color=color, linewidth=self.app.defaults["global_cursor_width"])
  471. self.big_cursor_isdisabled = False
  472. else:
  473. self.big_cursor = False
  474. c = FakeCursor()
  475. c.mouse_state_updated.connect(self.clear_cursor)
  476. return c
  477. def draw_cursor(self, x_pos, y_pos, color=None):
  478. """
  479. Draw a cursor at the mouse grid snapped position
  480. :param x_pos: mouse x position
  481. :param y_pos: mouse y position
  482. :param color: custom color of the mouse
  483. :return:
  484. """
  485. # there is no point in drawing mouse cursor when panning as it jumps in a confusing way
  486. if self.app.app_cursor.enabled is True and self.panning is False:
  487. if color:
  488. color = color
  489. else:
  490. if self.app.defaults['global_theme'] == 'white':
  491. color = '#000000'
  492. else:
  493. color = '#FFFFFF'
  494. if self.big_cursor is False:
  495. try:
  496. x, y = self.snap(x_pos, y_pos)
  497. # Pointer (snapped)
  498. # The size of the cursor is multiplied by 1.65 because that value made the cursor similar with the
  499. # one in the OpenGL(3D) graphic engine
  500. pointer_size = int(float(self.app.defaults["global_cursor_size"]) * 1.65)
  501. elements = self.axes.plot(x, y, '+', color=color, ms=pointer_size,
  502. mew=self.app.defaults["global_cursor_width"], animated=True)
  503. for el in elements:
  504. self.axes.draw_artist(el)
  505. except Exception as e:
  506. # this happen at app initialization since self.app.geo_editor does not exist yet
  507. # I could reshuffle the object instantiating order but what's the point?
  508. # I could crash something else and that's pythonic, too
  509. log.debug("PlotCanvasLegacy.draw_cursor() big_cursor is False --> %s" % str(e))
  510. else:
  511. try:
  512. self.ch_line.set_markeredgewidth(self.app.defaults["global_cursor_width"])
  513. self.cv_line.set_markeredgewidth(self.app.defaults["global_cursor_width"])
  514. except Exception:
  515. pass
  516. try:
  517. x, y = self.app.geo_editor.snap(x_pos, y_pos)
  518. self.ch_line.set_ydata(y)
  519. self.cv_line.set_xdata(x)
  520. except Exception:
  521. # this happen at app initialization since self.app.geo_editor does not exist yet
  522. # I could reshuffle the object instantiating order but what's the point?
  523. # I could crash something else and that's pythonic, too
  524. pass
  525. self.canvas.draw_idle()
  526. self.canvas.blit(self.axes.bbox)
  527. def clear_cursor(self, state):
  528. if state is True:
  529. if self.big_cursor is True and self.big_cursor_isdisabled is True:
  530. if self.app.defaults["global_cursor_color_enabled"]:
  531. color = self.app.defaults["global_cursor_color"]
  532. else:
  533. if self.app.defaults['global_theme'] == 'white':
  534. color = '#000000'
  535. else:
  536. color = '#FFFFFF'
  537. self.ch_line = self.axes.axhline(color=color, linewidth=self.app.defaults["global_cursor_width"])
  538. self.cv_line = self.axes.axvline(color=color, linewidth=self.app.defaults["global_cursor_width"])
  539. self.big_cursor_isdisabled = False
  540. if self.app.defaults["global_cursor_color_enabled"] is True:
  541. self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1], color=self.app.cursor_color_3D)
  542. else:
  543. self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
  544. else:
  545. if self.big_cursor is True:
  546. self.big_cursor_isdisabled = True
  547. try:
  548. self.ch_line.remove()
  549. self.cv_line.remove()
  550. self.canvas.draw_idle()
  551. except Exception as e:
  552. log.debug("PlotCanvasLegacy.clear_cursor() big_cursor is True --> %s" % str(e))
  553. self.canvas.restore_region(self.background)
  554. self.canvas.blit(self.axes.bbox)
  555. def on_key_down(self, event):
  556. """
  557. :param event:
  558. :return:
  559. """
  560. log.debug('on_key_down(): ' + str(event.key))
  561. self.key = event.key
  562. def on_key_up(self, event):
  563. """
  564. :param event:
  565. :return:
  566. """
  567. self.key = None
  568. def connect(self, event_name, callback):
  569. """
  570. Attach an event handler to the canvas through the native Qt interface.
  571. :param event_name: Name of the event
  572. :type event_name: str
  573. :param callback: Function to call
  574. :type callback: function
  575. :return: Nothing
  576. """
  577. self.canvas.connect(event_name, callback)
  578. def clear(self):
  579. """
  580. Clears axes and figure.
  581. :return: None
  582. """
  583. # Clear
  584. self.axes.cla()
  585. try:
  586. self.figure.clf()
  587. except KeyError:
  588. log.warning("KeyError in MPL figure.clf()")
  589. # Re-build
  590. self.figure.add_axes(self.axes)
  591. self.axes.set_aspect(1)
  592. self.axes.grid(True)
  593. self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
  594. self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
  595. self.adjust_axes(-10, -10, 100, 100)
  596. # Re-draw
  597. self.canvas.draw_idle()
  598. def redraw(self):
  599. """
  600. Created only to serve for compatibility with the VisPy plotcanvas (the other graphic engine, 3D)
  601. :return:
  602. """
  603. self.clear()
  604. def adjust_axes(self, xmin, ymin, xmax, ymax):
  605. """
  606. Adjusts all axes while maintaining the use of the whole canvas
  607. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  608. request that will be modified to fit these restrictions.
  609. :param xmin: Requested minimum value for the X axis.
  610. :type xmin: float
  611. :param ymin: Requested minimum value for the Y axis.
  612. :type ymin: float
  613. :param xmax: Requested maximum value for the X axis.
  614. :type xmax: float
  615. :param ymax: Requested maximum value for the Y axis.
  616. :type ymax: float
  617. :return: None
  618. """
  619. # FlatCAMApp.App.log.debug("PC.adjust_axes()")
  620. if not self.app.collection.get_list():
  621. xmin = -10
  622. ymin = -10
  623. xmax = 100
  624. ymax = 100
  625. width = xmax - xmin
  626. height = ymax - ymin
  627. try:
  628. r = width / height
  629. except ZeroDivisionError:
  630. log.error("Height is %f" % height)
  631. return
  632. canvas_w, canvas_h = self.canvas.get_width_height()
  633. canvas_r = float(canvas_w) / canvas_h
  634. x_ratio = float(self.x_margin) / canvas_w
  635. y_ratio = float(self.y_margin) / canvas_h
  636. if r > canvas_r:
  637. ycenter = (ymin + ymax) / 2.0
  638. newheight = height * r / canvas_r
  639. ymin = ycenter - newheight / 2.0
  640. ymax = ycenter + newheight / 2.0
  641. else:
  642. xcenter = (xmax + xmin) / 2.0
  643. newwidth = width * canvas_r / r
  644. xmin = xcenter - newwidth / 2.0
  645. xmax = xcenter + newwidth / 2.0
  646. # Adjust axes
  647. for ax in self.figure.get_axes():
  648. if ax._label != 'base':
  649. ax.set_frame_on(False) # No frame
  650. ax.set_xticks([]) # No tick
  651. ax.set_yticks([]) # No ticks
  652. ax.patch.set_visible(False) # No background
  653. ax.set_aspect(1)
  654. ax.set_xlim((xmin, xmax))
  655. ax.set_ylim((ymin, ymax))
  656. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  657. # Sync re-draw to proper paint on form resize
  658. self.canvas.draw()
  659. # #### Temporary place-holder for cached update #####
  660. self.update_screen_request.emit([0, 0, 0, 0, 0])
  661. def auto_adjust_axes(self, *args):
  662. """
  663. Calls ``adjust_axes()`` using the extents of the base axes.
  664. :rtype : None
  665. :return: None
  666. """
  667. xmin, xmax = self.axes.get_xlim()
  668. ymin, ymax = self.axes.get_ylim()
  669. self.adjust_axes(xmin, ymin, xmax, ymax)
  670. def fit_view(self):
  671. self.auto_adjust_axes()
  672. def fit_center(self, loc, rect=None):
  673. x = loc[0]
  674. y = loc[1]
  675. xmin, xmax = self.axes.get_xlim()
  676. ymin, ymax = self.axes.get_ylim()
  677. half_width = (xmax - xmin) / 2
  678. half_height = (ymax - ymin) / 2
  679. # Adjust axes
  680. for ax in self.figure.get_axes():
  681. ax.set_xlim((x - half_width, x + half_width))
  682. ax.set_ylim((y - half_height, y + half_height))
  683. # Re-draw
  684. self.canvas.draw()
  685. # #### Temporary place-holder for cached update #####
  686. self.update_screen_request.emit([0, 0, 0, 0, 0])
  687. def zoom(self, factor, center=None):
  688. """
  689. Zooms the plot by factor around a given
  690. center point. Takes care of re-drawing.
  691. :param factor: Number by which to scale the plot.
  692. :type factor: float
  693. :param center: Coordinates [x, y] of the point around which to scale the plot.
  694. :type center: list
  695. :return: None
  696. """
  697. factor = 1 / factor
  698. xmin, xmax = self.axes.get_xlim()
  699. ymin, ymax = self.axes.get_ylim()
  700. width = xmax - xmin
  701. height = ymax - ymin
  702. if center is None or center == [None, None]:
  703. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  704. # For keeping the point at the pointer location
  705. relx = (xmax - center[0]) / width
  706. rely = (ymax - center[1]) / height
  707. new_width = width / factor
  708. new_height = height / factor
  709. xmin = center[0] - new_width * (1 - relx)
  710. xmax = center[0] + new_width * relx
  711. ymin = center[1] - new_height * (1 - rely)
  712. ymax = center[1] + new_height * rely
  713. # Adjust axes
  714. for ax in self.figure.get_axes():
  715. ax.set_xlim((xmin, xmax))
  716. ax.set_ylim((ymin, ymax))
  717. # Async re-draw
  718. self.canvas.draw_idle()
  719. # #### Temporary place-holder for cached update #####
  720. self.update_screen_request.emit([0, 0, 0, 0, 0])
  721. def pan(self, x, y, idle=True):
  722. xmin, xmax = self.axes.get_xlim()
  723. ymin, ymax = self.axes.get_ylim()
  724. width = xmax - xmin
  725. height = ymax - ymin
  726. # Adjust axes
  727. for ax in self.figure.get_axes():
  728. ax.set_xlim((xmin + x * width, xmax + x * width))
  729. ax.set_ylim((ymin + y * height, ymax + y * height))
  730. # Re-draw
  731. if idle:
  732. self.canvas.draw_idle()
  733. else:
  734. self.canvas.draw()
  735. # #### Temporary place-holder for cached update #####
  736. self.update_screen_request.emit([0, 0, 0, 0, 0])
  737. def new_axes(self, name):
  738. """
  739. Creates and returns an Axes object attached to this object's Figure.
  740. :param name: Unique label for the axes.
  741. :return: Axes attached to the figure.
  742. :rtype: Axes
  743. """
  744. new_ax = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  745. return new_ax
  746. def remove_current_axes(self):
  747. """
  748. :return: The name of the deleted axes
  749. """
  750. axes_to_remove = self.figure.axes.gca()
  751. current_axes_name = deepcopy(axes_to_remove._label)
  752. self.figure.axes.remove(axes_to_remove)
  753. return current_axes_name
  754. def on_scroll(self, event):
  755. """
  756. Scroll event handler.
  757. :param event: Event object containing the event information.
  758. :return: None
  759. """
  760. # So it can receive key presses
  761. # self.canvas.grab_focus()
  762. self.canvas.setFocus()
  763. # Event info
  764. # z, direction = event.get_scroll_direction()
  765. if self.key is None:
  766. if event.button == 'up':
  767. self.zoom(1 / 1.5, self.mouse)
  768. else:
  769. self.zoom(1.5, self.mouse)
  770. return
  771. if self.key == 'shift':
  772. if event.button == 'up':
  773. self.pan(0.3, 0)
  774. else:
  775. self.pan(-0.3, 0)
  776. return
  777. if self.key == 'control':
  778. if event.button == 'up':
  779. self.pan(0, 0.3)
  780. else:
  781. self.pan(0, -0.3)
  782. return
  783. def on_mouse_press(self, event):
  784. self.is_dragging = True
  785. self.mouse_press_pos = (event.x, event.y)
  786. # Check for middle mouse button press
  787. if self.app.defaults["global_pan_button"] == '2':
  788. pan_button = 3 # right button for Matplotlib
  789. else:
  790. pan_button = 2 # middle button for Matplotlib
  791. if event.button == pan_button:
  792. # Prepare axes for pan (using 'matplotlib' pan function)
  793. self.pan_axes = []
  794. for a in self.figure.get_axes():
  795. if (event.x is not None and event.y is not None and a.in_axes(event) and
  796. a.get_navigate() and a.can_pan()):
  797. a.start_pan(event.x, event.y, 1)
  798. self.pan_axes.append(a)
  799. # Set pan view flag
  800. if len(self.pan_axes) > 0:
  801. self.panning = True
  802. if event.dblclick:
  803. self.double_click.emit(event)
  804. def on_mouse_release(self, event):
  805. mouse_release_pos = (event.x, event.y)
  806. delta = 0.05
  807. if abs(self.distance(self.mouse_press_pos, mouse_release_pos)) < delta:
  808. self.is_dragging = False
  809. # Check for middle mouse button release to complete pan procedure
  810. # Check for middle mouse button press
  811. if self.app.defaults["global_pan_button"] == '2':
  812. pan_button = 3 # right button for Matplotlib
  813. else:
  814. pan_button = 2 # middle button for Matplotlib
  815. if event.button == pan_button:
  816. for a in self.pan_axes:
  817. a.end_pan()
  818. # Clear pan flag
  819. self.panning = False
  820. # And update the cursor
  821. if self.app.defaults["global_cursor_color_enabled"] is True:
  822. self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1], color=self.app.cursor_color_3D)
  823. else:
  824. self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
  825. def on_mouse_move(self, event):
  826. """
  827. Mouse movement event handler. Stores the coordinates. Updates view on pan.
  828. :param event: Contains information about the event.
  829. :return: None
  830. """
  831. try:
  832. x = float(event.xdata)
  833. y = float(event.ydata)
  834. except TypeError:
  835. return
  836. self.mouse = [event.xdata, event.ydata]
  837. self.canvas.restore_region(self.background)
  838. # Update pan view on mouse move
  839. if self.panning is True:
  840. for a in self.pan_axes:
  841. a.drag_pan(1, event.key, event.x, event.y)
  842. # x_pan, y_pan = self.app.geo_editor.snap(event.xdata, event.ydata)
  843. # self.draw_cursor(x_pos=x_pan, y_pos=y_pan)
  844. # Async re-draw (redraws only on thread idle state, uses timer on backend)
  845. self.canvas.draw_idle()
  846. # #### Temporary place-holder for cached update #####
  847. # self.update_screen_request.emit([0, 0, 0, 0, 0])
  848. if self.app.defaults["global_cursor_color_enabled"] is True:
  849. self.draw_cursor(x_pos=x, y_pos=y, color=self.app.cursor_color_3D)
  850. else:
  851. self.draw_cursor(x_pos=x, y_pos=y)
  852. # self.canvas.blit(self.axes.bbox)
  853. def translate_coords(self, position):
  854. """
  855. This does not do much. It's just for code compatibility
  856. :param position: Mouse event position
  857. :return: Tuple with mouse position
  858. """
  859. return position[0], position[1]
  860. def on_draw(self, renderer):
  861. # Store background on canvas redraw
  862. self.background = self.canvas.copy_from_bbox(self.axes.bbox)
  863. def get_axes_pixelsize(self):
  864. """
  865. Axes size in pixels.
  866. :return: Pixel width and height
  867. :rtype: tuple
  868. """
  869. bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
  870. width, height = bbox.width, bbox.height
  871. width *= self.figure.dpi
  872. height *= self.figure.dpi
  873. return width, height
  874. def get_density(self):
  875. """
  876. Returns unit length per pixel on horizontal
  877. and vertical axes.
  878. :return: X and Y density
  879. :rtype: tuple
  880. """
  881. xpx, ypx = self.get_axes_pixelsize()
  882. xmin, xmax = self.axes.get_xlim()
  883. ymin, ymax = self.axes.get_ylim()
  884. width = xmax - xmin
  885. height = ymax - ymin
  886. return width / xpx, height / ypx
  887. def snap(self, x, y):
  888. """
  889. Adjusts coordinates to snap settings.
  890. :param x: Input coordinate X
  891. :param y: Input coordinate Y
  892. :return: Snapped (x, y)
  893. """
  894. snap_x, snap_y = (x, y)
  895. snap_distance = np.inf
  896. # ### Grid snap
  897. if self.app.grid_status():
  898. if self.app.defaults["global_gridx"] != 0:
  899. try:
  900. snap_x_ = round(x / float(self.app.defaults["global_gridx"])) * \
  901. float(self.app.defaults["global_gridx"])
  902. except TypeError:
  903. snap_x_ = x
  904. else:
  905. snap_x_ = x
  906. # If the Grid_gap_linked on Grid Toolbar is checked then the snap distance on GridY entry will be ignored
  907. # and it will use the snap distance from GridX entry
  908. if self.app.ui.grid_gap_link_cb.isChecked():
  909. if self.app.defaults["global_gridx"] != 0:
  910. try:
  911. snap_y_ = round(y / float(self.app.defaults["global_gridx"])) * \
  912. float(self.app.defaults["global_gridx"])
  913. except TypeError:
  914. snap_y_ = y
  915. else:
  916. snap_y_ = y
  917. else:
  918. if self.app.defaults["global_gridy"] != 0:
  919. try:
  920. snap_y_ = round(y / float(self.app.defaults["global_gridy"])) * \
  921. float(self.app.defaults["global_gridy"])
  922. except TypeError:
  923. snap_y_ = y
  924. else:
  925. snap_y_ = y
  926. nearest_grid_distance = self.distance((x, y), (snap_x_, snap_y_))
  927. if nearest_grid_distance < snap_distance:
  928. snap_x, snap_y = (snap_x_, snap_y_)
  929. return snap_x, snap_y
  930. @staticmethod
  931. def distance(pt1, pt2):
  932. return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
  933. class FakeCursor(QtCore.QObject):
  934. """
  935. This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
  936. This way I don't have to chane (disable) things related to the cursor all over when
  937. using the low performance Matplotlib 2D graphic engine.
  938. """
  939. mouse_state_updated = pyqtSignal(bool)
  940. def __init__(self):
  941. super().__init__()
  942. self._enabled = True
  943. @property
  944. def enabled(self):
  945. return True if self._enabled else False
  946. @enabled.setter
  947. def enabled(self, value):
  948. self._enabled = value
  949. self.mouse_state_updated.emit(value)
  950. def set_data(self, pos, **kwargs):
  951. """Internal event handler to draw the cursor when the mouse moves."""
  952. return
  953. class ShapeCollectionLegacy:
  954. """
  955. This will create the axes for each collection of shapes and will also
  956. hold the collection of shapes into a dict self._shapes.
  957. This handles the shapes redraw on canvas.
  958. """
  959. def __init__(self, obj, app, name=None, annotation_job=None, linewidth=1):
  960. """
  961. :param obj: This is the object to which the shapes collection is attached and for
  962. which it will have to draw shapes
  963. :param app: This is the FLatCAM.App usually, needed because we have to access attributes there
  964. :param name: This is the name given to the Matplotlib axes; it needs to be unique due of
  965. Matplotlib requurements
  966. :param annotation_job: Make this True if the job needed is just for annotation
  967. :param linewidth: THe width of the line (outline where is the case)
  968. """
  969. self.obj = obj
  970. self.app = app
  971. self.annotation_job = annotation_job
  972. self._shapes = {}
  973. self.shape_dict = {}
  974. self.shape_id = 0
  975. self._color = None
  976. self._face_color = None
  977. self._visible = True
  978. self._update = False
  979. self._alpha = None
  980. self._tool_tolerance = None
  981. self._tooldia = None
  982. self._obj = None
  983. self._gcode_parsed = None
  984. self._linewidth = linewidth
  985. if name is None:
  986. axes_name = self.obj.options['name']
  987. else:
  988. axes_name = name
  989. # Axes must exist and be attached to canvas.
  990. if axes_name not in self.app.plotcanvas.figure.axes:
  991. self.axes = self.app.plotcanvas.new_axes(axes_name)
  992. def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
  993. update=False, layer=1, tolerance=0.01, obj=None, gcode_parsed=None, tool_tolerance=None, tooldia=None,
  994. linewidth=None):
  995. """
  996. This function will add shapes to the shape collection
  997. :param shape: the Shapely shape to be added to the shape collection
  998. :param color: edge color of the shape, hex value
  999. :param face_color: the body color of the shape, hex value
  1000. :param alpha: level of transparency of the shape [0.0 ... 1.0]; Float
  1001. :param visible: if True will allow the shapes to be added
  1002. :param update: not used; just for compatibility with VIsPy canvas
  1003. :param layer: just for compatibility with VIsPy canvas
  1004. :param tolerance: just for compatibility with VIsPy canvas
  1005. :param obj: not used
  1006. :param gcode_parsed: not used; just for compatibility with VIsPy canvas
  1007. :param tool_tolerance: just for compatibility with VIsPy canvas
  1008. :param tooldia:
  1009. :param linewidth: the width of the line
  1010. :return:
  1011. """
  1012. self._color = color if color is not None else "#006E20"
  1013. # self._face_color = face_color if face_color is not None else "#BBF268"
  1014. self._face_color = face_color
  1015. if linewidth is None:
  1016. line_width = self._linewidth
  1017. else:
  1018. line_width = linewidth
  1019. if len(self._color) > 7:
  1020. self._color = self._color[:7]
  1021. if self._face_color is not None:
  1022. if len(self._face_color) > 7:
  1023. self._face_color = self._face_color[:7]
  1024. # self._alpha = int(self._face_color[-2:], 16) / 255
  1025. self._alpha = 0.75
  1026. if alpha is not None:
  1027. self._alpha = alpha
  1028. self._visible = visible
  1029. self._update = update
  1030. # CNCJob object related arguments
  1031. self._obj = obj
  1032. self._gcode_parsed = gcode_parsed
  1033. self._tool_tolerance = tool_tolerance
  1034. self._tooldia = tooldia
  1035. # if self._update:
  1036. # self.clear()
  1037. try:
  1038. for sh in shape:
  1039. self.shape_id += 1
  1040. self.shape_dict.update({
  1041. 'color': self._color,
  1042. 'face_color': self._face_color,
  1043. 'linewidth': line_width,
  1044. 'alpha': self._alpha,
  1045. 'visible': self._visible,
  1046. 'shape': sh
  1047. })
  1048. self._shapes.update({
  1049. self.shape_id: deepcopy(self.shape_dict)
  1050. })
  1051. except TypeError:
  1052. self.shape_id += 1
  1053. self.shape_dict.update({
  1054. 'color': self._color,
  1055. 'face_color': self._face_color,
  1056. 'linewidth': line_width,
  1057. 'alpha': self._alpha,
  1058. 'visible': self._visible,
  1059. 'shape': shape
  1060. })
  1061. self._shapes.update({
  1062. self.shape_id: deepcopy(self.shape_dict)
  1063. })
  1064. return self.shape_id
  1065. def remove(self, shape_id, update=None):
  1066. for k in list(self._shapes.keys()):
  1067. if shape_id == k:
  1068. self._shapes.pop(k, None)
  1069. if update is True:
  1070. self.redraw()
  1071. def clear(self, update=None):
  1072. """
  1073. Clear the canvas of the shapes.
  1074. :param update:
  1075. :return: None
  1076. """
  1077. self._shapes.clear()
  1078. self.shape_id = 0
  1079. self.axes.cla()
  1080. try:
  1081. self.app.plotcanvas.auto_adjust_axes()
  1082. except Exception as e:
  1083. log.debug("ShapeCollectionLegacy.clear() --> %s" % str(e))
  1084. if update is True:
  1085. self.redraw()
  1086. def redraw(self, update_colors=None):
  1087. """
  1088. This draw the shapes in the shapes collection, on canvas
  1089. :return: None
  1090. """
  1091. path_num = 0
  1092. local_shapes = deepcopy(self._shapes)
  1093. try:
  1094. obj_type = self.obj.kind
  1095. except AttributeError:
  1096. obj_type = 'utility'
  1097. # if we don't use this then when adding each new shape, the old ones will be added again, too
  1098. # if obj_type == 'utility':
  1099. # self.axes.patches.clear()
  1100. self.axes.patches.clear()
  1101. for element in local_shapes:
  1102. if local_shapes[element]['visible'] is True:
  1103. if obj_type == 'excellon':
  1104. # Plot excellon (All polygons?)
  1105. if self.obj.options["solid"] and isinstance(local_shapes[element]['shape'], Polygon):
  1106. try:
  1107. patch = PolygonPatch(local_shapes[element]['shape'],
  1108. facecolor=local_shapes[element]['face_color'],
  1109. edgecolor=local_shapes[element]['color'],
  1110. alpha=local_shapes[element]['alpha'],
  1111. zorder=3,
  1112. linewidth=local_shapes[element]['linewidth']
  1113. )
  1114. self.axes.add_patch(patch)
  1115. except Exception as e:
  1116. log.debug("ShapeCollectionLegacy.redraw() excellon poly --> %s" % str(e))
  1117. else:
  1118. try:
  1119. if isinstance(local_shapes[element]['shape'], Polygon):
  1120. x, y = local_shapes[element]['shape'].exterior.coords.xy
  1121. self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
  1122. for ints in local_shapes[element]['shape'].interiors:
  1123. x, y = ints.coords.xy
  1124. self.axes.plot(x, y, 'o-', linewidth=local_shapes[element]['linewidth'])
  1125. elif isinstance(local_shapes[element]['shape'], LinearRing):
  1126. x, y = local_shapes[element]['shape'].coords.xy
  1127. self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
  1128. except Exception as e:
  1129. log.debug("ShapeCollectionLegacy.redraw() excellon no poly --> %s" % str(e))
  1130. elif obj_type == 'geometry':
  1131. if type(local_shapes[element]['shape']) == Polygon:
  1132. try:
  1133. x, y = local_shapes[element]['shape'].exterior.coords.xy
  1134. self.axes.plot(x, y, local_shapes[element]['color'],
  1135. linestyle='-',
  1136. linewidth=local_shapes[element]['linewidth'])
  1137. for ints in local_shapes[element]['shape'].interiors:
  1138. x, y = ints.coords.xy
  1139. self.axes.plot(x, y, local_shapes[element]['color'],
  1140. linestyle='-',
  1141. linewidth=local_shapes[element]['linewidth'])
  1142. except Exception as e:
  1143. log.debug("ShapeCollectionLegacy.redraw() geometry poly --> %s" % str(e))
  1144. elif type(local_shapes[element]['shape']) == LineString or \
  1145. type(local_shapes[element]['shape']) == LinearRing:
  1146. try:
  1147. x, y = local_shapes[element]['shape'].coords.xy
  1148. self.axes.plot(x, y, local_shapes[element]['color'],
  1149. linestyle='-',
  1150. linewidth=local_shapes[element]['linewidth'])
  1151. except Exception as e:
  1152. log.debug("ShapeCollectionLegacy.redraw() geometry no poly --> %s" % str(e))
  1153. elif obj_type == 'gerber':
  1154. if self.obj.options["multicolored"]:
  1155. linespec = '-'
  1156. else:
  1157. linespec = 'k-'
  1158. if self.obj.options["solid"]:
  1159. if update_colors:
  1160. gerber_fill_color = update_colors[0]
  1161. gerber_outline_color = update_colors[1]
  1162. else:
  1163. gerber_fill_color = local_shapes[element]['face_color']
  1164. gerber_outline_color = local_shapes[element]['color']
  1165. try:
  1166. patch = PolygonPatch(local_shapes[element]['shape'],
  1167. facecolor=gerber_fill_color,
  1168. edgecolor=gerber_outline_color,
  1169. alpha=local_shapes[element]['alpha'],
  1170. zorder=2,
  1171. linewidth=local_shapes[element]['linewidth'])
  1172. self.axes.add_patch(patch)
  1173. except AssertionError:
  1174. log.warning("A geometry component was not a polygon:")
  1175. log.warning(str(element))
  1176. except Exception as e:
  1177. log.debug(
  1178. "PlotCanvasLegacy.ShepeCollectionLegacy.redraw() gerber 'solid' --> %s" % str(e))
  1179. else:
  1180. try:
  1181. x, y = local_shapes[element]['shape'].exterior.xy
  1182. self.axes.plot(x, y, linespec, linewidth=local_shapes[element]['linewidth'])
  1183. for ints in local_shapes[element]['shape'].interiors:
  1184. x, y = ints.coords.xy
  1185. self.axes.plot(x, y, linespec, linewidth=local_shapes[element]['linewidth'])
  1186. except Exception as e:
  1187. log.debug("ShapeCollectionLegacy.redraw() gerber no 'solid' --> %s" % str(e))
  1188. elif obj_type == 'cncjob':
  1189. if local_shapes[element]['face_color'] is None:
  1190. try:
  1191. linespec = '--'
  1192. linecolor = local_shapes[element]['color']
  1193. # if geo['kind'][0] == 'C':
  1194. # linespec = 'k-'
  1195. x, y = local_shapes[element]['shape'].coords.xy
  1196. self.axes.plot(x, y, linespec, color=linecolor,
  1197. linewidth=local_shapes[element]['linewidth'])
  1198. except Exception as e:
  1199. log.debug("ShapeCollectionLegacy.redraw() cncjob with face_color --> %s" % str(e))
  1200. else:
  1201. try:
  1202. path_num += 1
  1203. if self.obj.ui.annotation_cb.get_value():
  1204. if isinstance(local_shapes[element]['shape'], Polygon):
  1205. self.axes.annotate(
  1206. str(path_num),
  1207. xy=local_shapes[element]['shape'].exterior.coords[0],
  1208. xycoords='data', fontsize=20)
  1209. else:
  1210. self.axes.annotate(
  1211. str(path_num),
  1212. xy=local_shapes[element]['shape'].coords[0],
  1213. xycoords='data', fontsize=20)
  1214. patch = PolygonPatch(local_shapes[element]['shape'],
  1215. facecolor=local_shapes[element]['face_color'],
  1216. edgecolor=local_shapes[element]['color'],
  1217. alpha=local_shapes[element]['alpha'], zorder=2,
  1218. linewidth=local_shapes[element]['linewidth'])
  1219. self.axes.add_patch(patch)
  1220. except Exception as e:
  1221. log.debug("ShapeCollectionLegacy.redraw() cncjob no face_color --> %s" % str(e))
  1222. elif obj_type == 'utility':
  1223. # not a FlatCAM object, must be utility
  1224. if local_shapes[element]['face_color']:
  1225. try:
  1226. patch = PolygonPatch(local_shapes[element]['shape'],
  1227. facecolor=local_shapes[element]['face_color'],
  1228. edgecolor=local_shapes[element]['color'],
  1229. alpha=local_shapes[element]['alpha'],
  1230. zorder=2,
  1231. linewidth=local_shapes[element]['linewidth'])
  1232. self.axes.add_patch(patch)
  1233. except Exception as e:
  1234. log.debug("ShapeCollectionLegacy.redraw() utility poly with face_color --> %s" % str(e))
  1235. else:
  1236. if isinstance(local_shapes[element]['shape'], Polygon):
  1237. try:
  1238. ext_shape = local_shapes[element]['shape'].exterior
  1239. if ext_shape is not None:
  1240. x, y = ext_shape.xy
  1241. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
  1242. linewidth=local_shapes[element]['linewidth'])
  1243. for ints in local_shapes[element]['shape'].interiors:
  1244. if ints is not None:
  1245. x, y = ints.coords.xy
  1246. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
  1247. linewidth=local_shapes[element]['linewidth'])
  1248. except Exception as e:
  1249. log.debug("ShapeCollectionLegacy.redraw() utility poly no face_color --> %s" % str(e))
  1250. else:
  1251. try:
  1252. if local_shapes[element]['shape'] is not None:
  1253. x, y = local_shapes[element]['shape'].coords.xy
  1254. self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
  1255. linewidth=local_shapes[element]['linewidth'])
  1256. except Exception as e:
  1257. log.debug("ShapeCollectionLegacy.redraw() utility lines no face_color --> %s" % str(e))
  1258. self.app.plotcanvas.auto_adjust_axes()
  1259. def set(self, text, pos, visible=True, font_size=16, color=None):
  1260. """
  1261. This will set annotations on the canvas.
  1262. :param text: a list of text elements to be used as annotations
  1263. :param pos: a list of positions for showing the text elements above
  1264. :param visible: if True will display annotations, if False will clear them on canvas
  1265. :param font_size: the font size or the annotations
  1266. :param color: color of the annotations
  1267. :return: None
  1268. """
  1269. if color is None:
  1270. color = "#000000FF"
  1271. if visible is not True:
  1272. self.clear()
  1273. return
  1274. if len(text) != len(pos):
  1275. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not annotate due of a difference between the number "
  1276. "of text elements and the number of text positions."))
  1277. return
  1278. for idx in range(len(text)):
  1279. try:
  1280. self.axes.annotate(text[idx], xy=pos[idx], xycoords='data', fontsize=font_size, color=color)
  1281. except Exception as e:
  1282. log.debug("ShapeCollectionLegacy.set() --> %s" % str(e))
  1283. self.app.plotcanvas.auto_adjust_axes()
  1284. @property
  1285. def visible(self):
  1286. return self._visible
  1287. @visible.setter
  1288. def visible(self, value):
  1289. if value is False:
  1290. self.axes.cla()
  1291. self.app.plotcanvas.auto_adjust_axes()
  1292. else:
  1293. if self._visible is False:
  1294. self.redraw()
  1295. self._visible = value
  1296. def update_visibility(self, state, indexes=None):
  1297. if indexes:
  1298. for i in indexes:
  1299. if i in self._shapes:
  1300. self._shapes[i]['visible'] = state
  1301. else:
  1302. for i in self._shapes:
  1303. self._shapes[i]['visible'] = state
  1304. self.redraw()
  1305. @property
  1306. def enabled(self):
  1307. return self._visible
  1308. @enabled.setter
  1309. def enabled(self, value):
  1310. if value is False:
  1311. self.axes.cla()
  1312. self.app.plotcanvas.auto_adjust_axes()
  1313. else:
  1314. if self._visible is False:
  1315. self.redraw()
  1316. self._visible = value
  1317. # class MplCursor(Cursor):
  1318. # """
  1319. # Unfortunately this gets attached to the current axes and if a new axes is added
  1320. # it will not be showed until that axes is deleted.
  1321. # Not the kind of behavior needed here so I don't use it anymore.
  1322. # """
  1323. # def __init__(self, axes, color='red', linewidth=1):
  1324. #
  1325. # super().__init__(ax=axes, useblit=True, color=color, linewidth=linewidth)
  1326. # self._enabled = True
  1327. #
  1328. # self.axes = axes
  1329. # self.color = color
  1330. # self.linewidth = linewidth
  1331. #
  1332. # self.x = None
  1333. # self.y = None
  1334. #
  1335. # @property
  1336. # def enabled(self):
  1337. # return True if self._enabled else False
  1338. #
  1339. # @enabled.setter
  1340. # def enabled(self, value):
  1341. # self._enabled = value
  1342. # self.visible = self._enabled
  1343. # self.canvas.draw()
  1344. #
  1345. # def onmove(self, event):
  1346. # pass
  1347. #
  1348. # def set_data(self, event, pos):
  1349. # """Internal event handler to draw the cursor when the mouse moves."""
  1350. # self.x = pos[0]
  1351. # self.y = pos[1]
  1352. #
  1353. # if self.ignore(event):
  1354. # return
  1355. # if not self.canvas.widgetlock.available(self):
  1356. # return
  1357. # if event.inaxes != self.ax:
  1358. # self.linev.set_visible(False)
  1359. # self.lineh.set_visible(False)
  1360. #
  1361. # if self.needclear:
  1362. # self.canvas.draw()
  1363. # self.needclear = False
  1364. # return
  1365. # self.needclear = True
  1366. # if not self.visible:
  1367. # return
  1368. # self.linev.set_xdata((self.x, self.x))
  1369. #
  1370. # self.lineh.set_ydata((self.y, self.y))
  1371. # self.linev.set_visible(self.visible and self.vertOn)
  1372. # self.lineh.set_visible(self.visible and self.horizOn)
  1373. #
  1374. # self._update()