PlotCanvas.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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. # ########################################################## ##
  8. from PyQt5 import QtCore
  9. import logging
  10. from flatcamGUI.VisPyCanvas import VisPyCanvas, time, Color
  11. from flatcamGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
  12. from vispy.scene.visuals import InfiniteLine, Line
  13. import numpy as np
  14. from vispy.geometry import Rect
  15. log = logging.getLogger('base')
  16. class PlotCanvas(QtCore.QObject, VisPyCanvas):
  17. """
  18. Class handling the plotting area in the application.
  19. """
  20. def __init__(self, container, fcapp):
  21. """
  22. The constructor configures the VisPy figure that
  23. will contain all plots, creates the base axes and connects
  24. events to the plotting area.
  25. :param container: The parent container in which to draw plots.
  26. :rtype: PlotCanvas
  27. """
  28. super(PlotCanvas, self).__init__()
  29. # VisPyCanvas.__init__(self)
  30. # VisPyCanvas does not allow new attributes. Override.
  31. self.unfreeze()
  32. self.fcapp = fcapp
  33. # Parent container
  34. self.container = container
  35. settings = QtCore.QSettings("Open Source", "FlatCAM")
  36. if settings.contains("theme"):
  37. theme = settings.value('theme', type=str)
  38. else:
  39. theme = 'white'
  40. if theme == 'white':
  41. self.line_color = (0.3, 0.0, 0.0, 1.0)
  42. else:
  43. self.line_color = (0.4, 0.4, 0.4, 1.0)
  44. # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
  45. # which might decrease performance
  46. self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
  47. # <VisPyCanvas>
  48. self.create_native()
  49. self.native.setParent(self.fcapp.ui)
  50. # <QtCore.QObject>
  51. self.container.addWidget(self.native)
  52. # ## AXIS # ##
  53. self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
  54. parent=self.view.scene)
  55. self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
  56. parent=self.view.scene)
  57. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  58. # all CNC have a limited workspace
  59. self.draw_workspace()
  60. self.line_parent = None
  61. self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
  62. parent=self.line_parent)
  63. self.cursor_h_line = InfiniteLine(pos=None, color=self.line_color, vertical=False,
  64. parent=self.line_parent)
  65. # if self.app.defaults['global_workspace'] is True:
  66. # if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  67. # self.wkspace_t = Line(pos=)
  68. self.shape_collections = []
  69. self.shape_collection = self.new_shape_collection()
  70. self.fcapp.pool_recreated.connect(self.on_pool_recreated)
  71. self.text_collection = self.new_text_collection()
  72. # TODO: Should be setting to show/hide CNC job annotations (global or per object)
  73. self.text_collection.enabled = True
  74. self.c = None
  75. self.big_cursor = None
  76. # Keep VisPy canvas happy by letting it be "frozen" again.
  77. self.freeze()
  78. self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
  79. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  80. # all CNC have a limited workspace
  81. def draw_workspace(self):
  82. a = np.empty((0, 0))
  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. a4p_mm = np.array([(0, 0), (210, 0), (210, 297), (0, 297)])
  88. a4l_mm = np.array([(0, 0), (297, 0), (297, 210), (0, 210)])
  89. a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
  90. a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
  91. if self.fcapp.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  92. if self.fcapp.defaults['global_workspaceT'] == 'A4P':
  93. a = a4p_mm
  94. elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
  95. a = a4l_mm
  96. elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
  97. a = a3p_mm
  98. elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
  99. a = a3l_mm
  100. else:
  101. if self.fcapp.defaults['global_workspaceT'] == 'A4P':
  102. a = a4p_in
  103. elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
  104. a = a4l_in
  105. elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
  106. a = a3p_in
  107. elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
  108. a = a3l_in
  109. self.delete_workspace()
  110. self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
  111. antialias=True, method='agg', parent=self.view.scene)
  112. self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
  113. antialias=True, method='agg', parent=self.view.scene)
  114. self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
  115. antialias=True, method='agg', parent=self.view.scene)
  116. self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
  117. antialias=True, method='agg', parent=self.view.scene)
  118. if self.fcapp.defaults['global_workspace'] is False:
  119. self.delete_workspace()
  120. # delete the workspace lines from the plot by removing the parent
  121. def delete_workspace(self):
  122. try:
  123. self.b_line.parent = None
  124. self.r_line.parent = None
  125. self.t_line.parent = None
  126. self.l_line.parent = None
  127. except Exception as e:
  128. pass
  129. # redraw the workspace lines on the plot by readding them to the parent view.scene
  130. def restore_workspace(self):
  131. try:
  132. self.b_line.parent = self.view.scene
  133. self.r_line.parent = self.view.scene
  134. self.t_line.parent = self.view.scene
  135. self.l_line.parent = self.view.scene
  136. except Exception as e:
  137. pass
  138. def graph_event_connect(self, event_name, callback):
  139. return getattr(self.events, event_name).connect(callback)
  140. def graph_event_disconnect(self, event_name, callback=None):
  141. if callback is None:
  142. getattr(self.events, event_name).disconnect()
  143. else:
  144. getattr(self.events, event_name).disconnect(callback)
  145. def zoom(self, factor, center=None):
  146. """
  147. Zooms the plot by factor around a given
  148. center point. Takes care of re-drawing.
  149. :param factor: Number by which to scale the plot.
  150. :type factor: float
  151. :param center: Coordinates [x, y] of the point around which to scale the plot.
  152. :type center: list
  153. :return: None
  154. """
  155. self.view.camera.zoom(factor, center)
  156. def new_shape_group(self, shape_collection=None):
  157. if shape_collection:
  158. return ShapeGroup(shape_collection)
  159. return ShapeGroup(self.shape_collection)
  160. def new_shape_collection(self, **kwargs):
  161. # sc = ShapeCollection(parent=self.view.scene, pool=self.app.pool, **kwargs)
  162. # self.shape_collections.append(sc)
  163. # return sc
  164. return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
  165. def new_cursor(self, big=None):
  166. """
  167. Will create a mouse cursor pointer on canvas
  168. :param big: if True will create a mouse cursor made out of infinite lines
  169. :return: the mouse cursor object
  170. """
  171. if big is True:
  172. self.big_cursor = True
  173. self.c = CursorBig()
  174. # in case there are multiple new_cursor calls, best to disconnect first the signals
  175. try:
  176. self.c.mouse_state_updated.disconnect(self.on_mouse_state)
  177. except (TypeError, AttributeError):
  178. pass
  179. try:
  180. self.c.mouse_position_updated.disconnect(self.on_mouse_position)
  181. except (TypeError, AttributeError):
  182. pass
  183. self.c.mouse_state_updated.connect(self.on_mouse_state)
  184. self.c.mouse_position_updated.connect(self.on_mouse_position)
  185. else:
  186. self.big_cursor = False
  187. self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
  188. self.c.antialias = 0
  189. return self.c
  190. def on_mouse_state(self, state):
  191. if state:
  192. self.cursor_h_line.parent = self.view.scene
  193. self.cursor_v_line.parent = self.view.scene
  194. else:
  195. self.cursor_h_line.parent = None
  196. self.cursor_v_line.parent = None
  197. def on_mouse_position(self, pos):
  198. # self.line_color = color
  199. self.cursor_h_line.set_data(pos=pos[1], color=self.line_color)
  200. self.cursor_v_line.set_data(pos=pos[0], color=self.line_color)
  201. self.view.scene.update()
  202. def on_mouse_scroll(self, event):
  203. # key modifiers
  204. modifiers = event.modifiers
  205. pan_delta_x = self.fcapp.defaults["global_gridx"]
  206. pan_delta_y = self.fcapp.defaults["global_gridy"]
  207. curr_pos = event.pos
  208. # Controlled pan by mouse wheel
  209. if 'Shift' in modifiers:
  210. p1 = np.array(curr_pos)[:2]
  211. if event.delta[1] > 0:
  212. curr_pos[0] -= pan_delta_x
  213. else:
  214. curr_pos[0] += pan_delta_x
  215. p2 = np.array(curr_pos)[:2]
  216. self.view.camera.pan(p2 - p1)
  217. elif 'Control' in modifiers:
  218. p1 = np.array(curr_pos)[:2]
  219. if event.delta[1] > 0:
  220. curr_pos[1] += pan_delta_y
  221. else:
  222. curr_pos[1] -= pan_delta_y
  223. p2 = np.array(curr_pos)[:2]
  224. self.view.camera.pan(p2 - p1)
  225. if self.fcapp.grid_status() == True:
  226. pos_canvas = self.translate_coords(curr_pos)
  227. pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  228. # Update cursor
  229. self.fcapp.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
  230. symbol='++', edge_color=self.fcapp.cursor_color_3D,
  231. size=self.fcapp.defaults["global_cursor_size"])
  232. def new_text_group(self, collection=None):
  233. if collection:
  234. return TextGroup(collection)
  235. else:
  236. return TextGroup(self.text_collection)
  237. def new_text_collection(self, **kwargs):
  238. return TextCollection(parent=self.view.scene, **kwargs)
  239. def fit_view(self, rect=None):
  240. # Lock updates in other threads
  241. self.shape_collection.lock_updates()
  242. if not rect:
  243. rect = Rect(-1, -1, 20, 20)
  244. try:
  245. rect.left, rect.right = self.shape_collection.bounds(axis=0)
  246. rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
  247. except TypeError:
  248. pass
  249. # adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly
  250. # otherwise the shape collection boundary will have no border
  251. rect.left *= 0.96
  252. rect.bottom *= 0.96
  253. rect.right *= 1.01
  254. rect.top *= 1.01
  255. self.view.camera.rect = rect
  256. self.shape_collection.unlock_updates()
  257. def fit_center(self, loc, rect=None):
  258. # Lock updates in other threads
  259. self.shape_collection.lock_updates()
  260. if not rect:
  261. try:
  262. rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
  263. except TypeError:
  264. pass
  265. self.view.camera.rect = rect
  266. self.shape_collection.unlock_updates()
  267. def clear(self):
  268. pass
  269. def redraw(self):
  270. self.shape_collection.redraw([])
  271. self.text_collection.redraw()
  272. def on_pool_recreated(self, pool):
  273. self.shape_collection.pool = pool
  274. class CursorBig(QtCore.QObject):
  275. """
  276. This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
  277. This way I don't have to chane (disable) things related to the cursor all over when
  278. using the low performance Matplotlib 2D graphic engine.
  279. """
  280. mouse_state_updated = QtCore.pyqtSignal(bool)
  281. mouse_position_updated = QtCore.pyqtSignal(list)
  282. def __init__(self):
  283. super().__init__()
  284. self._enabled = None
  285. @property
  286. def enabled(self):
  287. return True if self._enabled else False
  288. @enabled.setter
  289. def enabled(self, value):
  290. self._enabled = value
  291. self.mouse_state_updated.emit(value)
  292. def set_data(self, pos, **kwargs):
  293. """Internal event handler to draw the cursor when the mouse moves."""
  294. if 'edge_color' in kwargs:
  295. color = kwargs['edge_color']
  296. else:
  297. if self.app.defaults['global_theme'] == 'white':
  298. color = '#000000FF'
  299. else:
  300. color = '#FFFFFFFF'
  301. position = [pos[0][0], pos[0][1]]
  302. self.mouse_position_updated.emit(position)