Merged jpcgt/flatcam into master

This commit is contained in:
Kamil Sopko
2015-01-01 22:12:03 +01:00
9 changed files with 401 additions and 257 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.pyc

View File

@@ -21,12 +21,52 @@ from rtree import index as rtindex
class DrawToolShape(object): class DrawToolShape(object):
@staticmethod
def get_pts(o):
"""
Returns a list of all points in the object, where
the object can be a Polygon, Not a polygon, or a list
of such. Search is done recursively.
:param: geometric object
:return: List of points
:rtype: list
"""
pts = []
## Iterable: descend into each item.
try:
for subo in o:
pts += DrawToolShape.get_pts(subo)
## Non-iterable
except TypeError:
## DrawToolShape: descend into .geo.
if isinstance(o, DrawToolShape):
pts += DrawToolShape.get_pts(o.geo)
## Descend into .exerior and .interiors
elif type(o) == Polygon:
pts += DrawToolShape.get_pts(o.exterior)
for i in o.interiors:
pts += DrawToolShape.get_pts(i)
## Has .coords: list them.
else:
pts += list(o.coords)
return pts
def __init__(self, geo=[]): def __init__(self, geo=[]):
# Shapely type or list of such # Shapely type or list of such
self.geo = geo self.geo = geo
self.utility = False self.utility = False
def get_all_points(self):
return DrawToolShape.get_pts(self)
class DrawToolUtilityShape(DrawToolShape): class DrawToolUtilityShape(DrawToolShape):
@@ -383,39 +423,27 @@ class FCPath(FCPolygon):
class FCSelect(DrawTool): class FCSelect(DrawTool):
def __init__(self, draw_app): def __init__(self, draw_app):
DrawTool.__init__(self, draw_app) DrawTool.__init__(self, draw_app)
self.shape_buffer = self.draw_app.shape_buffer self.storage = self.draw_app.storage
#self.shape_buffer = self.draw_app.shape_buffer
self.selected = self.draw_app.selected self.selected = self.draw_app.selected
self.start_msg = "Click on geometry to select" self.start_msg = "Click on geometry to select"
def click(self, point): def click(self, point):
min_distance = Inf _, closest_shape = self.storage.nearest(point)
closest_shape = None
for shape in self.shape_buffer: if self.draw_app.key != 'control':
self.draw_app.selected = []
# Remove all if 'control' is not help self.draw_app.set_selected(closest_shape)
if self.draw_app.key != 'control': self.draw_app.app.log.debug("Selected shape containing: " + str(closest_shape.geo))
#shape["selected"] = False
self.draw_app.set_unselected(shape)
# TODO: Do this with rtree? return ""
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
self.draw_app.set_selected(closest_shape)
return "Shape selected."
return "Nothing selected."
class FCMove(FCShapeTool): class FCMove(FCShapeTool):
def __init__(self, draw_app): def __init__(self, draw_app):
FCShapeTool.__init__(self, draw_app) FCShapeTool.__init__(self, draw_app)
self.shape_buffer = self.draw_app.shape_buffer #self.shape_buffer = self.draw_app.shape_buffer
self.origin = None self.origin = None
self.destination = None self.destination = None
self.start_msg = "Click on reference point." self.start_msg = "Click on reference point."
@@ -498,7 +526,7 @@ class FlatCAMDraw(QtCore.QObject):
self.drawing_toolbar = QtGui.QToolBar() self.drawing_toolbar = QtGui.QToolBar()
self.drawing_toolbar.setDisabled(disabled) self.drawing_toolbar.setDisabled(disabled)
self.app.ui.addToolBar(self.drawing_toolbar) self.app.ui.addToolBar(self.drawing_toolbar)
self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), 'Select') self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'")
self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle') self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
self.add_arc_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc') self.add_arc_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc')
self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle') self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
@@ -507,8 +535,9 @@ class FlatCAMDraw(QtCore.QObject):
self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union') self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union')
self.subtract_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), 'Polygon Subtraction') self.subtract_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), 'Polygon Subtraction')
self.cutpath_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path') self.cutpath_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path')
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 'm'")
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 'c'")
self.delete_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'")
### Snap Toolbar ### ### Snap Toolbar ###
self.snap_toolbar = QtGui.QToolBar() self.snap_toolbar = QtGui.QToolBar()
@@ -541,6 +570,7 @@ class FlatCAMDraw(QtCore.QObject):
self.union_btn.triggered.connect(self.union) self.union_btn.triggered.connect(self.union)
self.subtract_btn.triggered.connect(self.subtract) self.subtract_btn.triggered.connect(self.subtract)
self.cutpath_btn.triggered.connect(self.cutpath) self.cutpath_btn.triggered.connect(self.cutpath)
self.delete_btn.triggered.connect(self.on_delete_btn)
## Toolbar events and properties ## Toolbar events and properties
self.tools = { self.tools = {
@@ -565,12 +595,8 @@ class FlatCAMDraw(QtCore.QObject):
### Data ### Data
self.active_tool = None self.active_tool = None
## List of shapes, None for removed ones. List self.storage = FlatCAMDraw.make_storage()
## never decreases size. self.utility = []
self.main_index = []
## List of shapes.
self.shape_buffer = []
## List of selected shapes. ## List of selected shapes.
self.selected = [] self.selected = []
@@ -580,9 +606,9 @@ class FlatCAMDraw(QtCore.QObject):
self.key = None # Currently pressed key self.key = None # Currently pressed key
def make_callback(tool): def make_callback(thetool):
def f(): def f():
self.on_tool_select(tool) self.on_tool_select(thetool)
return f return f
for tool in self.tools: for tool in self.tools:
@@ -624,11 +650,42 @@ class FlatCAMDraw(QtCore.QObject):
def activate(self): def activate(self):
pass pass
def add_shape(self, shape):
"""
Adds a shape to the shape storage.
:param shape: Shape to be added.
:type shape: DrawToolShape
:return: None
"""
# List of DrawToolShape?
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)
if isinstance(shape, DrawToolUtilityShape):
self.utility.append(shape)
else:
self.storage.insert(shape)
def deactivate(self): def deactivate(self):
self.clear() self.clear()
self.drawing_toolbar.setDisabled(True) self.drawing_toolbar.setDisabled(True)
self.snap_toolbar.setDisabled(True) # TODO: Combine and move into tool self.snap_toolbar.setDisabled(True) # TODO: Combine and move into tool
def delete_utility_geometry(self):
#for_deletion = [shape for shape in self.shape_buffer if shape.utility]
#for_deletion = [shape for shape in self.storage.get_objects() if shape.utility]
for_deletion = [shape for shape in self.utility]
for shape in for_deletion:
self.delete_shape(shape)
def cutpath(self): def cutpath(self):
selected = self.get_selected() selected = self.get_selected()
tools = selected[1:] tools = selected[1:]
@@ -649,11 +706,12 @@ class FlatCAMDraw(QtCore.QObject):
def toolbar_tool_toggle(self, key): def toolbar_tool_toggle(self, key):
self.options[key] = self.sender().isChecked() 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.selected = []
self.storage = FlatCAMDraw.make_storage()
self.replot() self.replot()
def edit_fcgeometry(self, fcgeometry): def edit_fcgeometry(self, fcgeometry):
@@ -664,25 +722,14 @@ class FlatCAMDraw(QtCore.QObject):
:param fcgeometry: FlatCAMGeometry :param fcgeometry: FlatCAMGeometry
:return: None :return: None
""" """
assert isinstance(fcgeometry, Geometry)
if fcgeometry.solid_geometry is None: self.clear()
geometry = []
else:
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. # Link shapes into editor.
for shape in geometry: for shape in fcgeometry.flatten():
# self.shape_buffer.append({'geometry': shape, if shape is not None: # TODO: Make flatten never create a None
# # 'selected': False, self.add_shape(DrawToolShape(shape))
# 'utility': False})
self.shape_buffer.append(DrawToolShape(geometry))
self.replot() self.replot()
self.drawing_toolbar.setDisabled(False) self.drawing_toolbar.setDisabled(False)
@@ -795,9 +842,7 @@ class FlatCAMDraw(QtCore.QObject):
if isinstance(geo, DrawToolShape) and geo.geo is not None: if isinstance(geo, DrawToolShape) and geo.geo is not None:
# Remove any previous utility shape # Remove any previous utility shape
for shape in self.shape_buffer: self.delete_utility_geometry()
if shape.utility:
self.shape_buffer.remove(shape)
# Add the new utility shape # Add the new utility shape
self.add_shape(geo) self.add_shape(geo)
@@ -805,7 +850,10 @@ class FlatCAMDraw(QtCore.QObject):
# Efficient plotting for fast animation # Efficient plotting for fast animation
#self.canvas.canvas.restore_region(self.canvas.background) #self.canvas.canvas.restore_region(self.canvas.background)
elements = self.plot_shape(geometry=geo.geo, linespec="b--", animated=True) elements = self.plot_shape(geometry=geo.geo,
linespec="b--",
linewidth=1,
animated=True)
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)
@@ -841,9 +889,8 @@ class FlatCAMDraw(QtCore.QObject):
# TODO: ...? # TODO: ...?
self.on_tool_select("select") self.on_tool_select("select")
self.app.info("Cancelled.") self.app.info("Cancelled.")
for_deletion = [shape for shape in self.shape_buffer if shape.utility]
for shape in for_deletion: self.delete_utility_geometry()
self.shape_buffer.remove(shape)
self.replot() self.replot()
self.select_btn.setChecked(True) self.select_btn.setChecked(True)
@@ -881,6 +928,10 @@ class FlatCAMDraw(QtCore.QObject):
def on_canvas_key_release(self, event): def on_canvas_key_release(self, event):
self.key = None self.key = None
def on_delete_btn(self):
self.delete_selected()
self.replot()
def get_selected(self): def get_selected(self):
""" """
Returns list of shapes that are selected in the editor. Returns list of shapes that are selected in the editor.
@@ -917,93 +968,94 @@ class FlatCAMDraw(QtCore.QObject):
if geometry is None: if geometry is None:
geometry = self.active_tool.geometry geometry = self.active_tool.geometry
# try:
# _ = iter(geometry)
# iterable_geometry = geometry
# except TypeError:
# iterable_geometry = [geometry]
## Iterable: Descend into each element.
try: try:
_ = iter(geometry) for geo in geometry:
iterable_geometry = geometry plot_elements += self.plot_shape(geometry=geo,
linespec=linespec,
linewidth=linewidth,
animated=animated)
## Non-iterable
except TypeError: except TypeError:
iterable_geometry = [geometry]
for geo in iterable_geometry: ## DrawToolShape
if isinstance(geometry, DrawToolShape):
plot_elements += self.plot_shape(geometry=geometry.geo,
linespec=linespec,
linewidth=linewidth,
animated=animated)
if type(geo) == Polygon: ## Polygon: Dscend into exterior and each interior.
x, y = geo.exterior.coords.xy if type(geometry) == Polygon:
plot_elements += self.plot_shape(geometry=geometry.exterior,
linespec=linespec,
linewidth=linewidth,
animated=animated)
plot_elements += self.plot_shape(geometry=geometry.interiors,
linespec=linespec,
linewidth=linewidth,
animated=animated)
# x, y = geo.exterior.coords.xy
# element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
# plot_elements.append(element)
# for ints in geo.interiors:
# x, y = ints.coords.xy
# element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
# plot_elements.append(element)
# continue
if type(geometry) == LineString or type(geometry) == LinearRing:
x, y = geometry.coords.xy
element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated) element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
plot_elements.append(element) plot_elements.append(element)
for ints in geo.interiors: # continue
x, y = ints.coords.xy
element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
plot_elements.append(element)
continue
if type(geo) == LineString or type(geo) == LinearRing: # if type(geo) == MultiPolygon:
x, y = geo.coords.xy # for poly in geo:
element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated) # x, y = poly.exterior.coords.xy
plot_elements.append(element) # element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
continue # plot_elements.append(element)
# for ints in poly.interiors:
# x, y = ints.coords.xy
# element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
# plot_elements.append(element)
# continue
if type(geo) == MultiPolygon: if type(geometry) == Point:
for poly in geo: x, y = geometry.coords.xy
x, y = poly.exterior.coords.xy
element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
plot_elements.append(element)
for ints in poly.interiors:
x, y = ints.coords.xy
element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
plot_elements.append(element)
continue
if type(geo) == Point:
x, y = geo.coords.xy
element, = self.axes.plot(x, y, 'bo', linewidth=linewidth, animated=animated) element, = self.axes.plot(x, y, 'bo', linewidth=linewidth, animated=animated)
plot_elements.append(element) plot_elements.append(element)
continue # continue
return plot_elements return plot_elements
# self.canvas.auto_adjust_axes() # self.canvas.auto_adjust_axes()
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
"""
# List of DrawToolShape?
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)
self.shape_buffer.append(shape)
# Do not add utility shapes to the index.
if not isinstance(shape, DrawToolUtilityShape):
self.main_index.append(shape)
self.add2index(len(self.main_index) - 1, shape)
def plot_all(self): def plot_all(self):
self.app.log.debug("plot_all()") self.app.log.debug("plot_all()")
self.axes.cla() self.axes.cla()
for shape in self.shape_buffer: #for shape in self.shape_buffer:
for shape in self.storage.get_objects():
if shape.geo is None: # TODO: This shouldn't have happened if shape.geo is None: # TODO: This shouldn't have happened
continue continue
if shape.utility:
self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
continue
if shape in self.selected: if shape in self.selected:
self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2) self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2)
continue continue
self.plot_shape(geometry=shape.geo) self.plot_shape(geometry=shape.geo)
for shape in self.utility:
self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
continue
self.canvas.auto_adjust_axes() self.canvas.auto_adjust_axes()
def add2index(self, id, geo): def add2index(self, id, geo):
@@ -1076,26 +1128,29 @@ class FlatCAMDraw(QtCore.QObject):
self.add_shape(self.active_tool.geometry) self.add_shape(self.active_tool.geometry)
# Remove any utility shapes # Remove any utility shapes
for shape in self.shape_buffer: self.delete_utility_geometry()
if shape.utility:
self.shape_buffer.remove(shape)
self.replot() self.replot()
self.active_tool = type(self.active_tool)(self) self.active_tool = type(self.active_tool)(self)
def delete_shape(self, shape): def delete_shape(self, shape):
try: # try:
# Remove from index list # # Remove from index list
shp_idx = self.main_index.index(shape) # shp_idx = self.main_index.index(shape)
self.main_index[shp_idx] = None # self.main_index[shp_idx] = None
#
# # Remove from rtree index
# self.remove_from_index(shp_idx, shape)
# except ValueError:
# pass
#
# if shape in self.shape_buffer:
# self.shape_buffer.remove(shape)
if shape in self.utility:
self.utility.remove(shape)
return
# Remove from rtree index self.storage.remove(shape)
self.remove_from_index(shp_idx, shape)
except ValueError:
pass
if shape in self.shape_buffer:
self.shape_buffer.remove(shape)
if shape in self.selected: if shape in self.selected:
self.selected.remove(shape) self.selected.remove(shape)
@@ -1105,6 +1160,15 @@ class FlatCAMDraw(QtCore.QObject):
self.axes = self.canvas.new_axes("draw") self.axes = self.canvas.new_axes("draw")
self.plot_all() self.plot_all()
@staticmethod
def make_storage():
## Shape storage.
storage = FlatCAMRTreeStorage()
storage.get_points = DrawToolShape.get_pts
return storage
def set_selected(self, shape): def set_selected(self, shape):
# Remove and add to the end. # Remove and add to the end.
@@ -1134,14 +1198,13 @@ class FlatCAMDraw(QtCore.QObject):
### in the index. ### in the index.
if self.options["corner_snap"]: if self.options["corner_snap"]:
try: try:
bbox = self.rtree_index.nearest((x, y), objects=True).next().bbox nearest_pt, shape = self.storage.nearest((x, y))
nearest_pt = (bbox[0], bbox[1])
nearest_pt_distance = distance((x, y), nearest_pt) nearest_pt_distance = distance((x, y), nearest_pt)
if nearest_pt_distance <= self.options["snap_max"]: if nearest_pt_distance <= self.options["snap_max"]:
snap_distance = nearest_pt_distance snap_distance = nearest_pt_distance
snap_x, snap_y = nearest_pt snap_x, snap_y = nearest_pt
except StopIteration: except (StopIteration, AssertionError):
pass pass
### Grid snap ### Grid snap
@@ -1170,7 +1233,8 @@ class FlatCAMDraw(QtCore.QObject):
:return: None :return: None
""" """
fcgeometry.solid_geometry = [] fcgeometry.solid_geometry = []
for shape in self.shape_buffer: #for shape in self.shape_buffer:
for shape in self.storage.get_objects():
fcgeometry.solid_geometry.append(shape.geo) fcgeometry.solid_geometry.append(shape.geo)
def union(self): def union(self):
@@ -1184,8 +1248,9 @@ class FlatCAMDraw(QtCore.QObject):
results = cascaded_union([t.geo for t in self.get_selected()]) results = cascaded_union([t.geo for t in self.get_selected()])
# Delete originals. # Delete originals.
for shape in self.get_selected(): for_deletion = [s for s in self.get_selected()]
self.shape_buffer.remove(shape) for shape in for_deletion:
self.delete_shape(shape)
# Selected geometry is now gone! # Selected geometry is now gone!
self.selected = [] self.selected = []

View File

@@ -5,58 +5,4 @@ FlatCAM: 2D Post-processing for Manufacturing
FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router. FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
Among other things, it can take a Gerber file generated by your favorite PCB Among other things, it can take a Gerber file generated by your favorite PCB
CAD program, and create G-Code for Isolation routing. But there's more. CAD program, and create G-Code for Isolation routing.
This fork is mainly for improving shell commands.
added so far:
* cutout
* mirror
* cncdrilljob
todo:
* commandline witch reads whole shell sequence from given file
example of shell flow:
```
#!flatcam shell
new
open_gerber /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-Margin.gbr -outname Margin
open_gerber /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-B_Cu.gbr -outname BottomCu
open_excellon /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2.drl -outname Drills
mirror BottomCu -box Margin -axis X
mirror Drills -box Margin -axis X
cutout Margin -dia 3 -margin 0 -gapsize 0.6 -gaps lr
isolate BottomCu -dia 0.4 -overlap 1
drillcncjob Drills -tools 1 -drillz -2 -travelz 2 -feedrate 5 -outname Drills_cncjob_0.8
drillcncjob Drills -tools 2 -drillz -2 -travelz 2 -feedrate 5 -outname Drills_cncjob_3.0
cncjob BottomCu_iso -tooldia 0.4
cncjob Margin_cutout -tooldia 3
write_gcode BottomCu_iso_cnc /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-B_Cu.gbr_iso_cnc.ngc
write_gcode Margin_cutout_cnc /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2-Margin.gbr_cutout_cnc.ngc
write_gcode Drills_cncjob_3.0 /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2.drl_Drills_cncjob_3.0.ngc
write_gcode Drills_cncjob_0.8 /home/sopak/kicad/ThermalShield/Gerber/ThermalPicoShield2.drl_Drills_cncjob_0.8.ngc
```

208
camlib.py
View File

@@ -138,16 +138,7 @@ class Geometry(object):
else: else:
return self.solid_geometry.bounds return self.solid_geometry.bounds
def flatten_to_paths(self, geometry=None, reset=True): def flatten(self, geometry=None, reset=True, pathonly=False):
"""
Creates a list of non-iterable linear geometry elements and
indexes them in rtree.
:param geometry: Iterable geometry
:param reset: Wether to clear (True) or append (False) to self.flat_geometry
:return: self.flat_geometry, self.flat_geometry_rtree
"""
if geometry is None: if geometry is None:
geometry = self.solid_geometry geometry = self.solid_geometry
@@ -157,30 +148,88 @@ class Geometry(object):
## If iterable, expand recursively. ## If iterable, expand recursively.
try: try:
for geo in geometry: for geo in geometry:
self.flatten_to_paths(geometry=geo, reset=False) self.flatten(geometry=geo,
reset=False,
pathonly=pathonly)
## Not iterable, do the actual indexing and add. ## Not iterable, do the actual indexing and add.
except TypeError: except TypeError:
if type(geometry) == Polygon: if pathonly and type(geometry) == Polygon:
g = geometry.exterior self.flat_geometry.append(geometry.exterior)
self.flat_geometry.append(g) self.flatten(geometry=geometry.interiors,
reset=False,
## Add first and last points of the path to the index. pathonly=True)
self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
for interior in geometry.interiors:
g = interior
self.flat_geometry.append(g)
self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
else: else:
g = geometry self.flat_geometry.append(geometry)
self.flat_geometry.append(g) # if type(geometry) == Polygon:
self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0]) # self.flat_geometry.append(geometry)
self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
return self.flat_geometry, self.flat_geometry_rtree return self.flat_geometry
def make2Dindex(self):
self.flatten()
def get_pts(o):
pts = []
if type(o) == Polygon:
g = o.exterior
pts += list(g.coords)
for i in o.interiors:
pts += list(i.coords)
else:
pts += list(o.coords)
return pts
idx = FlatCAMRTreeStorage()
idx.get_points = get_pts
for shape in self.flat_geometry:
idx.insert(shape)
return idx
# def flatten_to_paths(self, geometry=None, reset=True):
# """
# Creates a list of non-iterable linear geometry elements and
# indexes them in rtree.
#
# :param geometry: Iterable geometry
# :param reset: Wether to clear (True) or append (False) to self.flat_geometry
# :return: self.flat_geometry, self.flat_geometry_rtree
# """
#
# if geometry is None:
# geometry = self.solid_geometry
#
# if reset:
# self.flat_geometry = []
#
# ## If iterable, expand recursively.
# try:
# for geo in geometry:
# self.flatten_to_paths(geometry=geo, reset=False)
#
# ## Not iterable, do the actual indexing and add.
# except TypeError:
# if type(geometry) == Polygon:
# g = geometry.exterior
# self.flat_geometry.append(g)
#
# ## Add first and last points of the path to the index.
# self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
# self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
#
# for interior in geometry.interiors:
# g = interior
# self.flat_geometry.append(g)
# self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
# self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
# else:
# g = geometry
# self.flat_geometry.append(g)
# self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
# self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
#
# return self.flat_geometry, self.flat_geometry_rtree
def isolation_geometry(self, offset): def isolation_geometry(self, offset):
""" """
@@ -2282,10 +2331,21 @@ class CNCjob(Geometry):
""" """
assert isinstance(geometry, Geometry) assert isinstance(geometry, Geometry)
## Flatten the geometry and get rtree index ## Flatten the geometry
flat_geometry, rti = geometry.flatten_to_paths() flat_geometry = geometry.flatten(pathonly=True)
log.debug("%d paths" % len(flat_geometry)) log.debug("%d paths" % len(flat_geometry))
## Index first and last points in paths
def get_pts(o):
return [o.coords[0], o.coords[-1]]
storage = FlatCAMRTreeStorage()
storage.get_points = get_pts
for shape in flat_geometry:
if shape is not None: # TODO: This shouldn't have happened.
storage.insert(shape)
if tooldia is not None: if tooldia is not None:
self.tooldia = tooldia self.tooldia = tooldia
@@ -2306,37 +2366,39 @@ class CNCjob(Geometry):
## Iterate over geometry paths getting the nearest each time. ## Iterate over geometry paths getting the nearest each time.
path_count = 0 path_count = 0
current_pt = (0, 0) current_pt = (0, 0)
hits = list(rti.nearest(current_pt, 1)) pt, geo = storage.nearest(current_pt)
while len(hits) > 0: try:
path_count += 1 while True:
print "Current: ", "(%.3f, %.3f)" % current_pt path_count += 1
geo = flat_geometry[hits[0]] #print "Current: ", "(%.3f, %.3f)" % current_pt
# Determine which end of the path is closest. # Remove before modifying, otherwise
distance2start = distance(current_pt, geo.coords[0]) # deletion will fail.
distance2stop = distance(current_pt, geo.coords[-1]) storage.remove(geo)
print " Path index =", hits[0]
print " Start: ", "(%.3f, %.3f)" % geo.coords[0], " D(Start): %.3f" % distance2start
print " Stop : ", "(%.3f, %.3f)" % geo.coords[-1], " D(Stop): %.3f" % distance2stop
# Reverse if end is closest. if list(pt) == list(geo.coords[-1]):
if distance2start > distance2stop: #print "Reversing"
print " Reversing!" geo.coords = list(geo.coords)[::-1]
geo.coords = list(geo.coords)[::-1]
# G-code # G-code
if type(geo) == LineString or type(geo) == LinearRing: if type(geo) == LineString or type(geo) == LinearRing:
self.gcode += self.linear2gcode(geo, tolerance=tolerance) self.gcode += self.linear2gcode(geo, tolerance=tolerance)
elif type(geo) == Point: elif type(geo) == Point:
self.gcode += self.point2gcode(geo) self.gcode += self.point2gcode(geo)
else: else:
log.warning("G-code generation not implemented for %s" % (str(type(geo)))) log.warning("G-code generation not implemented for %s" % (str(type(geo))))
# Delete from index, update current location and continue. # Delete from index, update current location and continue.
rti.delete(hits[0], geo.coords[0]) #rti.delete(hits[0], geo.coords[0])
rti.delete(hits[0], geo.coords[-1]) #rti.delete(hits[0], geo.coords[-1])
current_pt = geo.coords[-1]
hits = list(rti.nearest(current_pt, 1)) current_pt = geo.coords[-1]
# Next
pt, geo = storage.nearest(current_pt)
except StopIteration: # Nothing found in storage.
pass
log.debug("%s paths traced." % path_count) log.debug("%s paths traced." % path_count)
@@ -3188,11 +3250,21 @@ def distance(pt1, pt2):
class FlatCAMRTree(object): class FlatCAMRTree(object):
def __init__(self): def __init__(self):
# Python RTree Index
self.rti = rtindex.Index() self.rti = rtindex.Index()
## Track object-point relationship
# Each is list of points in object.
self.obj2points = [] self.obj2points = []
# Index is index in rtree, value is index of
# object in obj2points.
self.points2obj = [] self.points2obj = []
self.get_points = lambda go: go.coords
def grow_obj2points(self, idx): def grow_obj2points(self, idx):
if len(self.obj2points) > idx: if len(self.obj2points) > idx:
# len == 2, idx == 1, ok. # len == 2, idx == 1, ok.
@@ -3207,15 +3279,14 @@ class FlatCAMRTree(object):
self.grow_obj2points(objid) self.grow_obj2points(objid)
self.obj2points[objid] = [] self.obj2points[objid] = []
for pt in obj.coords: for pt in self.get_points(obj):
self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid) self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid)
self.obj2points[objid].append(len(self.points2obj)) self.obj2points[objid].append(len(self.points2obj))
self.points2obj.append(objid) self.points2obj.append(objid)
def remove_obj(self, objid, obj): def remove_obj(self, objid, obj):
# Use all ptids to delete from index # Use all ptids to delete from index
for i in range(len(self.obj2points[objid])): for i, pt in enumerate(self.get_points(obj)):
pt = obj.coords[i]
self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1])) self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1]))
def nearest(self, pt): def nearest(self, pt):
@@ -3233,17 +3304,32 @@ class FlatCAMRTreeStorage(FlatCAMRTree):
super(FlatCAMRTreeStorage, self).insert(len(self.objects) - 1, obj) super(FlatCAMRTreeStorage, self).insert(len(self.objects) - 1, obj)
def remove(self, obj): def remove(self, obj):
# Get index in list
objidx = self.objects.index(obj) objidx = self.objects.index(obj)
# Remove from list
self.objects[objidx] = None self.objects[objidx] = None
# Remove from index
self.remove_obj(objidx, obj) self.remove_obj(objidx, obj)
def get_objects(self): def get_objects(self):
return (o for o in self.objects if o is not None) return (o for o in self.objects if o is not None)
def nearest(self, pt): def nearest(self, pt):
"""
Returns the nearest matching points and the object
it belongs to.
:param pt: Query point.
:return: (match_x, match_y), Object owner of
matching point.
:rtype: tuple
"""
tidx = super(FlatCAMRTreeStorage, self).nearest(pt) tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object] return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
class myO: class myO:
def __init__(self, coords): def __init__(self, coords):
self.coords = coords self.coords = coords

