PlotCanvasLegacy.py 61 KB

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