diff --git a/CHANGELOG.md b/CHANGELOG.md index 489f4d88..5a1bd2cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta ================================================= +12.12.2020 + +- added an experimental 3D area (archball camera) + 11.12.2020 - updated the 'Default_no_M6' preprocessor by removing the Tx command in the Toolchange section to make it compatible with GRBL controllers diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index 461a0a19..09a4d35d 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -454,9 +454,9 @@ class MainGUI(QtWidgets.QMainWindow): QtGui.QIcon(self.app.resource_location + '/pref.png'), '%s\t%s' % (_('Preferences'), _('Shift+P'))) - # ######################################################################## - # ########################## OPTIONS # ################################### - # ######################################################################## + # ############################################################################################################# + # ########################################### OPTIONS # ###################################################### + # ############################################################################################################# self.menuoptions = self.menu.addMenu(_('Options')) self.menuoptions_transform_rotate = self.menuoptions.addAction( @@ -492,9 +492,19 @@ class MainGUI(QtWidgets.QMainWindow): # Separator self.menuoptions.addSeparator() - # ######################################################################## - # ########################## View # ###################################### - # ######################################################################## + # ########################### Options ->Experimental ########################################################## + self.menuoptions_experimental = self.menuoptions.addMenu( + QtGui.QIcon(self.app.resource_location + '/experiment32.png'), _('Experimental')) + + self.menuoptions_experimental_3D_area = self.menuoptions_experimental.addAction( + QtGui.QIcon(self.app.resource_location + '/3D_area32.png'), + '%s\t%s' % (_('3D Area'), '')) + # Separator + self.menuoptions.addSeparator() + + # ############################################################################################################# + # ################################## View # ################################################################### + # ############################################################################################################# self.menuview = self.menu.addMenu(_('View')) self.menuviewenable = self.menuview.addAction( QtGui.QIcon(self.app.resource_location + '/replot16.png'), diff --git a/appGUI/PlotCanvas3d.py b/appGUI/PlotCanvas3d.py new file mode 100644 index 00000000..f259ae03 --- /dev/null +++ b/appGUI/PlotCanvas3d.py @@ -0,0 +1,451 @@ +# ########################################################## +# FlatCAM: 2D Post-processing for Manufacturing # +# Author: Dennis Hayrullin (c) # +# Date: 2016 # +# MIT Licence # +# ########################################################## + +from PyQt5 import QtCore +from PyQt5.QtGui import QPalette +# from PyQt5.QtCore import QSettings + +import time + +import vispy.scene as scene +from vispy.scene.cameras.base_camera import BaseCamera +from vispy.color import Color +from appGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor +from vispy.scene.visuals import InfiniteLine, Line, Rectangle, Text, XYZAxis + +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 + +white = Color("#ffffff") +black = Color("#000000") + + +class PlotCanvas3d(QtCore.QObject, scene.SceneCanvas): + """ + 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 settings.contains("axis_font_size"): + a_fsize = settings.value('axis_font_size', type=int) + else: + a_fsize = 8 + + if theme == 'white': + theme_color = Color('#FFFFFF') + tick_color = Color('#000000') + back_color = str(QPalette().color(QPalette.Window).name()) + else: + theme_color = Color('#000000') + tick_color = Color('gray') + back_color = Color('#000000') + + self.central_widget.bgcolor = back_color + self.central_widget.border_color = back_color + + self.grid_widget = self.central_widget.add_grid() + self.grid_widget.spacing = 0 + + top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2) + top_padding.height_max = 0 + + # self.yaxis = scene.AxisWidget( + # orientation='left', axis_color=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1 + # ) + # self.yaxis.width_max = 55 + # self.grid_widget.add_widget(self.yaxis, row=1, col=0) + # + # self.xaxis = scene.AxisWidget( + # orientation='bottom', axis_color=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1, + # anchors=['center', 'bottom'] + # ) + # self.xaxis.height_max = 30 + # self.grid_widget.add_widget(self.xaxis, row=2, col=1) + + right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2) + # right_padding.width_max = 24 + right_padding.width_max = 0 + + self.view = self.grid_widget.add_view(row=1, col=1, border_color=tick_color, bgcolor=theme_color) + # self.view.camera = Camera_3D(aspect=1, rect=(-25, -25, 150, 150)) + self.view.camera = Camera_3D() + + xax = scene.Axis(pos=[[0, 0], [0, 500]], tick_direction=(0, 1), domain=(0, 500), + font_size=16, axis_color='k', tick_color='k', text_color='k', + parent=self.view.scene) + xax.transform = scene.STTransform(translate=(0, 0, -0.2)) + + yax = scene.Axis(pos=[[0, 0], [500, 0]], tick_direction=(1, 0), domain=(0, 500), + font_size=16, axis_color='k', tick_color='k', text_color='k', + parent=self.view.scene) + yax.transform = scene.STTransform(translate=(0, 0, -0.2)) + + self.xyz_axis = XYZAxis(parent=self.view.scene) + + # Following function was removed from 'prepare_draw()' of 'Grid' class by patch, + # it is necessary to call manually + # self.grid_widget._update_child_widget_dim() + + # self.xaxis.link_view(self.view) + # self.yaxis.link_view(self.view) + + # if theme == 'white': + # self.grid = scene.GridLines(parent=self.view.scene, color='dimgray') + # else: + # self.grid = scene.GridLines(parent=self.view.scene, color='#dededeff') + # + # self.grid.set_gl_state(depth_test=False) + + # 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.create_native() + self.native.setParent(self.fcapp.ui) + + # + self.container.addWidget(self.native) + + 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 + + # 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"].upper() + + # 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 + + # 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 + + # 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 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 translate_coords(self, pos): + """ + Translate pixels to FlatCAM units. + + """ + tr = self.grid.get_transform('canvas', 'visual') + return tr.map(pos) + + def translate_coords_2(self, pos): + """ + Translate FlatCAM units to pixels. + """ + tr = self.grid.get_transform('visual', 'document') + return tr.map(pos) + + 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): + """ + Will create a mouse cursor pointer on canvas + + :return: the mouse cursor object + """ + self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene) + self.c.antialias = 0 + return self.c + + 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 + + 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 Camera_3D(scene.ArcballCamera): + + def __init__(self, **kwargs): + super(Camera_3D, self).__init__(**kwargs) + + self.minimum_scene_size = 0.01 + self.maximum_scene_size = 10000 + + self.last_event = None + self.last_time = 0 + + # Default mouse button for panning is RMB + self.pan_button_setting = "2" + + def zoom(self, factor, center=None): + center = center if (center is not None) else self.center + super(Camera_3D, self).zoom(factor, center) + + # def viewbox_mouse_event(self, event): + # """ + # The SubScene received a mouse event; update transform + # accordingly. + # + # Parameters + # ---------- + # event : instance of Event + # The event. + # """ + # if event.handled or not self.interactive: + # return + # + # # key modifiers + # modifiers = event.mouse_event.modifiers + # + # # Limit mouse move events + # last_event = event.last_event + # t = time.time() + # if t - self.last_time > 0.015: + # self.last_time = t + # if self.last_event: + # last_event = self.last_event + # self.last_event = None + # else: + # if not self.last_event: + # self.last_event = last_event + # event.handled = True + # return + # + # # ################### Scrolling ########################## + # BaseCamera.viewbox_mouse_event(self, event) + # + # if event.type == 'mouse_wheel': + # if not modifiers: + # center = self._scene_transform.imap(event.pos) + # scale = (1 + self.zoom_factor) ** (-event.delta[1] * 30) + # self.limited_zoom(scale, center) + # event.handled = True + # + # elif event.type == 'mouse_move': + # if event.press_event is None: + # return + # + # # ################ Panning ############################ + # # self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button'] + # if event.button == int(self.pan_button_setting) and not modifiers: + # # Translate + # p1 = np.array(last_event.pos)[:2] + # p2 = np.array(event.pos)[:2] + # p1s = self._transform.imap(p1) + # p2s = self._transform.imap(p2) + # self.pan(p1s-p2s) + # event.handled = True + # elif event.button in [2, 3] and 'Shift' in modifiers: + # # Zoom + # p1c = np.array(last_event.pos)[:2] + # p2c = np.array(event.pos)[:2] + # scale = ((1 + self.zoom_factor) ** + # ((p1c-p2c) * np.array([1, -1]))) + # center = self._transform.imap(event.press_event.pos[:2]) + # self.limited_zoom(scale, center) + # event.handled = True + # else: + # event.handled = False + # elif event.type == 'mouse_press': + # # accept the event if it is button 1 or 2. + # # This is required in order to receive future events + # event.handled = event.button in [1, 2, 3] + # else: + # event.handled = False + + def limited_zoom(self, scale, center): + + try: + zoom_in = scale[1] < 1 + except IndexError: + zoom_in = scale < 1 + + if (not zoom_in and self.rect.width < self.maximum_scene_size) \ + or (zoom_in and self.rect.width > self.minimum_scene_size): + self.zoom(scale, center) diff --git a/appGUI/VisPyPatches.py b/appGUI/VisPyPatches.py index 36881e0c..5e7b3436 100644 --- a/appGUI/VisPyPatches.py +++ b/appGUI/VisPyPatches.py @@ -98,6 +98,9 @@ def apply_patches(): offset = domain[0] scale = domain[1] - domain[0] + if scale == 0 or not scale: + scale = 0.00000001 + transforms = self.axis.transforms length = self.axis.pos[1] - self.axis.pos[0] # in logical coords n_inches = np.sqrt(np.sum(length ** 2)) / transforms.dpi diff --git a/app_Main.py b/app_Main.py index 31af94b6..d788fac7 100644 --- a/app_Main.py +++ b/app_Main.py @@ -73,6 +73,7 @@ from camlib import to_dict, dict2obj, ET, ParseError, Geometry, CNCjob # FlatCAM appGUI from appGUI.PlotCanvas import * from appGUI.PlotCanvasLegacy import * +from appGUI.PlotCanvas3d import * from appGUI.MainGUI import * from appGUI.GUIElements import FCFileSaveDialog, message_dialog, FlatCAMSystemTray, FCInputDialogSlider @@ -1078,6 +1079,11 @@ class App(QtCore.QObject): self.collection.view.setMinimumWidth(290) self.log.debug("Finished creating Object Collection.") + # ########################################################################################################### + # ######################################## SETUP 3D Area #################################################### + # ########################################################################################################### + self.area_3d_tab = QtWidgets.QWidget() + # ########################################################################################################### # ######################################## SETUP Plot Area ################################################## # ########################################################################################################### @@ -2101,6 +2107,7 @@ class App(QtCore.QObject): self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy) self.ui.menuoptions_view_source.triggered.connect(self.on_view_source) self.ui.menuoptions_tools_db.triggered.connect(lambda: self.on_tools_database(source='app')) + self.ui.menuoptions_experimental_3D_area.triggered.connect(self.on_3d_area) def connect_menuview_signals(self): self.ui.menuviewenable.triggered.connect(self.enable_all_plots) @@ -6063,6 +6070,56 @@ class App(QtCore.QObject): # detect changes in the Tools in Tools DB, connect signals from table widget in tab self.tools_db_tab.ui_connect() + def on_3d_area(self): + if self.is_legacy is True: + msg = '[ERROR_NOTCL] %s' % _("Not available for Legacy 2D graphic mode.") + self.inform.emit(msg) + return + + # add the tab if it was closed + try: + self.ui.plot_tab_area.addTab(self.area_3d_tab, _("3D Area")) + self.area_3d_tab.setObjectName("3D_area_tab") + except Exception as e: + self.log.debug("App.on_3d_area() --> %s" % str(e)) + return + + plot_container_3d = QtWidgets.QVBoxLayout() + self.area_3d_tab.setLayout(plot_container_3d) + + try: + plotcanvas3d = PlotCanvas3d(plot_container_3d, self) + except Exception as er: + msg_txt = traceback.format_exc() + self.log.debug("App.on_3d_area() failed -> %s" % str(er)) + self.log.debug("OpenGL canvas initialization failed with the following error.\n" + msg_txt) + msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n") + msg += msg_txt + self.inform.emit(msg) + return 'fail' + + # So it can receive key presses + plotcanvas3d.native.setFocus() + + pan_button = 2 if self.defaults["global_pan_button"] == '2' else 3 + # Set the mouse button for panning + plotcanvas3d.view.camera.pan_button_setting = pan_button + + # self.mm = plotcanvas3D.graph_event_connect('mouse_move', self.on_mouse_move_over_plot) + # self.mp = plotcanvas3D.graph_event_connect('mouse_press', self.on_mouse_click_over_plot) + # self.mr = plotcanvas3D.graph_event_connect('mouse_release', self.on_mouse_click_release_over_plot) + # self.mdc = plotcanvas3D.graph_event_connect('mouse_double_click', self.on_mouse_double_click_over_plot) + + # Keys over plot enabled + # self.kp = plotcanvas3D.graph_event_connect('key_press', self.ui.keyPressEvent) + + # hide coordinates toolbars in the infobar + self.ui.coords_toolbar.hide() + self.ui.delta_coords_toolbar.hide() + + # Switch plot_area to Area 3D page + self.ui.plot_tab_area.setCurrentWidget(self.area_3d_tab) + def on_geometry_tool_add_from_db_executed(self, tool): """ Here add the tool from DB in the selected geometry object. @@ -6161,6 +6218,9 @@ class App(QtCore.QObject): elif tab_obj_name == "bookmarks_tab": self.book_dialog_tab.rebuild_actions() self.book_dialog_tab.deleteLater() + elif tab_obj_name == '3D_area_tab': + self.area_3d_tab.deleteLater() + self.area_3d_tab = QtWidgets.QWidget() else: pass diff --git a/assets/resources/3d_area32.png b/assets/resources/3d_area32.png new file mode 100644 index 00000000..edb66f04 Binary files /dev/null and b/assets/resources/3d_area32.png differ diff --git a/assets/resources/dark_resources/3D_area32.png b/assets/resources/dark_resources/3D_area32.png new file mode 100644 index 00000000..d5dbe7a0 Binary files /dev/null and b/assets/resources/dark_resources/3D_area32.png differ diff --git a/assets/resources/dark_resources/experiment32.png b/assets/resources/dark_resources/experiment32.png new file mode 100644 index 00000000..25d463bc Binary files /dev/null and b/assets/resources/dark_resources/experiment32.png differ diff --git a/assets/resources/experiment32.png b/assets/resources/experiment32.png new file mode 100644 index 00000000..047e8215 Binary files /dev/null and b/assets/resources/experiment32.png differ