PlotCanvas.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # Author: Dennis Hayrullin (c) #
  4. # Date: 2016 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtCore
  8. import logging
  9. from flatcamGUI.VisPyCanvas import VisPyCanvas, time, Color
  10. from flatcamGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
  11. from vispy.scene.visuals import InfiniteLine, Line
  12. import numpy as np
  13. from vispy.geometry import Rect
  14. log = logging.getLogger('base')
  15. class PlotCanvas(QtCore.QObject, VisPyCanvas):
  16. """
  17. Class handling the plotting area in the application.
  18. """
  19. def __init__(self, container, fcapp):
  20. """
  21. The constructor configures the VisPy figure that
  22. will contain all plots, creates the base axes and connects
  23. events to the plotting area.
  24. :param container: The parent container in which to draw plots.
  25. :rtype: PlotCanvas
  26. """
  27. super(PlotCanvas, self).__init__()
  28. # VisPyCanvas.__init__(self)
  29. # VisPyCanvas does not allow new attributes. Override.
  30. self.unfreeze()
  31. self.fcapp = fcapp
  32. # Parent container
  33. self.container = container
  34. settings = QtCore.QSettings("Open Source", "FlatCAM")
  35. if settings.contains("theme"):
  36. theme = settings.value('theme', type=str)
  37. else:
  38. theme = 'white'
  39. if theme == 'white':
  40. self.line_color = (0.3, 0.0, 0.0, 1.0)
  41. else:
  42. self.line_color = (0.4, 0.4, 0.4, 1.0)
  43. # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
  44. # which might decrease performance
  45. self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
  46. # <VisPyCanvas>
  47. self.create_native()
  48. self.native.setParent(self.fcapp.ui)
  49. # <QtCore.QObject>
  50. self.container.addWidget(self.native)
  51. # ## AXIS # ##
  52. self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
  53. parent=self.view.scene)
  54. self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
  55. parent=self.view.scene)
  56. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  57. # all CNC have a limited workspace
  58. self.draw_workspace(pagesize=self.fcapp.defaults["global_workspaceT"])
  59. self.line_parent = None
  60. self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
  61. parent=self.line_parent)
  62. self.cursor_h_line = InfiniteLine(pos=None, color=self.line_color, vertical=False,
  63. parent=self.line_parent)
  64. # if self.app.defaults['global_workspace'] is True:
  65. # if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  66. # self.wkspace_t = Line(pos=)
  67. self.shape_collections = []
  68. self.shape_collection = self.new_shape_collection()
  69. self.fcapp.pool_recreated.connect(self.on_pool_recreated)
  70. self.text_collection = self.new_text_collection()
  71. # TODO: Should be setting to show/hide CNC job annotations (global or per object)
  72. self.text_collection.enabled = True
  73. self.c = None
  74. self.big_cursor = None
  75. # Keep VisPy canvas happy by letting it be "frozen" again.
  76. self.freeze()
  77. self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
  78. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  79. # all CNC have a limited workspace
  80. # def draw_workspace(self):
  81. # a = np.empty((0, 0))
  82. #
  83. # a4p_in = np.array([(0, 0), (8.3, 0), (8.3, 11.7), (0, 11.7)])
  84. # a4l_in = np.array([(0, 0), (11.7, 0), (11.7, 8.3), (0, 8.3)])
  85. # a3p_in = np.array([(0, 0), (11.7, 0), (11.7, 16.5), (0, 16.5)])
  86. # a3l_in = np.array([(0, 0), (16.5, 0), (16.5, 11.7), (0, 11.7)])
  87. #
  88. # a4p_mm = np.array([(0, 0), (210, 0), (210, 297), (0, 297)])
  89. # a4l_mm = np.array([(0, 0), (297, 0), (297, 210), (0, 210)])
  90. # a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
  91. # a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
  92. #
  93. # if self.fcapp.defaults['units'].upper() == 'MM':
  94. # if self.fcapp.defaults['global_workspaceT'] == 'A4P':
  95. # a = a4p_mm
  96. # elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
  97. # a = a4l_mm
  98. # elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
  99. # a = a3p_mm
  100. # elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
  101. # a = a3l_mm
  102. # else:
  103. # if self.fcapp.defaults['global_workspaceT'] == 'A4P':
  104. # a = a4p_in
  105. # elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
  106. # a = a4l_in
  107. # elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
  108. # a = a3p_in
  109. # elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
  110. # a = a3l_in
  111. #
  112. # self.delete_workspace()
  113. #
  114. # self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
  115. # antialias=True, method='agg', parent=self.view.scene)
  116. # self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
  117. # antialias=True, method='agg', parent=self.view.scene)
  118. #
  119. # self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
  120. # antialias=True, method='agg', parent=self.view.scene)
  121. # self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
  122. # antialias=True, method='agg', parent=self.view.scene)
  123. #
  124. # if self.fcapp.defaults['global_workspace'] is False:
  125. # self.delete_workspace()
  126. # delete the workspace lines from the plot by removing the parent
  127. def draw_workspace(self, pagesize):
  128. pagesize_dict = dict()
  129. pagesize_dict.update(
  130. {
  131. 'A0': (841, 1189),
  132. 'A1': (594, 841),
  133. 'A2': (420, 594),
  134. 'A3': (297, 420),
  135. 'A4': (210, 297),
  136. 'A5': (148, 210),
  137. 'A6': (105, 148),
  138. 'A7': (74, 105),
  139. 'A8': (52, 74),
  140. 'A9': (37, 52),
  141. 'A10': (26, 37),
  142. 'B0': (1000, 1414),
  143. 'B1': (707, 1000),
  144. 'B2': (500, 707),
  145. 'B3': (353, 500),
  146. 'B4': (250, 353),
  147. 'B5': (176, 250),
  148. 'B6': (125, 176),
  149. 'B7': (88, 125),
  150. 'B8': (62, 88),
  151. 'B9': (44, 62),
  152. 'B10': (31, 44),
  153. 'C0': (917, 1297),
  154. 'C1': (648, 917),
  155. 'C2': (458, 648),
  156. 'C3': (324, 458),
  157. 'C4': (229, 324),
  158. 'C5': (162, 229),
  159. 'C6': (114, 162),
  160. 'C7': (81, 114),
  161. 'C8': (57, 81),
  162. 'C9': (40, 57),
  163. 'C10': (28, 40),
  164. # American paper sizes
  165. 'LETTER': (8.5*25.4, 11*25.4),
  166. 'LEGAL': (8.5*25.4, 14*25.4),
  167. 'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
  168. # From https://en.wikipedia.org/wiki/Paper_size
  169. 'JUNIOR_LEGAL': (5*25.4, 8*25.4),
  170. 'HALF_LETTER': (5.5*25.4, 8*25.4),
  171. 'GOV_LETTER': (8*25.4, 10.5*25.4),
  172. 'GOV_LEGAL': (8.5*25.4, 13*25.4),
  173. 'LEDGER': (17*25.4, 11*25.4),
  174. }
  175. )
  176. try:
  177. if self.fcapp.defaults['units'].upper() == 'MM':
  178. dims = pagesize_dict[pagesize]
  179. else:
  180. dims = (pagesize_dict[pagesize][0]/25.4, pagesize_dict[pagesize][1]/25.4)
  181. except Exception as e:
  182. log.debug("PlotCanvas.draw_workspace() --> %s" % str(e))
  183. return
  184. if self.fcapp.defaults['global_workspace_orientation'] == 'l':
  185. dims = (dims[1], dims[0])
  186. a = np.array([(0, 0), (dims[0], 0), (dims[0], dims[1]), (0, dims[1])])
  187. self.delete_workspace()
  188. self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
  189. antialias=True, method='agg', parent=self.view.scene)
  190. self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
  191. antialias=True, method='agg', parent=self.view.scene)
  192. self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
  193. antialias=True, method='agg', parent=self.view.scene)
  194. self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
  195. antialias=True, method='agg', parent=self.view.scene)
  196. if self.fcapp.defaults['global_workspace'] is False:
  197. self.delete_workspace()
  198. def delete_workspace(self):
  199. try:
  200. self.b_line.parent = None
  201. self.r_line.parent = None
  202. self.t_line.parent = None
  203. self.l_line.parent = None
  204. except Exception:
  205. pass
  206. # redraw the workspace lines on the plot by readding them to the parent view.scene
  207. def restore_workspace(self):
  208. try:
  209. self.b_line.parent = self.view.scene
  210. self.r_line.parent = self.view.scene
  211. self.t_line.parent = self.view.scene
  212. self.l_line.parent = self.view.scene
  213. except Exception:
  214. pass
  215. def graph_event_connect(self, event_name, callback):
  216. return getattr(self.events, event_name).connect(callback)
  217. def graph_event_disconnect(self, event_name, callback=None):
  218. if callback is None:
  219. getattr(self.events, event_name).disconnect()
  220. else:
  221. getattr(self.events, event_name).disconnect(callback)
  222. def zoom(self, factor, center=None):
  223. """
  224. Zooms the plot by factor around a given
  225. center point. Takes care of re-drawing.
  226. :param factor: Number by which to scale the plot.
  227. :type factor: float
  228. :param center: Coordinates [x, y] of the point around which to scale the plot.
  229. :type center: list
  230. :return: None
  231. """
  232. self.view.camera.zoom(factor, center)
  233. def new_shape_group(self, shape_collection=None):
  234. if shape_collection:
  235. return ShapeGroup(shape_collection)
  236. return ShapeGroup(self.shape_collection)
  237. def new_shape_collection(self, **kwargs):
  238. # sc = ShapeCollection(parent=self.view.scene, pool=self.app.pool, **kwargs)
  239. # self.shape_collections.append(sc)
  240. # return sc
  241. return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
  242. def new_cursor(self, big=None):
  243. """
  244. Will create a mouse cursor pointer on canvas
  245. :param big: if True will create a mouse cursor made out of infinite lines
  246. :return: the mouse cursor object
  247. """
  248. if big is True:
  249. self.big_cursor = True
  250. self.c = CursorBig()
  251. # in case there are multiple new_cursor calls, best to disconnect first the signals
  252. try:
  253. self.c.mouse_state_updated.disconnect(self.on_mouse_state)
  254. except (TypeError, AttributeError):
  255. pass
  256. try:
  257. self.c.mouse_position_updated.disconnect(self.on_mouse_position)
  258. except (TypeError, AttributeError):
  259. pass
  260. self.c.mouse_state_updated.connect(self.on_mouse_state)
  261. self.c.mouse_position_updated.connect(self.on_mouse_position)
  262. else:
  263. self.big_cursor = False
  264. self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
  265. self.c.antialias = 0
  266. return self.c
  267. def on_mouse_state(self, state):
  268. if state:
  269. self.cursor_h_line.parent = self.view.scene
  270. self.cursor_v_line.parent = self.view.scene
  271. else:
  272. self.cursor_h_line.parent = None
  273. self.cursor_v_line.parent = None
  274. def on_mouse_position(self, pos):
  275. # self.line_color = color
  276. self.cursor_h_line.set_data(pos=pos[1], color=self.line_color)
  277. self.cursor_v_line.set_data(pos=pos[0], color=self.line_color)
  278. self.view.scene.update()
  279. def on_mouse_scroll(self, event):
  280. # key modifiers
  281. modifiers = event.modifiers
  282. pan_delta_x = self.fcapp.defaults["global_gridx"]
  283. pan_delta_y = self.fcapp.defaults["global_gridy"]
  284. curr_pos = event.pos
  285. # Controlled pan by mouse wheel
  286. if 'Shift' in modifiers:
  287. p1 = np.array(curr_pos)[:2]
  288. if event.delta[1] > 0:
  289. curr_pos[0] -= pan_delta_x
  290. else:
  291. curr_pos[0] += pan_delta_x
  292. p2 = np.array(curr_pos)[:2]
  293. self.view.camera.pan(p2 - p1)
  294. elif 'Control' in modifiers:
  295. p1 = np.array(curr_pos)[:2]
  296. if event.delta[1] > 0:
  297. curr_pos[1] += pan_delta_y
  298. else:
  299. curr_pos[1] -= pan_delta_y
  300. p2 = np.array(curr_pos)[:2]
  301. self.view.camera.pan(p2 - p1)
  302. if self.fcapp.grid_status() == True:
  303. pos_canvas = self.translate_coords(curr_pos)
  304. pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  305. # Update cursor
  306. self.fcapp.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
  307. symbol='++', edge_color=self.fcapp.cursor_color_3D,
  308. size=self.fcapp.defaults["global_cursor_size"])
  309. def new_text_group(self, collection=None):
  310. if collection:
  311. return TextGroup(collection)
  312. else:
  313. return TextGroup(self.text_collection)
  314. def new_text_collection(self, **kwargs):
  315. return TextCollection(parent=self.view.scene, **kwargs)
  316. def fit_view(self, rect=None):
  317. # Lock updates in other threads
  318. self.shape_collection.lock_updates()
  319. if not rect:
  320. rect = Rect(-1, -1, 20, 20)
  321. try:
  322. rect.left, rect.right = self.shape_collection.bounds(axis=0)
  323. rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
  324. except TypeError:
  325. pass
  326. # adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly
  327. # otherwise the shape collection boundary will have no border
  328. rect.left *= 0.96
  329. rect.bottom *= 0.96
  330. rect.right *= 1.01
  331. rect.top *= 1.01
  332. self.view.camera.rect = rect
  333. self.shape_collection.unlock_updates()
  334. def fit_center(self, loc, rect=None):
  335. # Lock updates in other threads
  336. self.shape_collection.lock_updates()
  337. if not rect:
  338. try:
  339. rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
  340. except TypeError:
  341. pass
  342. self.view.camera.rect = rect
  343. self.shape_collection.unlock_updates()
  344. def clear(self):
  345. pass
  346. def redraw(self):
  347. self.shape_collection.redraw([])
  348. self.text_collection.redraw()
  349. def on_pool_recreated(self, pool):
  350. self.shape_collection.pool = pool
  351. class CursorBig(QtCore.QObject):
  352. """
  353. This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
  354. This way I don't have to chane (disable) things related to the cursor all over when
  355. using the low performance Matplotlib 2D graphic engine.
  356. """
  357. mouse_state_updated = QtCore.pyqtSignal(bool)
  358. mouse_position_updated = QtCore.pyqtSignal(list)
  359. def __init__(self):
  360. super().__init__()
  361. self._enabled = None
  362. @property
  363. def enabled(self):
  364. return True if self._enabled else False
  365. @enabled.setter
  366. def enabled(self, value):
  367. self._enabled = value
  368. self.mouse_state_updated.emit(value)
  369. def set_data(self, pos, **kwargs):
  370. """Internal event handler to draw the cursor when the mouse moves."""
  371. if 'edge_color' in kwargs:
  372. color = kwargs['edge_color']
  373. else:
  374. if self.app.defaults['global_theme'] == 'white':
  375. color = '#000000FF'
  376. else:
  377. color = '#FFFFFFFF'
  378. position = [pos[0][0], pos[0][1]]
  379. self.mouse_position_updated.emit(position)