Snap-to feature in drawing tool.

This commit is contained in:
Juan Pablo Caram
2014-11-30 13:48:16 -05:00
parent 2dad3ce903
commit 7a95d739ef
3 changed files with 190 additions and 43 deletions

View File

@@ -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.deactivate()
self.draw.drawing_toolbar.setDisabled(True)
geo.plot() geo.plot()
def get_last_folder(self): def get_last_folder(self):

View File

@@ -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() # self.move_timer.stop()
#
if self.active_tool is None: # if self.active_tool is None:
return # return
#
# Make a function to avoid late evaluation # # Make a function to avoid late evaluation
def make_callback(): # def make_callback():
def f(): # def f():
self.on_canvas_move_effective(event) # self.on_canvas_move_effective(event)
return f # return f
callback = make_callback() # callback = make_callback()
#
self.move_timer.timeout.connect(callback) # self.move_timer.timeout.connect(callback)
self.move_timer.start(500) # Stops if aready running # 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 Adjusts coordinates to snap settings.
into the editor.
:param fcgeometry: FlatCAMGeometry :param x: Input coordinate X
:return: None :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. snap_x, snap_y = (x, y)
self.shape_buffer = [] snap_distance = Inf
# Link shapes into editor. ### Object (corner?) snap
for shape in geometry: if self.options["corner_snap"]:
self.shape_buffer.append({'geometry': shape, try:
'selected': False, bbox = self.rtree_index.nearest((x, y), objects=True).next().bbox
'utility': False}) nearest_pt = (bbox[0], bbox[1])
self.replot() nearest_pt_distance = distance((x, y), nearest_pt)
self.drawing_toolbar.setDisabled(False) 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
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_)
return snap_x, snap_y
def update_fcgeometry(self, fcgeometry): def update_fcgeometry(self, fcgeometry):
""" """
@@ -637,3 +780,7 @@ class FlatCAMDraw:
}) })
self.replot() self.replot()
def distance(pt1, pt2):
return sqrt((pt1[0]-pt2[0])**2 + (pt1[1]-pt2[1])**2)

View File

@@ -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}