Snap-to feature in drawing tool.
This commit is contained in:
parent
2dad3ce903
commit
7a95d739ef
|
@ -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):
|
||||
|
|
227
FlatCAMDraw.py
227
FlatCAMDraw.py
|
@ -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)
|
|
@ -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}
|
Loading…
Reference in New Issue