Просмотр исходного кода

Snap-to feature in drawing tool.

Juan Pablo Caram 11 лет назад
Родитель
Сommit
7a95d739ef
3 измененных файлов с 190 добавлено и 43 удалено
  1. 2 2
      FlatCAMApp.py
  2. 187 40
      FlatCAMDraw.py
  3. 1 1
      defaults.json

+ 2 - 2
FlatCAMApp.py

@@ -516,8 +516,8 @@ class App(QtCore.QObject):
             return
             return
 
 
         self.draw.update_fcgeometry(geo)
         self.draw.update_fcgeometry(geo)
-        self.draw.clear()
-        self.draw.drawing_toolbar.setDisabled(True)
+        self.draw.deactivate()
+
         geo.plot()
         geo.plot()
 
 
     def get_last_folder(self):
     def get_last_folder(self):

+ 187 - 40
FlatCAMDraw.py

@@ -12,6 +12,9 @@ from shapely.geometry.base import BaseGeometry
 
 
 from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
 from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
 
 
+from mpl_toolkits.axes_grid.anchored_artists import AnchoredDrawingArea
+
+from rtree import index as rtindex
 
 
 class DrawTool(object):
 class DrawTool(object):
     def __init__(self, draw_app):
     def __init__(self, draw_app):
@@ -208,6 +211,12 @@ class FCMove(FCShapeTool):
         self.complete = True
         self.complete = True
 
 
     def utility_geometry(self, data=None):
     def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data:
