PlotCanvas.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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. self.workspace_line = None
  47. self.pagesize_dict = dict()
  48. self.pagesize_dict.update(
  49. {
  50. 'A0': (841, 1189),
  51. 'A1': (594, 841),
  52. 'A2': (420, 594),
  53. 'A3': (297, 420),
  54. 'A4': (210, 297),
  55. 'A5': (148, 210),
  56. 'A6': (105, 148),
  57. 'A7': (74, 105),
  58. 'A8': (52, 74),
  59. 'A9': (37, 52),
  60. 'A10': (26, 37),
  61. 'B0': (1000, 1414),
  62. 'B1': (707, 1000),
  63. 'B2': (500, 707),
  64. 'B3': (353, 500),
  65. 'B4': (250, 353),
  66. 'B5': (176, 250),
  67. 'B6': (125, 176),
  68. 'B7': (88, 125),
  69. 'B8': (62, 88),
  70. 'B9': (44, 62),
  71. 'B10': (31, 44),
  72. 'C0': (917, 1297),
  73. 'C1': (648, 917),
  74. 'C2': (458, 648),
  75. 'C3': (324, 458),
  76. 'C4': (229, 324),
  77. 'C5': (162, 229),
  78. 'C6': (114, 162),
  79. 'C7': (81, 114),
  80. 'C8': (57, 81),
  81. 'C9': (40, 57),
  82. 'C10': (28, 40),
  83. # American paper sizes
  84. 'LETTER': (8.5*25.4, 11*25.4),
  85. 'LEGAL': (8.5*25.4, 14*25.4),
  86. 'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
  87. # From https://en.wikipedia.org/wiki/Paper_size
  88. 'JUNIOR_LEGAL': (5*25.4, 8*25.4),
  89. 'HALF_LETTER': (5.5*25.4, 8*25.4),
  90. 'GOV_LETTER': (8*25.4, 10.5*25.4),
  91. 'GOV_LEGAL': (8.5*25.4, 13*25.4),
  92. 'LEDGER': (17*25.4, 11*25.4),
  93. }
  94. )
  95. # <VisPyCanvas>
  96. self.create_native()
  97. self.native.setParent(self.fcapp.ui)
  98. # <QtCore.QObject>
  99. self.container.addWidget(self.native)
  100. # ## AXIS # ##
  101. self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
  102. parent=self.view.scene)
  103. self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
  104. parent=self.view.scene)
  105. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  106. # all CNC have a limited workspace
  107. if self.fcapp.defaults['global_workspace'] is True:
  108. self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
  109. self.line_parent = None
  110. self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
  111. parent=self.line_parent)
  112. self.cursor_h_line = InfiniteLine(pos=None, color=self.line_color, vertical=False,
  113. parent=self.line_parent)
  114. # if self.app.defaults['global_workspace'] is True:
  115. # if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  116. # self.wkspace_t = Line(pos=)
  117. self.shape_collections = []
  118. self.shape_collection = self.new_shape_collection()
  119. self.fcapp.pool_recreated.connect(self.on_pool_recreated)
  120. self.text_collection = self.new_text_collection()
  121. # TODO: Should be setting to show/hide CNC job annotations (global or per object)
  122. self.text_collection.enabled = True
  123. self.c = None
  124. self.big_cursor = None
  125. # Keep VisPy canvas happy by letting it be "frozen" again.
  126. self.freeze()
  127. self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
  128. def draw_workspace(self, workspace_size):
  129. """
  130. Draw a rectangular shape on canvas to specify our valid workspace.
  131. :param workspace_size: the workspace size; tuple
  132. :return:
  133. """
  134. try:
  135. if self.fcapp.defaults['units'].upper() == 'MM':
  136. dims = self.pagesize_dict[workspace_size]
  137. else:
  138. dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
  139. except Exception as e:
  140. log.debug("PlotCanvas.draw_workspace() --> %s" % str(e))
  141. return
  142. if self.fcapp.defaults['global_workspace_orientation'] == 'l':
  143. dims = (dims[1], dims[0])
  144. a = np.array([(0, 0), (dims[0], 0), (dims[0], dims[1]), (0, dims[1])])
  145. if not self.workspace_line:
  146. self.workspace_line = Line(pos=np.array((a[0], a[1], a[2], a[3], a[0])), color=(0.70, 0.3, 0.3, 0.7),
  147. antialias=True, method='agg', parent=self.view.scene)
  148. else:
  149. self.workspace_line.parent = self.view.scene
  150. def delete_workspace(self):
  151. try:
  152. self.workspace_line.parent = None
  153. except Exception:
  154. pass
  155. # redraw the workspace lines on the plot by re adding them to the parent view.scene
  156. def restore_workspace(self):
  157. try:
  158. self.workspace_line.parent = self.view.scene
  159. except Exception:
  160. pass
  161. def graph_event_connect(self, event_name, callback):
  162. return getattr(self.events, event_name).connect(callback)
  163. def graph_event_disconnect(self, event_name, callback=None):
  164. if callback is None:
  165. getattr(self.events, event_name).disconnect()
  166. else:
  167. getattr(self.events, event_name).disconnect(callback)
  168. def zoom(self, factor, center=None):
  169. """
  170. Zooms the plot by factor around a given
  171. center point. Takes care of re-drawing.
  172. :param factor: Number by which to scale the plot.
  173. :type factor: float
  174. :param center: Coordinates [x, y] of the point around which to scale the plot.
  175. :type center: list
  176. :return: None
  177. """
  178. self.view.camera.zoom(factor, center)
  179. def new_shape_group(self, shape_collection=None):
  180. if shape_collection:
  181. return ShapeGroup(shape_collection)
  182. return ShapeGroup(self.shape_collection)
  183. def new_shape_collection(self, **kwargs):
  184. # sc = ShapeCollection(parent=self.view.scene, pool=self.app.pool, **kwargs)
  185. # self.shape_collections.append(sc)
  186. # return sc
  187. return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
  188. def new_cursor(self, big=None):
  189. """
  190. Will create a mouse cursor pointer on canvas
  191. :param big: if True will create a mouse cursor made out of infinite lines
  192. :return: the mouse cursor object
  193. """
  194. if big is True:
  195. self.big_cursor = True
  196. self.c = CursorBig()
  197. # in case there are multiple new_cursor calls, best to disconnect first the signals
  198. try:
  199. self.c.mouse_state_updated.disconnect(self.on_mouse_state)
  200. except (TypeError, AttributeError):
  201. pass
  202. try:
  203. self.c.mouse_position_updated.disconnect(self.on_mouse_position)
  204. except (TypeError, AttributeError):
  205. pass
  206. self.c.mouse_state_updated.connect(self.on_mouse_state)
  207. self.c.mouse_position_updated.connect(self.on_mouse_position)
  208. else:
  209. self.big_cursor = False
  210. self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
  211. self.c.antialias = 0
  212. return self.c
  213. def on_mouse_state(self, state):
  214. if state:
  215. self.cursor_h_line.parent = self.view.scene
  216. self.cursor_v_line.parent = self.view.scene
  217. else:
  218. self.cursor_h_line.parent = None
  219. self.cursor_v_line.parent = None
  220. def on_mouse_position(self, pos):
  221. # self.line_color = color
  222. self.cursor_h_line.set_data(pos=pos[1], color=self.line_color)
  223. self.cursor_v_line.set_data(pos=pos[0], color=self.line_color)
  224. self.view.scene.update()
  225. def on_mouse_scroll(self, event):
  226. # key modifiers
  227. modifiers = event.modifiers
  228. pan_delta_x = self.fcapp.defaults["global_gridx"]
  229. pan_delta_y = self.fcapp.defaults["global_gridy"]
  230. curr_pos = event.pos
  231. # Controlled pan by mouse wheel
  232. if 'Shift' in modifiers:
  233. p1 = np.array(curr_pos)[:2]
  234. if event.delta[1] > 0:
  235. curr_pos[0] -= pan_delta_x
  236. else:
  237. curr_pos[0] += pan_delta_x
  238. p2 = np.array(curr_pos)[:2]
  239. self.view.camera.pan(p2 - p1)
  240. elif 'Control' in modifiers:
  241. p1 = np.array(curr_pos)[:2]
  242. if event.delta[1] > 0:
  243. curr_pos[1] += pan_delta_y
  244. else:
  245. curr_pos[1] -= pan_delta_y
  246. p2 = np.array(curr_pos)[:2]
  247. self.view.camera.pan(p2 - p1)
  248. if self.fcapp.grid_status():
  249. pos_canvas = self.translate_coords(curr_pos)
  250. pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  251. # Update cursor
  252. self.fcapp.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
  253. symbol='++', edge_color=self.fcapp.cursor_color_3D,
  254. size=self.fcapp.defaults["global_cursor_size"])
  255. def new_text_group(self, collection=None):
  256. if collection:
  257. return TextGroup(collection)
  258. else:
  259. return TextGroup(self.text_collection)
  260. def new_text_collection(self, **kwargs):
  261. return TextCollection(parent=self.view.scene, **kwargs)
  262. def fit_view(self, rect=None):
  263. # Lock updates in other threads
  264. self.shape_collection.lock_updates()
  265. if not rect:
  266. rect = Rect(-1, -1, 20, 20)
  267. try:
  268. rect.left, rect.right = self.shape_collection.bounds(axis=0)
  269. rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
  270. except TypeError:
  271. pass
  272. # adjust the view camera to be slightly bigger than the bounds so the shape collection can be seen clearly
  273. # otherwise the shape collection boundary will have no border
  274. dx = rect.right - rect.left
  275. dy = rect.top - rect.bottom
  276. x_factor = dx * 0.02
  277. y_factor = dy * 0.02
  278. rect.left -= x_factor
  279. rect.bottom -= y_factor
  280. rect.right += x_factor
  281. rect.top += y_factor
  282. # rect.left *= 0.96
  283. # rect.bottom *= 0.96
  284. # rect.right *= 1.04
  285. # rect.top *= 1.04
  286. # units = self.fcapp.defaults['units'].upper()
  287. # if units == 'MM':
  288. # compensation = 0.5
  289. # else:
  290. # compensation = 0.5 / 25.4
  291. # rect.left -= compensation
  292. # rect.bottom -= compensation
  293. # rect.right += compensation
  294. # rect.top += compensation
  295. self.view.camera.rect = rect
  296. self.shape_collection.unlock_updates()
  297. def fit_center(self, loc, rect=None):
  298. # Lock updates in other threads
  299. self.shape_collection.lock_updates()
  300. if not rect:
  301. try:
  302. rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
  303. except TypeError:
  304. pass
  305. self.view.camera.rect = rect
  306. self.shape_collection.unlock_updates()
  307. def clear(self):
  308. pass
  309. def redraw(self):
  310. self.shape_collection.redraw([])
  311. self.text_collection.redraw()
  312. def on_pool_recreated(self, pool):
  313. self.shape_collection.pool = pool
  314. class CursorBig(QtCore.QObject):
  315. """
  316. This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
  317. This way I don't have to chane (disable) things related to the cursor all over when
  318. using the low performance Matplotlib 2D graphic engine.
  319. """
  320. mouse_state_updated = QtCore.pyqtSignal(bool)
  321. mouse_position_updated = QtCore.pyqtSignal(list)
  322. def __init__(self):
  323. super().__init__()
  324. self._enabled = None
  325. @property
  326. def enabled(self):
  327. return True if self._enabled else False
  328. @enabled.setter
  329. def enabled(self, value):
  330. self._enabled = value
  331. self.mouse_state_updated.emit(value)
  332. def set_data(self, pos, **kwargs):
  333. """Internal event handler to draw the cursor when the mouse moves."""
  334. if 'edge_color' in kwargs:
  335. color = kwargs['edge_color']
  336. else:
  337. if self.app.defaults['global_theme'] == 'white':
  338. color = '#000000FF'
  339. else:
  340. color = '#FFFFFFFF'
  341. position = [pos[0][0], pos[0][1]]
  342. self.mouse_position_updated.emit(position)