PlotCanvas.py 15 KB

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