+        :return:
+        """
         if self.origin is None:
         if self.origin is None:
             return None
             return None
 
 
@@ -225,9 +234,15 @@ class FCCopy(FCMove):
         self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
         self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
         self.complete = True
         self.complete = True
 
 
-class FlatCAMDraw:
+
+########################
+### Main Application ###
+########################
+class FlatCAMDraw(QtCore.QObject):
     def __init__(self, app, disabled=False):
     def __init__(self, app, disabled=False):
         assert isinstance(app, FlatCAMApp.App)
         assert isinstance(app, FlatCAMApp.App)
+        super(FlatCAMDraw, self).__init__()
+
         self.app = app
         self.app = app
         self.canvas = app.plotcanvas
         self.canvas = app.plotcanvas
         self.axes = self.canvas.new_axes("draw")
         self.axes = self.canvas.new_axes("draw")
@@ -245,6 +260,24 @@ class FlatCAMDraw:
         self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/move32.png'), 'Move Objects')
         self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/move32.png'), 'Move Objects')
         self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/copy32.png'), 'Copy Objects')
         self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/copy32.png'), 'Copy Objects')
 
 
+        ### Snap Toolbar ###
+        self.snap_toolbar = QtGui.QToolBar()
+        self.grid_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/grid32.png'), 'Snap to grid')
+        self.grid_gap_x_entry = QtGui.QLineEdit()
+        self.grid_gap_x_entry.setMaximumWidth(70)
+        self.snap_toolbar.addWidget(self.grid_gap_x_entry)
+        self.grid_gap_y_entry = QtGui.QLineEdit()
+        self.grid_gap_y_entry.setMaximumWidth(70)
+        self.snap_toolbar.addWidget(self.grid_gap_y_entry)
+
+        self.corner_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/corner32.png'), 'Snap to corner')
+        self.snap_max_dist_entry = QtGui.QLineEdit()
+        self.snap_max_dist_entry.setMaximumWidth(70)
+        self.snap_toolbar.addWidget(self.snap_max_dist_entry)
+
+        self.snap_toolbar.setDisabled(disabled)
+        self.app.ui.addToolBar(self.snap_toolbar)
+
         ### Event handlers ###
         ### Event handlers ###
         ## Canvas events
         ## Canvas events
         self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
         self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
@@ -290,13 +323,85 @@ class FlatCAMDraw:
             self.tools[tool]["button"].triggered.connect(make_callback(tool))  # Events
             self.tools[tool]["button"].triggered.connect(make_callback(tool))  # Events
             self.tools[tool]["button"].setCheckable(True)  # Checkable
             self.tools[tool]["button"].setCheckable(True)  # Checkable
 
 
+        # for snap_tool in [self.grid_snap_btn, self.corner_snap_btn]:
+        #     snap_tool.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap"))
+        #     snap_tool.setCheckable(True)
+        self.grid_snap_btn.setCheckable(True)
+        self.grid_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap"))
+        self.corner_snap_btn.setCheckable(True)
+        self.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap"))
+
+        self.options = {
+            "snap-x": 0.1,
+            "snap-y": 0.1,
+            "snap_max": 0.05,
+            "grid_snap": False,
+            "corner_snap": False,
+        }
+
+        self.grid_gap_x_entry.setText(str(self.options["snap-x"]))
+        self.grid_gap_y_entry.setText(str(self.options["snap-y"]))
+        self.snap_max_dist_entry.setText(str(self.options["snap_max"]))
+
+        self.rtree_index = rtindex.Index()
+
+        def entry2option(option, entry):
+            self.options[option] = float(entry.text())
+
+        self.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
+        self.grid_gap_x_entry.editingFinished.connect(lambda: entry2option("snap-x", self.grid_gap_x_entry))
+        self.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator())
+        self.grid_gap_y_entry.editingFinished.connect(lambda: entry2option("snap-y", self.grid_gap_y_entry))
+        self.snap_max_dist_entry.setValidator(QtGui.QDoubleValidator())
+        self.snap_max_dist_entry.editingFinished.connect(lambda: entry2option("snap_max", self.snap_max_dist_entry))
+
+    def activate(self):
+        pass
+
+    def deactivate(self):
+        self.clear()
+        self.drawing_toolbar.setDisabled(True)
+        self.snap_toolbar.setDisabled(True)  # TODO: Combine and move into tool
+
+    def toolbar_tool_toggle(self, key):
+        self.options[key] = self.sender().isChecked()
+        print "grid_snap", self.options["grid_snap"]
+
     def clear(self):
     def clear(self):
         self.active_tool = None
         self.active_tool = None
         self.shape_buffer = []
         self.shape_buffer = []
         self.replot()
         self.replot()
 
 
+    def edit_fcgeometry(self, fcgeometry):
+        """
+        Imports the geometry from the given FlatCAM Geometry object
+        into the editor.
+
+        :param fcgeometry: FlatCAMGeometry
+        :return: None
+        """
+        try:
+            _ = iter(fcgeometry.solid_geometry)
+            geometry = fcgeometry.solid_geometry
+        except TypeError:
+            geometry = [fcgeometry.solid_geometry]
+
+        # Delete contents of editor.
+        self.shape_buffer = []
+
+        # Link shapes into editor.
+        for shape in geometry:
+            self.shape_buffer.append({'geometry': shape,
+                                      'selected': False,
+                                      'utility': False})
+
+        self.replot()
+        self.drawing_toolbar.setDisabled(False)
+        self.snap_toolbar.setDisabled(False)
+
     def on_tool_select(self, tool):
     def on_tool_select(self, tool):
         """
         """
+        Behavior of the toolbar. Tool initialization.
 
 
         :rtype : None
         :rtype : None
         """
         """
@@ -328,7 +433,7 @@ class FlatCAMDraw:
         """
         """
         if self.active_tool is not None:
         if self.active_tool is not None:
             # Dispatch event to active_tool
             # Dispatch event to active_tool
-            msg = self.active_tool.click((event.xdata, event.ydata))
+            msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
             self.app.info(msg)
             self.app.info(msg)
 
 
             # If it is a shape generating tool
             # If it is a shape generating tool
@@ -353,20 +458,20 @@ class FlatCAMDraw:
         self.on_canvas_move_effective(event)
         self.on_canvas_move_effective(event)
         return
         return
 
 
-        self.move_timer.stop()
-
-        if self.active_tool is None:
-            return
-
-        # Make a function to avoid late evaluation
-        def make_callback():
-            def f():
-                self.on_canvas_move_effective(event)
-            return f
-        callback = make_callback()
-
-        self.move_timer.timeout.connect(callback)
-        self.move_timer.start(500)  # Stops if aready running
+        # self.move_timer.stop()
+        #
+        # if self.active_tool is None:
+        #     return
+        #
+        # # Make a function to avoid late evaluation
+        # def make_callback():
+        #     def f():
+        #         self.on_canvas_move_effective(event)
+        #     return f
+        # callback = make_callback()
+        #
+        # self.move_timer.timeout.connect(callback)
+        # self.move_timer.start(500)  # Stops if aready running
 
 
     def on_canvas_move_effective(self, event):
     def on_canvas_move_effective(self, event):
         """
         """
@@ -391,9 +496,13 @@ class FlatCAMDraw:
         if self.active_tool is None:
         if self.active_tool is None:
             return
             return
 
 
+        x, y = self.snap(x, y)
+
+        ### Utility geometry (animated)
+        self.canvas.canvas.restore_region(self.canvas.background)
         geo = self.active_tool.utility_geometry(data=(x, y))
         geo = self.active_tool.utility_geometry(data=(x, y))
 
 
-        if geo is not None:
+        if geo is not None and not geo.is_empty:
 
 
             # Remove any previous utility shape
             # Remove any previous utility shape
             for shape in self.shape_buffer:
             for shape in self.shape_buffer:
@@ -408,14 +517,21 @@ class FlatCAMDraw:
             })
             })
 
 
             # Efficient plotting for fast animation
             # Efficient plotting for fast animation
+
+            #self.canvas.canvas.restore_region(self.canvas.background)
             elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
             elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
-            self.canvas.canvas.restore_region(self.canvas.background)
             for el in elements:
             for el in elements:
                 self.axes.draw_artist(el)
                 self.axes.draw_artist(el)
-            self.canvas.canvas.blit(self.axes.bbox)
+            #self.canvas.canvas.blit(self.axes.bbox)
 
 
             #self.replot()
             #self.replot()
 
 
+
+        elements = self.axes.plot(x, y, 'bo', animated=True)
+        for el in elements:
+                self.axes.draw_artist(el)
+        self.canvas.canvas.blit(self.axes.bbox)
+
     def on_canvas_key(self, event):
     def on_canvas_key(self, event):
         """
         """
         event.key has the key.
         event.key has the key.
@@ -429,7 +545,7 @@ class FlatCAMDraw:
         ### complete automatically, like a polygon or path.
         ### complete automatically, like a polygon or path.
         if event.key == ' ':
         if event.key == ' ':
             if isinstance(self.active_tool, FCShapeTool):
             if isinstance(self.active_tool, FCShapeTool):
-                self.active_tool.click((event.xdata, event.ydata))
+                self.active_tool.click(self.snap(event.xdata, event.ydata))
                 self.active_tool.make()
                 self.active_tool.make()
                 if self.active_tool.complete:
                 if self.active_tool.complete:
                     self.on_shape_complete()
                     self.on_shape_complete()
@@ -537,6 +653,15 @@ class FlatCAMDraw:
 
 
         self.canvas.auto_adjust_axes()
         self.canvas.auto_adjust_axes()
 
 
+    def add2index(self, id, geo):
+        try:
+            for pt in geo.coords:
+                self.rtree_index.add(id, pt)
+        except NotImplementedError:
+            # It's a polygon?
+            for pt in geo.exterior.coords:
+                self.rtree_index.add(id, pt)
+
     def on_shape_complete(self):
     def on_shape_complete(self):
         self.app.log.debug("on_shape_complete()")
         self.app.log.debug("on_shape_complete()")
 
 
@@ -551,10 +676,12 @@ class FlatCAMDraw:
                 self.shape_buffer.append({'geometry': geo,
                 self.shape_buffer.append({'geometry': geo,
                                           'selected': False,
                                           'selected': False,
                                           'utility': False})
                                           'utility': False})
+                self.add2index(len(self.shape_buffer)-1, geo)
         except TypeError:
         except TypeError:
             self.shape_buffer.append({'geometry': self.active_tool.geometry,
             self.shape_buffer.append({'geometry': self.active_tool.geometry,
                                       'selected': False,
                                       'selected': False,
                                       'utility': False})
                                       'utility': False})
+            self.add2index(len(self.shape_buffer)-1, self.active_tool.geometry)
 
 
         # Remove any utility shapes
         # Remove any utility shapes
         for shape in self.shape_buffer:
         for shape in self.shape_buffer:
@@ -569,31 +696,47 @@ class FlatCAMDraw:
         self.axes = self.canvas.new_axes("draw")
         self.axes = self.canvas.new_axes("draw")
         self.plot_all()
         self.plot_all()
 
 
-    def edit_fcgeometry(self, fcgeometry):
+    def snap(self, x, y):
         """
         """
-        Imports the geometry from the given FlatCAM Geometry object
-        into the editor.
+        Adjusts coordinates to snap settings.
 
 
-        :param fcgeometry: FlatCAMGeometry
-        :return: None
+        :param x: Input coordinate X
+        :param y: Input coordinate Y
+        :return: Snapped (x, y)
         """
         """
-        try:
-            _ = iter(fcgeometry.solid_geometry)
-            geometry = fcgeometry.solid_geometry
-        except TypeError:
-            geometry = [fcgeometry.solid_geometry]
 
 
-        # Delete contents of editor.
-        self.shape_buffer = []
+        snap_x, snap_y = (x, y)
+        snap_distance = Inf
+
+        ### Object (corner?) snap
+        if self.options["corner_snap"]:
+            try:
+                bbox = self.rtree_index.nearest((x, y), objects=True).next().bbox
+                nearest_pt = (bbox[0], bbox[1])
+
+                nearest_pt_distance = distance((x, y), nearest_pt)
+                if nearest_pt_distance <= self.options["snap_max"]:
+                    snap_distance = nearest_pt_distance
+                    snap_x, snap_y = nearest_pt
+            except StopIteration:
+                pass
+
+        ### Grid snap
+        if self.options["grid_snap"]:
+            if self.options["snap-x"] != 0:
+                snap_x_ = round(x/self.options["snap-x"])*self.options['snap-x']
+            else:
+                snap_x_ = x
 
 
-        # Link shapes into editor.
-        for shape in geometry:
-            self.shape_buffer.append({'geometry': shape,
-                                      'selected': False,
-                                      'utility': False})
+            if self.options["snap-y"] != 0:
+                snap_y_ = round(y/self.options["snap-y"])*self.options['snap-y']
+            else:
+                snap_y_ = y
+            nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
+            if nearest_grid_distance < snap_distance:
+                snap_x, snap_y = (snap_x_, snap_y_)
 
 
-        self.replot()
-        self.drawing_toolbar.setDisabled(False)
+        return snap_x, snap_y
 
 
     def update_fcgeometry(self, fcgeometry):
     def update_fcgeometry(self, fcgeometry):
         """
         """
@@ -636,4 +779,8 @@ class FlatCAMDraw:
                 'utility': False
                 'utility': False
             })
             })
 
 
-        self.replot()
+        self.replot()
+
+
+def distance(pt1, pt2):
+    return sqrt((pt1[0]-pt2[0])**2 + (pt1[1]-pt2[1])**2)

+ 1 - 1
defaults.json

@@ -1 +1 @@
-{}
+{"cncjob_append": "", "gerber_noncopperrounded": false, "geometry_paintoverlap": 0.15, "geometry_plot": true, "zoom_ratio": 1.5, "shell_at_startup": false, "gerber_isotooldia": 0.016, "serial": "32qac2m529hogh3lo33a", "shell_shape": [500, 300], "zoom_in_key": "3", "zoom_out_key": "2", "stats": {"save_defaults": 30}, "recent_limit": 10, "gerber_plot": true, "defaults_save_period_ms": 20000, "gerber_cutoutgapsize": 0.15, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "fit_key": "1", "excellon_plot": true, "excellon_feedrate": 3.0, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.1, "excellon_solid": false, "geometry_paintmargin": 0.0, "geometry_cutz": -0.002, "gerber_noncoppermargin": 0.0, "gerber_cutouttooldia": 0.07, "zdownrate": null, "gerber_gaps": "4", "last_folder": null, "gerber_bboxmargin": 0.0, "point_clipboard_format": "(%.4f, %.4f)", "cncjob_plot": true, "excellon_drillz": -0.1, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "geometry_cnctooldia": 0.016, "geometry_painttooldia": 0.07}