| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- # ##########################################################
- # FlatCAM: 2D Post-processing for Manufacturing #
- # Author: Dennis Hayrullin (c) #
- # Date: 2016 #
- # MIT Licence #
- # ##########################################################
- from PyQt5 import QtCore
- import logging
- from AppGUI.VisPyCanvas import VisPyCanvas, Color
- from AppGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
- from vispy.scene.visuals import InfiniteLine, Line, Rectangle, Text
- import gettext
- import AppTranslation as fcTranslate
- import builtins
- import numpy as np
- from vispy.geometry import Rect
- fcTranslate.apply_language('strings')
- if '_' not in builtins.__dict__:
- _ = gettext.gettext
- log = logging.getLogger('base')
- class PlotCanvas(QtCore.QObject, VisPyCanvas):
- """
- Class handling the plotting area in the application.
- """
- def __init__(self, container, fcapp):
- """
- The constructor configures the VisPy figure that
- will contain all plots, creates the base axes and connects
- events to the plotting area.
- :param container: The parent container in which to draw plots.
- :rtype: PlotCanvas
- """
- # super(PlotCanvas, self).__init__()
- # QtCore.QObject.__init__(self)
- # VisPyCanvas.__init__(self)
- super().__init__()
- # VisPyCanvas does not allow new attributes. Override.
- self.unfreeze()
- self.fcapp = fcapp
- # Parent container
- self.container = container
- settings = QtCore.QSettings("Open Source", "FlatCAM")
- if settings.contains("theme"):
- theme = settings.value('theme', type=str)
- else:
- theme = 'white'
- if theme == 'white':
- self.line_color = (0.3, 0.0, 0.0, 1.0)
- self.rect_hud_color = Color('#0000FF10')
- self.text_hud_color = 'black'
- else:
- self.line_color = (0.4, 0.4, 0.4, 1.0)
- self.rect_hud_color = Color('#80808040')
- self.text_hud_color = 'white'
- # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
- # which might decrease performance
- # self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
- self.workspace_line = None
- self.pagesize_dict = {}
- self.pagesize_dict.update(
- {
- 'A0': (841, 1189),
- 'A1': (594, 841),
- 'A2': (420, 594),
- 'A3': (297, 420),
- 'A4': (210, 297),
- 'A5': (148, 210),
- 'A6': (105, 148),
- 'A7': (74, 105),
- 'A8': (52, 74),
- 'A9': (37, 52),
- 'A10': (26, 37),
- 'B0': (1000, 1414),
- 'B1': (707, 1000),
- 'B2': (500, 707),
- 'B3': (353, 500),
- 'B4': (250, 353),
- 'B5': (176, 250),
- 'B6': (125, 176),
- 'B7': (88, 125),
- 'B8': (62, 88),
- 'B9': (44, 62),
- 'B10': (31, 44),
- 'C0': (917, 1297),
- 'C1': (648, 917),
- 'C2': (458, 648),
- 'C3': (324, 458),
- 'C4': (229, 324),
- 'C5': (162, 229),
- 'C6': (114, 162),
- 'C7': (81, 114),
- 'C8': (57, 81),
- 'C9': (40, 57),
- 'C10': (28, 40),
- # American paper sizes
- 'LETTER': (8.5*25.4, 11*25.4),
- 'LEGAL': (8.5*25.4, 14*25.4),
- 'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
- # From https://en.wikipedia.org/wiki/Paper_size
- 'JUNIOR_LEGAL': (5*25.4, 8*25.4),
- 'HALF_LETTER': (5.5*25.4, 8*25.4),
- 'GOV_LETTER': (8*25.4, 10.5*25.4),
- 'GOV_LEGAL': (8.5*25.4, 13*25.4),
- 'LEDGER': (17*25.4, 11*25.4),
- }
- )
- # <VisPyCanvas>
- self.create_native()
- self.native.setParent(self.fcapp.ui)
- # <QtCore.QObject>
- self.container.addWidget(self.native)
- # ## AXIS # ##
- self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
- parent=self.view.scene)
- self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
- parent=self.view.scene)
- self.line_parent = None
- if self.fcapp.defaults["global_cursor_color_enabled"]:
- c_color = Color(self.fcapp.defaults["global_cursor_color"]).rgba
- else:
- c_color = self.line_color
- self.cursor_v_line = InfiniteLine(pos=None, color=c_color, vertical=True,
- parent=self.line_parent)
- self.cursor_h_line = InfiniteLine(pos=None, color=c_color, vertical=False,
- parent=self.line_parent)
- # font size
- qsettings = QtCore.QSettings("Open Source", "FlatCAM")
- if qsettings.contains("hud_font_size"):
- fsize = qsettings.value('hud_font_size', type=int)
- else:
- fsize = 8
- # units
- units = self.fcapp.defaults["units"].lower()
- # coordinates and anchors
- height = fsize * 11 # 90. Constant 11 is something that works
- width = height * 2 # width is double the height = it is something that works
- center_x = (width / 2) + 5
- center_y = (height / 2) + 5
- # text
- self.text_hud = Text('', color=self.text_hud_color, pos=(10, center_y), method='gpu', anchor_x='left',
- parent=None)
- self.text_hud.font_size = fsize
- self.text_hud.text = 'Dx:\t%s [%s]\nDy:\t%s [%s]\n\nX: \t%s [%s]\nY: \t%s [%s]' % \
- ('0.0000', units, '0.0000', units, '0.0000', units, '0.0000', units)
- # rectangle
- self.rect_hud = Rectangle(center=(center_x, center_y), width=width, height=height, radius=[5, 5, 5, 5],
- border_color=self.rect_hud_color, color=self.rect_hud_color, parent=None)
- self.rect_hud.set_gl_state(depth_test=False)
- # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
- # all CNC have a limited workspace
- if self.fcapp.defaults['global_workspace'] is True:
- self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
- # HUD Display
- self.hud_enabled = False
- # enable the HUD if it is activated in FlatCAM Preferences
- if self.fcapp.defaults['global_hud'] is True:
- self.on_toggle_hud(state=True)
- # Axis Display
- self.axis_enabled = True
- # enable Axis
- self.on_toggle_axis(state=True)
- # enable Grid lines
- self.grid_lines_enabled = True
- self.shape_collections = []
- self.shape_collection = self.new_shape_collection()
- self.fcapp.pool_recreated.connect(self.on_pool_recreated)
- self.text_collection = self.new_text_collection()
- self.text_collection.enabled = True
- self.c = None
- self.big_cursor = None
- # Keep VisPy canvas happy by letting it be "frozen" again.
- self.freeze()
- self.fit_view()
- self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
- def on_toggle_axis(self, signal=None, state=None):
- if state is None:
- state = not self.axis_enabled
- if state:
- self.axis_enabled = True
- self.v_line.parent = self.view.scene
- self.h_line.parent = self.view.scene
- self.fcapp.ui.axis_status_label.setStyleSheet("""
- QLabel
- {
- color: black;
- background-color: orange;
- }
- """)
- self.fcapp.inform[str, bool].emit(_("Axis enabled."), False)
- else:
- self.axis_enabled = False
- self.v_line.parent = None
- self.h_line.parent = None
- self.fcapp.ui.axis_status_label.setStyleSheet("")
- self.fcapp.inform[str, bool].emit(_("Axis disabled."), False)
- def on_toggle_hud(self, signal=None, state=None):
- if state is None:
- state = not self.hud_enabled
- if state:
- self.hud_enabled = True
- self.rect_hud.parent = self.view
- self.text_hud.parent = self.view
- self.fcapp.defaults['global_hud'] = True
- self.fcapp.ui.hud_label.setStyleSheet("""
- QLabel
- {
- color: black;
- background-color: mediumpurple;
- }
- """)
- self.fcapp.inform[str, bool].emit(_("HUD enabled."), False)
- else:
- self.hud_enabled = False
- self.rect_hud.parent = None
- self.text_hud.parent = None
- self.fcapp.defaults['global_hud'] = False
- self.fcapp.ui.hud_label.setStyleSheet("")
- self.fcapp.inform[str, bool].emit(_("HUD disabled."), False)
- def on_toggle_grid_lines(self):
- state = not self.grid_lines_enabled
- if state:
- self.grid_lines_enabled = True
- self.grid.parent = self.view.scene
- self.fcapp.inform[str, bool].emit(_("Grid enabled."), False)
- else:
- self.grid_lines_enabled = False
- self.grid.parent = None
- self.fcapp.inform[str, bool].emit(_("Grid disabled."), False)
- # HACK: enabling/disabling the cursor seams to somehow update the shapes on screen
- # - perhaps is a bug in VisPy implementation
- if self.fcapp.grid_status():
- self.fcapp.app_cursor.enabled = False
- self.fcapp.app_cursor.enabled = True
- else:
- self.fcapp.app_cursor.enabled = True
- self.fcapp.app_cursor.enabled = False
- def draw_workspace(self, workspace_size):
- """
- Draw a rectangular shape on canvas to specify our valid workspace.
- :param workspace_size: the workspace size; tuple
- :return:
- """
- try:
- if self.fcapp.defaults['units'].upper() == 'MM':
- dims = self.pagesize_dict[workspace_size]
- else:
- dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
- except Exception as e:
- log.debug("PlotCanvas.draw_workspace() --> %s" % str(e))
- return
- if self.fcapp.defaults['global_workspace_orientation'] == 'l':
- dims = (dims[1], dims[0])
- a = np.array([(0, 0), (dims[0], 0), (dims[0], dims[1]), (0, dims[1])])
- # if not self.workspace_line:
- # 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),
- # antialias=True, method='agg', parent=self.view.scene)
- # else:
- # self.workspace_line.parent = self.view.scene
- 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),
- antialias=True, method='agg', parent=self.view.scene)
- self.fcapp.ui.wplace_label.set_value(workspace_size[:3])
- self.fcapp.ui.wplace_label.setToolTip(workspace_size)
- self.fcapp.ui.wplace_label.setStyleSheet("""
- QLabel
- {
- color: black;
- background-color: olivedrab;
- }
- """)
- def delete_workspace(self):
- try:
- self.workspace_line.parent = None
- except Exception:
- pass
- self.fcapp.ui.wplace_label.setStyleSheet("")
- # redraw the workspace lines on the plot by re adding them to the parent view.scene
- def restore_workspace(self):
- try:
- self.workspace_line.parent = self.view.scene
- except Exception:
- pass
- def graph_event_connect(self, event_name, callback):
- return getattr(self.events, event_name).connect(callback)
- def graph_event_disconnect(self, event_name, callback=None):
- if callback is None:
- getattr(self.events, event_name).disconnect()
- else:
- getattr(self.events, event_name).disconnect(callback)
- def zoom(self, factor, center=None):
- """
- Zooms the plot by factor around a given
- center point. Takes care of re-drawing.
- :param factor: Number by which to scale the plot.
- :type factor: float
- :param center: Coordinates [x, y] of the point around which to scale the plot.
- :type center: list
- :return: None
- """
- self.view.camera.zoom(factor, center)
- def new_shape_group(self, shape_collection=None):
- if shape_collection:
- return ShapeGroup(shape_collection)
- return ShapeGroup(self.shape_collection)
- def new_shape_collection(self, **kwargs):
- # sc = ShapeCollection(parent=self.view.scene, pool=self.app.pool, **kwargs)
- # self.shape_collections.append(sc)
- # return sc
- return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
- def new_cursor(self, big=None):
- """
- Will create a mouse cursor pointer on canvas
- :param big: if True will create a mouse cursor made out of infinite lines
- :return: the mouse cursor object
- """
- if big is True:
- self.big_cursor = True
- self.c = CursorBig(app=self.fcapp)
- # in case there are multiple new_cursor calls, best to disconnect first the signals
- try:
- self.c.mouse_state_updated.disconnect(self.on_mouse_state)
- except (TypeError, AttributeError):
- pass
- try:
- self.c.mouse_position_updated.disconnect(self.on_mouse_position)
- except (TypeError, AttributeError):
- pass
- self.c.mouse_state_updated.connect(self.on_mouse_state)
- self.c.mouse_position_updated.connect(self.on_mouse_position)
- else:
- self.big_cursor = False
- self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
- self.c.antialias = 0
- return self.c
- def on_mouse_state(self, state):
- if state:
- self.cursor_h_line.parent = self.view.scene
- self.cursor_v_line.parent = self.view.scene
- else:
- self.cursor_h_line.parent = None
- self.cursor_v_line.parent = None
- def on_mouse_position(self, pos):
- if self.fcapp.defaults['global_cursor_color_enabled']:
- color = Color(self.fcapp.defaults['global_cursor_color']).rgba
- else:
- color = self.line_color
- self.cursor_h_line.set_data(pos=pos[1], color=color)
- self.cursor_v_line.set_data(pos=pos[0], color=color)
- self.view.scene.update()
- def on_mouse_scroll(self, event):
- # key modifiers
- modifiers = event.modifiers
- pan_delta_x = self.fcapp.defaults["global_gridx"]
- pan_delta_y = self.fcapp.defaults["global_gridy"]
- curr_pos = event.pos
- # Controlled pan by mouse wheel
- if 'Shift' in modifiers:
- p1 = np.array(curr_pos)[:2]
- if event.delta[1] > 0:
- curr_pos[0] -= pan_delta_x
- else:
- curr_pos[0] += pan_delta_x
- p2 = np.array(curr_pos)[:2]
- self.view.camera.pan(p2 - p1)
- elif 'Control' in modifiers:
- p1 = np.array(curr_pos)[:2]
- if event.delta[1] > 0:
- curr_pos[1] += pan_delta_y
- else:
- curr_pos[1] -= pan_delta_y
- p2 = np.array(curr_pos)[:2]
- self.view.camera.pan(p2 - p1)
- if self.fcapp.grid_status():
- pos_canvas = self.translate_coords(curr_pos)
- pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
- # Update cursor
- self.fcapp.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
- symbol='++', edge_color=self.fcapp.cursor_color_3D,
- edge_width=self.fcapp.defaults["global_cursor_width"],
- size=self.fcapp.defaults["global_cursor_size"])
- def new_text_group(self, collection=None):
- if collection:
- return TextGroup(collection)
- else:
- return TextGroup(self.text_collection)
- def new_text_collection(self, **kwargs):
- return TextCollection(parent=self.view.scene, **kwargs)
- def fit_view(self, rect=None):
- # Lock updates in other threads
- self.shape_collection.lock_updates()
- if not rect:
- rect = Rect(-1, -1, 20, 20)
- try:
- rect.left, rect.right = self.shape_collection.bounds(axis=0)
- rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
- except TypeError:
- pass
- # adjust the view camera to be slightly bigger than the bounds so the shape collection can be seen clearly
- # otherwise the shape collection boundary will have no border
- dx = rect.right - rect.left
- dy = rect.top - rect.bottom
- x_factor = dx * 0.02
- y_factor = dy * 0.02
- rect.left -= x_factor
- rect.bottom -= y_factor
- rect.right += x_factor
- rect.top += y_factor
- # rect.left *= 0.96
- # rect.bottom *= 0.96
- # rect.right *= 1.04
- # rect.top *= 1.04
- # units = self.fcapp.defaults['units'].upper()
- # if units == 'MM':
- # compensation = 0.5
- # else:
- # compensation = 0.5 / 25.4
- # rect.left -= compensation
- # rect.bottom -= compensation
- # rect.right += compensation
- # rect.top += compensation
- self.view.camera.rect = rect
- self.shape_collection.unlock_updates()
- def fit_center(self, loc, rect=None):
- # Lock updates in other threads
- self.shape_collection.lock_updates()
- if not rect:
- try:
- rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
- except TypeError:
- pass
- self.view.camera.rect = rect
- self.shape_collection.unlock_updates()
- def clear(self):
- pass
- def redraw(self):
- self.shape_collection.redraw([])
- self.text_collection.redraw()
- def on_pool_recreated(self, pool):
- self.shape_collection.pool = pool
- class CursorBig(QtCore.QObject):
- """
- This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
- This way I don't have to chane (disable) things related to the cursor all over when
- using the low performance Matplotlib 2D graphic engine.
- """
- mouse_state_updated = QtCore.pyqtSignal(bool)
- mouse_position_updated = QtCore.pyqtSignal(list)
- def __init__(self, app):
- super().__init__()
- self.app = app
- self._enabled = None
- @property
- def enabled(self):
- return True if self._enabled else False
- @enabled.setter
- def enabled(self, value):
- self._enabled = value
- self.mouse_state_updated.emit(value)
- def set_data(self, pos, **kwargs):
- """Internal event handler to draw the cursor when the mouse moves."""
- if 'edge_color' in kwargs:
- color = kwargs['edge_color']
- else:
- if self.app.defaults['global_theme'] == 'white':
- color = '#000000FF'
- else:
- color = '#FFFFFFFF'
- position = [pos[0][0], pos[0][1]]
- self.mouse_position_updated.emit(position)
|