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
self.draw.update_fcgeometry(geo)
self.draw.clear()
self.draw.drawing_toolbar.setDisabled(True)
self.draw.deactivate()
geo.plot()
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 mpl_toolkits.axes_grid.anchored_artists import AnchoredDrawingArea
from rtree import index as rtindex
class DrawTool(object):
def __init__(self, draw_app):
@ -208,6 +211,12 @@ class FCMove(FCShapeTool):
self.complete = True
def utility_geometry(self, data=None):
"""
Temporary geometry on screen while using this tool.
:param data:
:return:
"""
if self.origin is 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.complete = True
class FlatCAMDraw:
########################
### Main Application ###
########################
class FlatCAMDraw(QtCore.QObject):
def __init__(self, app, disabled=False):
assert isinstance(app, FlatCAMApp.App)
super(FlatCAMDraw, self).__init__()
self.app = app
self.canvas = app.plotcanvas
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.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 ###
## Canvas events
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"].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):
self.active_tool = None
self.shape_buffer = []
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):
"""
Behavior of the toolbar. Tool initialization.
:rtype : None
"""
@ -328,7 +433,7 @@ class FlatCAMDraw:
"""
if self.active_tool is not None:
# 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)
# If it is a shape generating tool
@ -353,20 +458,20 @@ class FlatCAMDraw:
self.on_canvas_move_effective(event)
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):
"""
@ -391,9 +496,13 @@ class FlatCAMDraw:
if self.active_tool is None:
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))
if geo is not None:
if geo is not None and not geo.is_empty:
# Remove any previous utility shape
for shape in self.shape_buffer:
@ -408,14 +517,21 @@ class FlatCAMDraw:
})
# Efficient plotting for fast animation
#self.canvas.canvas.restore_region(self.canvas.background)
elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
self.canvas.canvas.restore_region(self.canvas.background)
for el in elements:
self.axes.draw_artist(el)
self.canvas.canvas.blit(self.axes.bbox)
#self.canvas.canvas.blit(self.axes.bbox)
#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):
"""
event.key has the key.
@ -429,7 +545,7 @@ class FlatCAMDraw:
### complete automatically, like a polygon or path.
if event.key == ' ':
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()
if self.active_tool.complete:
self.on_shape_complete()
@ -537,6 +653,15 @@ class FlatCAMDraw:
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):
self.app.log.debug("on_shape_complete()")
@ -551,10 +676,12 @@ class FlatCAMDraw:
self.shape_buffer.append({'geometry': geo,
'selected': False,
'utility': False})
self.add2index(len(self.shape_buffer)-1, geo)
except TypeError:
self.shape_buffer.append({'geometry': self.active_tool.geometry,
'selected': False,
'utility': False})
self.add2index(len(self.shape_buffer)-1, self.active_tool.geometry)
# Remove any utility shapes
for shape in self.shape_buffer:
@ -569,31 +696,47 @@ class FlatCAMDraw:
self.axes = self.canvas.new_axes("draw")
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
# Link shapes into editor.
for shape in geometry:
self.shape_buffer.append({'geometry': shape,
'selected': False,
'utility': False})
### 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])
self.replot()
self.drawing_toolbar.setDisabled(False)
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
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):
"""
@ -636,4 +779,8 @@ class FlatCAMDraw:
'utility': False
})
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}