View File

@@ -12,15 +12,17 @@ Drawing
* Force perpendicular * Force perpendicular
* Un-group (Union creates group) * Un-group (Union creates group)
* Group (But not union) * Group (But not union)
* Remove from index (rebuild index or make deleted instances * [DONE] Remove from index (rebuild index or make deleted instances
equal to None in the list). equal to None in the list).
* Better handling/abstraction of geometry types and lists of such. * Better handling/abstraction of geometry types and lists of such.
* Plotting and extraction of point is now done in a quite
efficient recursive way.
Algorithms Algorithms
---------- ----------
* Reverse path if end is nearer. * [DONE] Reverse path if end is nearer.
* Seed paint: Specify seed. * Seed paint: Specify seed.
@@ -48,4 +50,11 @@ Bugs
---- ----
* Unit conversion on opening. * Unit conversion on opening.
* `cascaded_union([])` bug requires more testing. * [DONE] `cascaded_union([])` bug requires more testing.
* Old version of GEOS
Other
-----
* Unit testing

BIN
share/deleteshape16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

BIN
share/deleteshape24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

BIN
share/deleteshape32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

37
tests/test_fcrts.py Normal file
View File

@@ -0,0 +1,37 @@
from camlib import *
from shapely.geometry import LineString, LinearRing
s = FlatCAMRTreeStorage()
geoms = [
LinearRing(((0.5699056603773586, 0.7216037735849057),
(0.9885849056603774, 0.7216037735849057),
(0.9885849056603774, 0.6689622641509434),
(0.5699056603773586, 0.6689622641509434),
(0.5699056603773586, 0.7216037735849057))),
LineString(((0.8684952830188680, 0.6952830188679245),
(0.8680655198743615, 0.6865349890935113),
(0.8667803692948564, 0.6778712076279851),
(0.8646522079829676, 0.6693751114229638),
(0.8645044888670096, 0.6689622641509434))),
LineString(((0.9874952830188680, 0.6952830188679245),
(0.9864925023483531, 0.6748709493942936),
(0.9856160316877274, 0.6689622641509434))),
]
for geo in geoms:
s.insert(geo)
current_pt = (0, 0)
pt, geo = s.nearest(current_pt)
while geo is not None:
print pt, geo
print "OBJECTS BEFORE:", s.objects
#geo.coords = list(geo.coords[::-1])
s.remove(geo)
print "OBJECTS AFTER:", s.objects
current_pt = geo.coords[-1]
pt, geo = s.nearest(current_pt)