Encapsulated shapes in DrawToolShape in DrawingTool. Selected shapes are now in list.

This commit is contained in:
jpcaram 2014-12-25 18:25:30 -05:00
parent 3d3f1d40cb
commit 9bfa13a5b6
3 changed files with 231 additions and 97 deletions

View File

@ -19,6 +19,22 @@ from mpl_toolkits.axes_grid.anchored_artists import AnchoredDrawingArea
from rtree import index as rtindex
class DrawToolShape(object):
def __init__(self, geo=[]):
# Shapely type or list of such
self.geo = geo
self.utility = False
class DrawToolUtilityShape(DrawToolShape):
def __init__(self, geo=[]):
super(DrawToolUtilityShape, self).__init__(geo=geo)
self.utility = True
class DrawTool(object):
"""
Abstract Class representing a tool in the drawing
@ -31,7 +47,7 @@ class DrawTool(object):
self.complete = False
self.start_msg = "Click on 1st point..."
self.points = []
self.geometry = None
self.geometry = None # DrawToolShape or None
def click(self, point):
"""
@ -80,7 +96,7 @@ class FCCircle(FCShapeTool):
p1 = self.points[0]
p2 = data
radius = sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
return Point(p1).buffer(radius)
return DrawToolUtilityShape(Point(p1).buffer(radius))
return None
@ -88,7 +104,7 @@ class FCCircle(FCShapeTool):
p1 = self.points[0]
p2 = self.points[1]
radius = distance(p1, p2)
self.geometry = Point(p1).buffer(radius)
self.geometry = DrawToolShape(Point(p1).buffer(radius))
self.complete = True
@ -144,7 +160,7 @@ class FCArc(FCShapeTool):
center = self.points[0]
p1 = data
return LineString([center, p1])
return DrawToolUtilityShape(LineString([center, p1]))
if len(self.points) == 2: # Show the arc
@ -157,9 +173,9 @@ class FCArc(FCShapeTool):
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
return [LineString(arc(center, radius, startangle, stopangle,
return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
self.direction, self.steps_per_circ)),
Point(center)]
Point(center)])
elif self.mode == '132':
p1 = array(self.points[0])
@ -172,9 +188,9 @@ class FCArc(FCShapeTool):
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
return [LineString(arc(center, radius, startangle, stopangle,
return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
direction, self.steps_per_circ)),
Point(center), Point(p1), Point(p3)]
Point(center), Point(p1), Point(p3)])
else: # '12c'
p1 = array(self.points[0])
@ -205,9 +221,9 @@ class FCArc(FCShapeTool):
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
return [LineString(arc(center, radius, startangle, stopangle,
return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
self.direction, self.steps_per_circ)),
Point(center)]
Point(center)])
return None
@ -221,8 +237,8 @@ class FCArc(FCShapeTool):
radius = distance(center, p1)
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
self.geometry = LineString(arc(center, radius, startangle, stopangle,
self.direction, self.steps_per_circ))
self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
self.direction, self.steps_per_circ)))
elif self.mode == '132':
p1 = array(self.points[0])
@ -235,8 +251,8 @@ class FCArc(FCShapeTool):
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
self.geometry = LineString(arc(center, radius, startangle, stopangle,
direction, self.steps_per_circ))
self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
direction, self.steps_per_circ)))
else: # self.mode == '12c'
p1 = array(self.points[0])
@ -268,8 +284,8 @@ class FCArc(FCShapeTool):
startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
self.geometry = LineString(arc(center, radius, startangle, stopangle,
self.direction, self.steps_per_circ))
self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
self.direction, self.steps_per_circ)))
self.complete = True
@ -298,7 +314,7 @@ class FCRectangle(FCShapeTool):
if len(self.points) == 1:
p1 = self.points[0]
p2 = data
return LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
return DrawToolUtilityShape(LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
return None
@ -306,7 +322,7 @@ class FCRectangle(FCShapeTool):
p1 = self.points[0]
p2 = self.points[1]
#self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
self.geometry = Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
self.geometry = DrawToolShape(Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
self.complete = True
@ -331,18 +347,18 @@ class FCPolygon(FCShapeTool):
if len(self.points) == 1:
temp_points = [x for x in self.points]
temp_points.append(data)
return LineString(temp_points)
return DrawToolUtilityShape(LineString(temp_points))
if len(self.points) > 1:
temp_points = [x for x in self.points]
temp_points.append(data)
return LinearRing(temp_points)
return DrawToolUtilityShape(LinearRing(temp_points))
return None
def make(self):
# self.geometry = LinearRing(self.points)
self.geometry = Polygon(self.points)
self.geometry = DrawToolShape(Polygon(self.points))
self.complete = True
@ -352,14 +368,14 @@ class FCPath(FCPolygon):
"""
def make(self):
self.geometry = LineString(self.points)
self.geometry = DrawToolShape(LineString(self.points))
self.complete = True
def utility_geometry(self, data=None):
if len(self.points) > 1:
temp_points = [x for x in self.points]
temp_points.append(data)
return LineString(temp_points)
return DrawToolUtilityShape(LineString(temp_points))
return None
@ -368,6 +384,7 @@ class FCSelect(DrawTool):
def __init__(self, draw_app):
DrawTool.__init__(self, draw_app)
self.shape_buffer = self.draw_app.shape_buffer
self.selected = self.draw_app.selected
self.start_msg = "Click on geometry to select"
def click(self, point):
@ -375,17 +392,21 @@ class FCSelect(DrawTool):
closest_shape = None
for shape in self.shape_buffer:
# Remove all if 'control' is not help
if self.draw_app.key != 'control':
shape["selected"] = False
#shape["selected"] = False
self.draw_app.set_unselected(shape)
# TODO: Do this with rtree?
dist = Point(point).distance(shape["geometry"])
dist = Point(point).distance(cascaded_union(shape.geo))
if dist < min_distance:
closest_shape = shape
min_distance = dist
if closest_shape is not None:
closest_shape["selected"] = True
#closest_shape["selected"] = True
self.draw_app.set_selected(closest_shape)
return "Shape selected."
return "Nothing selected."
@ -403,6 +424,9 @@ class FCMove(FCShapeTool):
self.origin = origin
def click(self, point):
if len(self.draw_app.get_selected()) == 0:
return "Nothing to move."
if self.origin is None:
self.set_origin(point)
return "Click on final location."
@ -415,11 +439,16 @@ class FCMove(FCShapeTool):
# Create new geometry
dx = self.destination[0] - self.origin[0]
dy = self.destination[1] - self.origin[1]
self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy))
for geom in self.draw_app.get_selected()]
# Delete old
for geo in self.draw_app.get_selected():
self.draw_app.shape_buffer.remove(geo)
self.draw_app.delete_selected()
# # Select the new
# for g in self.geometry:
# # Note that g is not in the app's buffer yet!
# self.draw_app.set_selected(g)
self.complete = True
@ -433,10 +462,14 @@ class FCMove(FCShapeTool):
if self.origin is None:
return None
if len(self.draw_app.get_selected()) == 0:
return None
dx = data[0] - self.origin[0]
dy = data[1] - self.origin[1]
return [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy)
for geom in self.draw_app.get_selected()])
class FCCopy(FCMove):
@ -444,7 +477,8 @@ class FCCopy(FCMove):
# Create new geometry
dx = self.destination[0] - self.origin[0]
dy = self.destination[1] - self.origin[1]
self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
self.geometry = DrawToolShape([affinity.translate(geom['geometry'], xoff=dx, yoff=dy)
for geom in self.draw_app.get_selected()])
self.complete = True
@ -527,6 +561,7 @@ class FlatCAMDraw(QtCore.QObject):
# Data
self.active_tool = None
self.shape_buffer = []
self.selected = []
self.move_timer = QtCore.QTimer()
self.move_timer.setSingleShot(True)
@ -614,9 +649,10 @@ class FlatCAMDraw(QtCore.QObject):
# Link shapes into editor.
for shape in geometry:
self.shape_buffer.append({'geometry': shape,
'selected': False,
'utility': False})
# self.shape_buffer.append({'geometry': shape,
# # 'selected': False,
# 'utility': False})
self.shape_buffer.append(DrawToolShape(geometry))
self.replot()
self.drawing_toolbar.setDisabled(False)
@ -648,11 +684,11 @@ class FlatCAMDraw(QtCore.QObject):
def on_canvas_click(self, event):
"""
event.x .y have canvas coordinates
event.xdaya .ydata have plot coordinates
event.x and .y have canvas coordinates
event.xdaya and .ydata have plot coordinates
:param event:
:return:
:param event: Event object dispatched by Matplotlib
:return: None
"""
if self.active_tool is not None:
# Dispatch event to active_tool
@ -672,14 +708,14 @@ class FlatCAMDraw(QtCore.QObject):
def on_canvas_move(self, event):
"""
event.x .y have canvas coordinates
event.xdaya .ydata have plot coordinates
event.x and .y have canvas coordinates
event.xdaya and .ydata have plot coordinates
:param event:
:param event: Event object dispatched by Matplotlib
:return:
"""
self.on_canvas_move_effective(event)
return
return None
# self.move_timer.stop()
#
@ -703,11 +739,11 @@ class FlatCAMDraw(QtCore.QObject):
For details on animating on MPL see:
http://wiki.scipy.org/Cookbook/Matplotlib/Animations
event.x .y have canvas coordinates
event.xdaya .ydata have plot coordinates
event.x and .y have canvas coordinates
event.xdaya and .ydata have plot coordinates
:param event:
:return:
:param event: Event object dispatched by Matplotlib
:return: None
"""
try:
@ -719,38 +755,33 @@ class FlatCAMDraw(QtCore.QObject):
if self.active_tool is None:
return
### Snap coordinates
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 and ((type(geo) == list and len(geo) > 0) or
(type(geo) != list and not geo.is_empty)):
if isinstance(geo, DrawToolShape) and geo.geo is not None:
# Remove any previous utility shape
for shape in self.shape_buffer:
if shape['utility']:
if shape.utility:
self.shape_buffer.remove(shape)
# Add the new utility shape
self.shape_buffer.append({
'geometry': geo,
'selected': False,
'utility': True
})
self.shape_buffer.append(geo)
# 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.geo, linespec="b--", animated=True)
for el in elements:
self.axes.draw_artist(el)
#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)
@ -780,7 +811,7 @@ class FlatCAMDraw(QtCore.QObject):
# TODO: ...?
self.on_tool_select("select")
self.app.info("Cancelled.")
for_deletion = [shape for shape in self.shape_buffer if shape['utility']]
for_deletion = [shape for shape in self.shape_buffer if shape.utility]
for shape in for_deletion:
self.shape_buffer.remove(shape)
@ -821,18 +852,39 @@ class FlatCAMDraw(QtCore.QObject):
self.key = None
def get_selected(self):
return [shape for shape in self.shape_buffer if shape["selected"]]
"""
Returns list of shapes that are selected in the editor.
:return: List of shapes.
"""
#return [shape for shape in self.shape_buffer if shape["selected"]]
return self.selected
def delete_selected(self):
for shape in self.get_selected():
# for shape in self.get_selected():
# self.shape_buffer.remove(shape)
# self.app.info("Shape deleted.")
for shape in self.selected:
self.shape_buffer.remove(shape)
self.app.info("Shape deleted.")
self.selected = []
def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
"""
Plots a geometric object or list of objects without rendeting. Plotted objects
are returned as a list. This allows for efficient/animated rendering.
:param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
:param linespec: Matplotlib linespec string.
:param linewidth: Width of lines in # of pixels.
:param animated: If geometry is to be animated. (See MPL plot())
:return: List of plotted elements.
"""
plot_elements = []
if geometry is None:
geometry = self.active_tool.geometry
try:
_ = iter(geometry)
iterable_geometry = geometry
@ -881,22 +933,29 @@ class FlatCAMDraw(QtCore.QObject):
self.app.log.debug("plot_all()")
self.axes.cla()
for shape in self.shape_buffer:
if shape['geometry'] is None: # TODO: This shouldn't have happened
if shape.geo is None: # TODO: This shouldn't have happened
continue
if shape['utility']:
self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1)
if shape.utility:
self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
continue
if shape['selected']:
self.plot_shape(geometry=shape['geometry'], linespec='k-', linewidth=2)
if shape in self.selected:
self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2)
continue
self.plot_shape(geometry=shape['geometry'])
self.plot_shape(geometry=shape.geo)
self.canvas.auto_adjust_axes()
def add2index(self, id, geo):
"""
:param id: Index of data in list being indexed.
:param geo: Some Shapely.geom kind
:return: None
"""
try:
for pt in geo.coords:
self.rtree_index.add(id, pt)
@ -914,31 +973,60 @@ class FlatCAMDraw(QtCore.QObject):
#self.plot_shape()
#self.canvas.auto_adjust_axes()
try:
for geo in self.active_tool.geometry:
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)
self.add_shape(self.active_tool.geometry)
# Remove any utility shapes
for shape in self.shape_buffer:
if shape['utility']:
if shape.utility:
self.shape_buffer.remove(shape)
self.replot()
self.active_tool = type(self.active_tool)(self)
def add_shape(self, shape):
"""
Adds a shape to the shape buffer and the rtree index.
:param shape: Shape to be added.
:type shape: DrawToolShape
:return: None
"""
print "add_shape()"
# List ?
if isinstance(shape, list):
for subshape in shape:
self.add_shape(subshape)
return
assert isinstance(shape, DrawToolShape)
assert shape.geo is not None
assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list)
try:
for geo in shape.geo:
self.add2index(len(self.shape_buffer), geo)
self.shape_buffer.append(shape)
except TypeError:
self.shape_buffer.append(shape)
self.add2index(len(self.shape_buffer) - 1, shape.geo)
def replot(self):
#self.canvas.clear()
self.axes = self.canvas.new_axes("draw")
self.plot_all()
def set_selected(self, shape):
# Remove and add to the end.
if shape in self.selected:
self.selected.remove(shape)
self.selected.append(shape)
def set_unselected(self, shape):
if shape in self.selected:
self.selected.remove(shape)
def snap(self, x, y):
"""
Adjusts coordinates to snap settings.
@ -967,12 +1055,12 @@ class FlatCAMDraw(QtCore.QObject):
### 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']
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']
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_))
@ -991,7 +1079,7 @@ class FlatCAMDraw(QtCore.QObject):
"""
fcgeometry.solid_geometry = []
for shape in self.shape_buffer:
fcgeometry.solid_geometry.append(shape['geometry'])
fcgeometry.solid_geometry.append(shape.geo)
def union(self):
"""
@ -1000,27 +1088,24 @@ class FlatCAMDraw(QtCore.QObject):
:return: None.
"""
targets = [shape for shape in self.shape_buffer if shape['selected']]
#targets = [shape for shape in self.selected]
results = cascaded_union([t['geometry'] for t in targets])
results = cascaded_union([t.geo for t in self.get_selected()])
for shape in targets:
# Delete originals.
for shape in self.get_selected():
self.shape_buffer.remove(shape)
try:
for geo in results:
# Selected geometry is now gone!
self.selected = []
self.shape_buffer.append({
'geometry': geo,
'selected': True,
'utility': False
})
except TypeError:
self.shape_buffer.append({
'geometry': results,
'selected': True,
'utility': False
})
# try:
# for geo in results:
# self.shape_buffer.append(DrawToolShape(geo))
# except TypeError:
# self.shape_buffer.append(DrawToolShape(geo))
self.add_shape(DrawToolShape(results))
self.replot()

45
doc/source/planning.rst Normal file
View File

@ -0,0 +1,45 @@
Development Planning
====================
Drawing
-------
* [DONE] Arcs
* Subtract Shapes
* Selected objects must be kept onlist to preserve order.
* Polygon to outline
* Force perpendicular
Algorithms
----------
* Reverse path if end is nearer.
* Seed paint: Specify seed.
Features
--------
* Z profile
* UNDO
G-Code
------
* More output options: Formatting.
* Don't lift the tool if unnecessary.
Excellon
--------
* Parse tool definitions in body
Bugs
----
* Unit conversion on opening.
* `cascaded_union([])` bug requires more testing.

View File

@ -59,6 +59,7 @@ Creating Shapes
The shape creation tools in the editor are:
* Circle
* Arc
* Rectangle
* Polygon
* Path
@ -72,6 +73,9 @@ on the status bar.
Shapes that do not require a fixed number of clicks to complete, like
polygons and paths, are complete by hitting the ``Space`` key.
Certain shape tools can have different options or modes. By hitting
`o` and/or `p` the tool will cycle through its options and/or modes.
.. seealso::
The FlatCAM Shell commands :ref:`add_circle`, :ref:`add_poly` and :ref:`add_rect`,