PlotCanvas.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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
  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. # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
  36. # which might decrease performance
  37. self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
  38. # <VisPyCanvas>
  39. self.create_native()
  40. self.native.setParent(self.fcapp.ui)
  41. # <QtCore.QObject>
  42. self.container.addWidget(self.native)
  43. # ## AXIS # ##
  44. self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
  45. parent=self.view.scene)
  46. self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
  47. parent=self.view.scene)
  48. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  49. # all CNC have a limited workspace
  50. self.draw_workspace()
  51. self.line_parent = None
  52. self.line_color = (0.3, 0.0, 0.0, 1.0)
  53. self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
  54. parent=self.line_parent)
  55. self.cursor_h_line = InfiniteLine(pos=None, color=self.line_color, vertical=False,
  56. parent=self.line_parent)
  57. # if self.app.defaults['global_workspace'] is True:
  58. # if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  59. # self.wkspace_t = Line(pos=)
  60. self.shape_collections = []
  61. self.shape_collection = self.new_shape_collection()
  62. self.fcapp.pool_recreated.connect(self.on_pool_recreated)
  63. self.text_collection = self.new_text_collection()
  64. # TODO: Should be setting to show/hide CNC job annotations (global or per object)
  65. self.text_collection.enabled = True
  66. self.c = None
  67. # Keep VisPy canvas happy by letting it be "frozen" again.
  68. self.freeze()
  69. # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
  70. # all CNC have a limited workspace
  71. def draw_workspace(self):
  72. a = np.empty((0, 0))
  73. a4p_in = np.array([(0, 0), (8.3, 0), (8.3, 11.7), (0, 11.7)])
  74. a4l_in = np.array([(0, 0), (11.7, 0), (11.7, 8.3), (0, 8.3)])
  75. a3p_in = np.array([(0, 0), (11.7, 0), (11.7, 16.5), (0, 16.5)])
  76. a3l_in = np.array([(0, 0), (16.5, 0), (16.5, 11.7), (0, 11.7)])
  77. a4p_mm = np.array([(0, 0), (210, 0), (210, 297), (0, 297)])
  78. a4l_mm = np.array([(0, 0), (297, 0), (297,210), (0, 210)])
  79. a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
  80. a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
  81. if self.fcapp.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  82. if self.fcapp.defaults['global_workspaceT'] == 'A4P':
  83. a = a4p_mm
  84. elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
  85. a = a4l_mm
  86. elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
  87. a = a3p_mm
  88. elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
  89. a = a3l_mm
  90. else:
  91. if self.fcapp.defaults['global_workspaceT'] == 'A4P':
  92. a = a4p_in
  93. elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
  94. a = a4l_in
  95. elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
  96. a = a3p_in
  97. elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
  98. a = a3l_in
  99. self.delete_workspace()
  100. self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
  101. antialias= True, method='agg', parent=self.view.scene)
  102. self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
  103. antialias= True, method='agg', parent=self.view.scene)
  104. self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
  105. antialias= True, method='agg', parent=self.view.scene)
  106. self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
  107. antialias= True, method='agg', parent=self.view.scene)
  108. if self.fcapp.defaults['global_workspace'] is False:
  109. self.delete_workspace()
  110. # delete the workspace lines from the plot by removing the parent
  111. def delete_workspace(self):
  112. try:
  113. self.b_line.parent = None
  114. self.r_line.parent = None
  115. self.t_line.parent = None
  116. self.l_line.parent = None
  117. except Exception as e:
  118. pass
  119. # redraw the workspace lines on the plot by readding them to the parent view.scene
  120. def restore_workspace(self):
  121. try:
  122. self.b_line.parent = self.view.scene
  123. self.r_line.parent = self.view.scene
  124. self.t_line.parent = self.view.scene
  125. self.l_line.parent = self.view.scene
  126. except Exception as e:
  127. pass
  128. def graph_event_connect(self, event_name, callback):
  129. return getattr(self.events, event_name).connect(callback)
  130. def graph_event_disconnect(self, event_name, callback=None):
  131. if callback is None:
  132. getattr(self.events, event_name).disconnect()
  133. else:
  134. getattr(self.events, event_name).disconnect(callback)
  135. def zoom(self, factor, center=None):
  136. """
  137. Zooms the plot by factor around a given
  138. center point. Takes care of re-drawing.
  139. :param factor: Number by which to scale the plot.
  140. :type factor: float
  141. :param center: Coordinates [x, y] of the point around which to scale the plot.
  142. :type center: list
  143. :return: None
  144. """
  145. self.view.camera.zoom(factor, center)
  146. def new_shape_group(self, shape_collection=None):
  147. if shape_collection:
  148. return ShapeGroup(shape_collection)
  149. return ShapeGroup(self.shape_collection)
  150. def new_shape_collection(self, **kwargs):
  151. # sc = ShapeCollection(parent=self.view.scene, pool=self.app.pool, **kwargs)
  152. # self.shape_collections.append(sc)
  153. # return sc
  154. return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
  155. def new_cursor(self, big=None):
  156. if big is True:
  157. self.c = CursorBig()
  158. self.c.mouse_state_updated.connect(self.on_mouse_state)
  159. self.c.mouse_position_updated.connect(self.on_mouse_position)
  160. else:
  161. self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
  162. self.c.antialias = 0
  163. return self.c
  164. def on_mouse_state(self, state):
  165. if state:
  166. self.cursor_h_line.parent = self.view.scene
  167. self.cursor_v_line.parent = self.view.scene
  168. else:
  169. self.cursor_h_line.parent = None
  170. self.cursor_v_line.parent = None
  171. def on_mouse_position(self, pos):
  172. # self.line_color = color
  173. self.cursor_h_line.set_data(pos=pos[1], color=self.line_color)
  174. self.cursor_v_line.set_data(pos=pos[0], color=self.line_color)
  175. self.view.scene.update()
  176. def new_text_group(self, collection=None):
  177. if collection:
  178. return TextGroup(collection)
  179. else:
  180. return TextGroup(self.text_collection)
  181. def new_text_collection(self, **kwargs):
  182. return TextCollection(parent=self.view.scene, **kwargs)
  183. def fit_view(self, rect=None):
  184. # Lock updates in other threads
  185. self.shape_collection.lock_updates()
  186. if not rect:
  187. rect = Rect(-1, -1, 20, 20)
  188. try:
  189. rect.left, rect.right = self.shape_collection.bounds(axis=0)
  190. rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
  191. except TypeError:
  192. pass
  193. # adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly
  194. # otherwise the shape collection boundary will have no border
  195. rect.left *= 0.96
  196. rect.bottom *= 0.96
  197. rect.right *= 1.01
  198. rect.top *= 1.01
  199. self.view.camera.rect = rect
  200. self.shape_collection.unlock_updates()
  201. def fit_center(self, loc, rect=None):
  202. # Lock updates in other threads
  203. self.shape_collection.lock_updates()
  204. if not rect:
  205. try:
  206. rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
  207. except TypeError:
  208. pass
  209. self.view.camera.rect = rect
  210. self.shape_collection.unlock_updates()
  211. def clear(self):
  212. pass
  213. def redraw(self):
  214. self.shape_collection.redraw([])
  215. self.text_collection.redraw()
  216. def on_pool_recreated(self, pool):
  217. self.shape_collection.pool = pool
  218. class CursorBig(QtCore.QObject):
  219. """
  220. This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
  221. This way I don't have to chane (disable) things related to the cursor all over when
  222. using the low performance Matplotlib 2D graphic engine.
  223. """
  224. mouse_state_updated = QtCore.pyqtSignal(bool)
  225. mouse_position_updated = QtCore.pyqtSignal(list)
  226. def __init__(self):
  227. super().__init__()
  228. self._enabled = None
  229. @property
  230. def enabled(self):
  231. return True if self._enabled else False
  232. @enabled.setter
  233. def enabled(self, value):
  234. self._enabled = value
  235. self.mouse_state_updated.emit(value)
  236. def set_data(self, pos, **kwargs):
  237. """Internal event handler to draw the cursor when the mouse moves."""
  238. if 'edge_color' in kwargs:
  239. color = kwargs['edge_color']
  240. else:
  241. color = (0.0, 0.0, 0.0, 1.0)
  242. position = [pos[0][0], pos[0][1]]
  243. self.mouse_position_updated.emit(position)