diff --git a/FlatCAMCommon.py b/FlatCAMCommon.py
index ea431927..6536c8a2 100644
--- a/FlatCAMCommon.py
+++ b/FlatCAMCommon.py
@@ -37,3 +37,4 @@ class LoudDict(dict):
"""
self.callback = callback
+
diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py
index 882144be..5ad10ce4 100644
--- a/FlatCAMDraw.py
+++ b/FlatCAMDraw.py
@@ -383,11 +383,15 @@ class FlatCAMDraw(QtCore.QObject):
:param fcgeometry: FlatCAMGeometry
:return: None
"""
- try:
- _ = iter(fcgeometry.solid_geometry)
- geometry = fcgeometry.solid_geometry
- except TypeError:
- geometry = [fcgeometry.solid_geometry]
+
+ if fcgeometry.solid_geometry is None:
+ geometry = []
+ else:
+ try:
+ _ = iter(fcgeometry.solid_geometry)
+ geometry = fcgeometry.solid_geometry
+ except TypeError:
+ geometry = [fcgeometry.solid_geometry]
# Delete contents of editor.
self.shape_buffer = []
@@ -650,6 +654,9 @@ 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
+ continue
+
if shape['utility']:
self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1)
continue
diff --git a/FlatCAMObj.py b/FlatCAMObj.py
index 3b0ec999..3f88c178 100644
--- a/FlatCAMObj.py
+++ b/FlatCAMObj.py
@@ -567,6 +567,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
"travelz": 0.1,
"feedrate": 5.0,
# "toolselection": ""
+ "tooldia": 0.1
})
# TODO: Document this.
@@ -613,6 +614,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
"travelz": self.ui.travelz_entry,
"feedrate": self.ui.feedrate_entry,
# "toolselection": self.ui.tools_entry
+ "tooldia": self.ui.tooldia_entry
})
assert isinstance(self.ui, ExcellonObjectUI)
@@ -620,13 +622,54 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
# self.ui.choose_tools_button.clicked.connect(self.show_tool_chooser)
self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click)
+ self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
+
+ def get_selected_tools_list(self):
+ """
+ Returns the keys to the self.tools dictionary corresponding
+ to the selections on the tool list in the GUI.
+ """
+ return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
+
+ def on_generate_milling_button_click(self, *args):
+ self.app.report_usage("excellon_on_create_milling_button")
+ self.read_form()
+
+ # Get the tools from the list
+ tools = self.get_selected_tools_list()
+
+ if len(tools) == 0:
+ self.app.inform.emit("Please select one or more tools from the list and try again.")
+ return
+
+ geo_name = self.options["name"] + "_mill"
+
+ def geo_init(geo_obj, app_obj):
+ assert isinstance(geo_obj, FlatCAMGeometry)
+ app_obj.progress.emit(20)
+
+ geo_obj.solid_geometry = []
+
+ for hole in self.drills:
+ if hole['tool'] in tools:
+ geo_obj.solid_geometry.append(
+ Point(hole['point']).buffer(self.tools[hole['tool']]["C"]/2 - self.options["tooldia"]/2).exterior
+ )
+
+ def geo_thread(app_obj):
+ app_obj.new_object("geometry", geo_name, geo_init)
+ app_obj.progress.emit(100)
+
+ # Send to worker
+ # self.app.worker.add_task(job_thread, [self.app])
+ self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
def on_create_cncjob_button_click(self, *args):
self.app.report_usage("excellon_on_create_cncjob_button")
self.read_form()
# Get the tools from the list
- tools = [str(x.text()) for x in self.ui.tools_table.selectedItems()]
+ tools = self.get_selected_tools_list()
if len(tools) == 0:
self.app.inform.emit("Please select one or more tools from the list and try again.")
@@ -890,7 +933,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
"cnctooldia": 0.4 / 25.4,
"painttooldia": 0.0625,
"paintoverlap": 0.15,
- "paintmargin": 0.01
+ "paintmargin": 0.01,
+ "paintmethod": "standard"
})
# Attributes to be included in serialization
@@ -918,7 +962,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
"cnctooldia": self.ui.cnctooldia_entry,
"painttooldia": self.ui.painttooldia_entry,
"paintoverlap": self.ui.paintoverlap_entry,
- "paintmargin": self.ui.paintmargin_entry
+ "paintmargin": self.ui.paintmargin_entry,
+ "paintmethod": self.ui.paintmethod_combo
})
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
@@ -945,13 +990,18 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit)
def paint_poly(self, inside_pt, tooldia, overlap):
+
+ # Which polygon.
poly = find_polygon(self.solid_geometry, inside_pt)
# Initializes the new geometry object
def gen_paintarea(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry)
#assert isinstance(app_obj, App)
- cp = clear_poly(poly.buffer(-self.options["paintmargin"]), tooldia, overlap)
+ #cp = clear_poly(poly.buffer(-self.options["paintmargin"]), tooldia, overlap)
+ cp = self.clear_polygon(poly.buffer(-self.options["paintmargin"]), tooldia, overlap=overlap)
+ if self.options["paintmethod"] == "seed":
+ cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]), tooldia, overlap=overlap)
geo_obj.solid_geometry = cp
geo_obj.options["cnctooldia"] = tooldia
@@ -1067,6 +1117,26 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
return factor
+ def plot_element(self, element):
+ try:
+ for sub_el in element:
+ self.plot_element(sub_el)
+ except TypeError:
+ if type(element) == Polygon:
+ x, y = element.exterior.coords.xy
+ self.axes.plot(x, y, 'r-')
+ for ints in element.interiors:
+ x, y = ints.coords.xy
+ self.axes.plot(x, y, 'r-')
+ return
+
+ if type(element) == LineString or type(element) == LinearRing:
+ x, y = element.coords.xy
+ self.axes.plot(x, y, 'r-')
+ return
+
+ FlatCAMApp.App.log.warning("Did not plot:", str(type(element)))
+
def plot(self):
"""
Plots the object into its axes. If None, of if the axes
@@ -1082,39 +1152,41 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
# Make sure solid_geometry is iterable.
# TODO: This method should not modify the object !!!
- try:
- _ = iter(self.solid_geometry)
- except TypeError:
- if self.solid_geometry is None:
- self.solid_geometry = []
- else:
- self.solid_geometry = [self.solid_geometry]
+ # try:
+ # _ = iter(self.solid_geometry)
+ # except TypeError:
+ # if self.solid_geometry is None:
+ # self.solid_geometry = []
+ # else:
+ # self.solid_geometry = [self.solid_geometry]
+ #
+ # for geo in self.solid_geometry:
+ #
+ # if type(geo) == Polygon:
+ # x, y = geo.exterior.coords.xy
+ # self.axes.plot(x, y, 'r-')
+ # for ints in geo.interiors:
+ # x, y = ints.coords.xy
+ # self.axes.plot(x, y, 'r-')
+ # continue
+ #
+ # if type(geo) == LineString or type(geo) == LinearRing:
+ # x, y = geo.coords.xy
+ # self.axes.plot(x, y, 'r-')
+ # continue
+ #
+ # if type(geo) == MultiPolygon:
+ # for poly in geo:
+ # x, y = poly.exterior.coords.xy
+ # self.axes.plot(x, y, 'r-')
+ # for ints in poly.interiors:
+ # x, y = ints.coords.xy
+ # self.axes.plot(x, y, 'r-')
+ # continue
+ #
+ # FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
- for geo in self.solid_geometry:
-
- if type(geo) == Polygon:
- x, y = geo.exterior.coords.xy
- self.axes.plot(x, y, 'r-')
- for ints in geo.interiors:
- x, y = ints.coords.xy
- self.axes.plot(x, y, 'r-')
- continue
-
- if type(geo) == LineString or type(geo) == LinearRing:
- x, y = geo.coords.xy
- self.axes.plot(x, y, 'r-')
- continue
-
- if type(geo) == MultiPolygon:
- for poly in geo:
- x, y = poly.exterior.coords.xy
- self.axes.plot(x, y, 'r-')
- for ints in poly.interiors:
- x, y = ints.coords.xy
- self.axes.plot(x, y, 'r-')
- continue
-
- FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
+ self.plot_element(self.solid_geometry)
self.app.plotcanvas.auto_adjust_axes()
# GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
diff --git a/ObjectUI.py b/ObjectUI.py
index 40866863..23a61b46 100644
--- a/ObjectUI.py
+++ b/ObjectUI.py
@@ -245,7 +245,9 @@ class GeometryObjectUI(ObjectUI):
)
self.custom_box.addWidget(self.generate_cnc_button)
- ## Paint area
+ ################
+ ## Paint area ##
+ ################
self.paint_label = QtGui.QLabel('Paint Area:')
self.paint_label.setToolTip(
"Creates tool paths to cover the\n"
@@ -288,7 +290,19 @@ class GeometryObjectUI(ObjectUI):
)
grid2.addWidget(marginlabel, 2, 0)
self.paintmargin_entry = LengthEntry()
- grid2.addWidget(self.paintmargin_entry)
+ grid2.addWidget(self.paintmargin_entry, 2, 1)
+
+ # Method
+ methodlabel = QtGui.QLabel('Method:')
+ methodlabel.setToolTip(
+ "Algorithm to paint the polygon."
+ )
+ grid2.addWidget(methodlabel, 3, 0)
+ self.paintmethod_combo = RadioSet([
+ {"label": "Standard", "value": "standard"},
+ {"label": "Seed-based", "value": "seed"}
+ ])
+ grid2.addWidget(self.paintmethod_combo, 3, 1)
# GO Button
self.generate_paint_button = QtGui.QPushButton('Generate')
@@ -386,6 +400,35 @@ class ExcellonObjectUI(ObjectUI):
)
self.custom_box.addWidget(self.generate_cnc_button)
+ ## Milling Holes
+ self.mill_hole_label = QtGui.QLabel('Mill Holes')
+ self.mill_hole_label.setToolTip(
+ "Create Geometry for milling holes."
+ )
+ self.custom_box.addWidget(self.mill_hole_label)
+
+ grid1 = QtGui.QGridLayout()
+ self.custom_box.addLayout(grid1)
+ tdlabel = QtGui.QLabel('Tool dia:')
+ tdlabel.setToolTip(
+ "Diameter of the cutting tool."
+ )
+ grid1.addWidget(tdlabel, 0, 0)
+ self.tooldia_entry = LengthEntry()
+ grid1.addWidget(self.tooldia_entry, 0, 1)
+
+ choose_tools_label2 = QtGui.QLabel(
+ "Select from the tools section above\n"
+ "the tools you want to include."
+ )
+ self.custom_box.addWidget(choose_tools_label2)
+
+ self.generate_milling_button = QtGui.QPushButton('Generate Geometry')
+ self.generate_milling_button.setToolTip(
+ "Create the Geometry Object\n"
+ "for milling toolpaths."
+ )
+ self.custom_box.addWidget(self.generate_milling_button)
class GerberObjectUI(ObjectUI):
"""
diff --git a/camlib.py b/camlib.py
index 31f57696..f329d11a 100644
--- a/camlib.py
+++ b/camlib.py
@@ -216,6 +216,14 @@ class Geometry(object):
"""
Creates geometry inside a polygon for a tool to cover
the whole area.
+
+ This algorithm shrinks the edges of the polygon and takes
+ the resulting edges as toolpaths.
+
+ :param polygon: Polygon to clear.
+ :param tooldia: Diameter of the tool.
+ :param overlap: Overlap of toolpasses.
+ :return:
"""
poly_cuts = [polygon.buffer(-tooldia/2.0)]
while True:
@@ -226,6 +234,58 @@ class Geometry(object):
break
return poly_cuts
+ def clear_polygon2(self, polygon, tooldia, seedpoint=None, overlap=0.15):
+ """
+ Creates geometry inside a polygon for a tool to cover
+ the whole area.
+
+ This algorithm starts with a seed point inside the polygon
+ and draws circles around it. Arcs inside the polygons are
+ valid cuts. Finalizes by cutting around the inside edge of
+ the polygon.
+
+ :param polygon:
+ :param tooldia:
+ :param seedpoint:
+ :param overlap:
+ :return:
+ """
+
+ if seedpoint is None:
+ seedpoint = polygon.representative_point()
+
+ # Current buffer radius
+ radius = tooldia/2*(1-overlap)
+
+ # The toolpaths
+ geoms = [Point(seedpoint).buffer(radius).exterior]
+
+ # Path margin
+ path_margin = polygon.buffer(-tooldia/2)
+
+ # Grow from seed until outside the box.
+ while 1:
+ path = Point(seedpoint).buffer(radius).exterior
+ path = path.intersection(path_margin)
+
+ # Touches polygon?
+ if path.is_empty:
+ break
+ else:
+ geoms.append(path)
+
+ radius += tooldia*(1-overlap)
+
+ # Clean edges
+ outer_edges = [x.exterior for x in autolist(polygon.buffer(-tooldia/2))]
+ inner_edges = []
+ for x in autolist(polygon.buffer(-tooldia/2)): # Over resulting polygons
+ for y in x.interiors: # Over interiors of each polygon
+ inner_edges.append(y)
+ geoms += outer_edges + inner_edges
+
+ return geoms
+
def scale(self, factor):
"""
Scales all of the object's geometry by a given factor. Override
@@ -2695,30 +2755,30 @@ def arc_angle(start, stop, direction):
return angle
-def clear_poly(poly, tooldia, overlap=0.1):
- """
- Creates a list of Shapely geometry objects covering the inside
- of a Shapely.Polygon. Use for removing all the copper in a region
- or bed flattening.
-
- :param poly: Target polygon
- :type poly: Shapely.Polygon
- :param tooldia: Diameter of the tool
- :type tooldia: float
- :param overlap: Fraction of the tool diameter to overlap
- in each pass.
- :type overlap: float
- :return: list of Shapely.Polygon
- :rtype: list
- """
- poly_cuts = [poly.buffer(-tooldia/2.0)]
- while True:
- poly = poly_cuts[-1].buffer(-tooldia*(1-overlap))
- if poly.area > 0:
- poly_cuts.append(poly)
- else:
- break
- return poly_cuts
+# def clear_poly(poly, tooldia, overlap=0.1):
+# """
+# Creates a list of Shapely geometry objects covering the inside
+# of a Shapely.Polygon. Use for removing all the copper in a region
+# or bed flattening.
+#
+# :param poly: Target polygon
+# :type poly: Shapely.Polygon
+# :param tooldia: Diameter of the tool
+# :type tooldia: float
+# :param overlap: Fraction of the tool diameter to overlap
+# in each pass.
+# :type overlap: float
+# :return: list of Shapely.Polygon
+# :rtype: list
+# """
+# poly_cuts = [poly.buffer(-tooldia/2.0)]
+# while True:
+# poly = poly_cuts[-1].buffer(-tooldia*(1-overlap))
+# if poly.area > 0:
+# poly_cuts.append(poly)
+# else:
+# break
+# return poly_cuts
def find_polygon(poly_set, point):
@@ -2775,7 +2835,7 @@ def dict2obj(d):
return d
-def plotg(geo):
+def plotg(geo, solid_poly=False):
try:
_ = iter(geo)
except:
@@ -2783,12 +2843,21 @@ def plotg(geo):
for g in geo:
if type(g) == Polygon:
- x, y = g.exterior.coords.xy
- plot(x, y)
- for ints in g.interiors:
- x, y = ints.coords.xy
+ if solid_poly:
+ patch = PolygonPatch(g,
+ facecolor="#BBF268",
+ edgecolor="#006E20",
+ alpha=0.75,
+ zorder=2)
+ ax = subplot(111)
+ ax.add_patch(patch)
+ else:
+ x, y = g.exterior.coords.xy
plot(x, y)
- continue
+ for ints in g.interiors:
+ x, y = ints.coords.xy
+ plot(x, y)
+ continue
if type(g) == LineString or type(g) == LinearRing:
x, y = g.coords.xy
@@ -3025,3 +3094,10 @@ class Zprofile:
return [{"path": path.intersection(self.polygons[i]),
"z": self.data[i][2]} for i in crossing]
+
+def autolist(obj):
+ try:
+ _ = iter(obj)
+ return obj
+ except TypeError:
+ return [obj]
\ No newline at end of file