- added the Exclusion zones processing to Excellon GCode generation

- fixed a non frequent plotting problem for CNCJob objects made out of Excellon objects
This commit is contained in:
Marius Stanciu 2020-05-21 21:49:48 +03:00 committed by Marius
parent facbdf0fd7
commit 51c9023bbe
4 changed files with 381 additions and 51 deletions

View File

@ -3404,7 +3404,8 @@ class MainGUI(QtWidgets.QMainWindow):
elif modifiers == QtCore.Qt.NoModifier:
if key == QtCore.Qt.Key_Escape or key == 'Escape':
sel_obj = self.app.collection.get_active()
assert sel_obj.kind == 'geometry', "Expected a Geometry Object, got %s" % type(sel_obj)
assert sel_obj.kind == 'geometry' or sel_obj.kind == 'excellon', \
"Expected a Geometry or Excellon Object, got %s" % type(sel_obj)
sel_obj.area_disconnect()
return

View File

@ -7,6 +7,11 @@ CHANGELOG for FlatCAM beta
=================================================
21.05.2020
- added the Exclusion zones processing to Excellon GCode generation
- fixed a non frequent plotting problem for CNCJob objects made out of Excellon objects
19.05.2020
- updated the Italian language (translation incomplete)

265
Common.py
View File

@ -12,11 +12,13 @@
# ##########################################################
from PyQt5 import QtCore
from shapely.geometry import Polygon, MultiPolygon
from shapely.geometry import Polygon, MultiPolygon, Point, LineString
from AppGUI.VisPyVisuals import ShapeCollection
from AppTool import AppTool
from copy import deepcopy
import numpy as np
import gettext
@ -167,8 +169,8 @@ class ExclusionAreas(QtCore.QObject):
{
"obj_type": string ("excellon" or "geometry") <- self.obj_type
"shape": Shapely polygon
"strategy": string ("over" or "around") <- self.strategy
"overz": float <- self.over_z
"strategy": string ("over" or "around") <- self.strategy_button
"overz": float <- self.over_z_button
}
'''
self.exclusion_areas_storage = []
@ -178,9 +180,9 @@ class ExclusionAreas(QtCore.QObject):
self.solid_geometry = []
self.obj_type = None
self.shape_type = 'square' # TODO use the self.app.defaults when made general (not in Geo object Pref UI)
self.over_z = 0.1
self.strategy = None
self.shape_type_button = None
self.over_z_button = None
self.strategy_button = None
self.cnc_button = None
def on_add_area_click(self, shape_button, overz_button, strategy_radio, cnc_button, solid_geo, obj_type):
@ -188,21 +190,25 @@ class ExclusionAreas(QtCore.QObject):
:param shape_button: a FCButton that has the value for the shape
:param overz_button: a FCDoubleSpinner that holds the Over Z value
:param strategy_radio: a RadioSet button with the strategy value
:param strategy_radio: a RadioSet button with the strategy_button value
:param cnc_button: a FCButton in Object UI that when clicked the CNCJob is created
We have a reference here so we can change the color signifying that exclusion areas are
available.
:param solid_geo: reference to the object solid geometry for which we add exclusion areas
:param obj_type: Type of FlatCAM object that called this method
:type obj_type: String: "excellon" or "geometry"
:return:
:param obj_type: Type of FlatCAM object that called this method. String: "excellon" or "geometry"
:type obj_type: str
:return: None
"""
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
self.app.call_source = 'geometry'
self.shape_type = shape_button.get_value()
self.over_z = overz_button.get_value()
self.strategy = strategy_radio.get_value()
self.shape_type_button = shape_button
# TODO use the self.app.defaults when made general (not in Geo object Pref UI)
# self.shape_type_button.set_value('square')
self.over_z_button = overz_button
self.strategy_button = strategy_radio
self.cnc_button = cnc_button
self.solid_geometry = solid_geo
@ -240,11 +246,11 @@ class ExclusionAreas(QtCore.QObject):
x1, y1 = curr_pos[0], curr_pos[1]
# shape_type = self.ui.area_shape_radio.get_value()
# shape_type_button = self.ui.area_shape_radio.get_value()
# do clear area only for left mouse clicks
if event.button == 1:
if self.shape_type == "square":
if self.shape_type_button.get_value() == "square":
if self.first_click is False:
self.first_click = True
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the area."))
@ -268,14 +274,14 @@ class ExclusionAreas(QtCore.QObject):
# {
# "obj_type": string("excellon" or "geometry") < - self.obj_type
# "shape": Shapely polygon
# "strategy": string("over" or "around") < - self.strategy
# "overz": float < - self.over_z
# "strategy_button": string("over" or "around") < - self.strategy_button
# "overz": float < - self.over_z_button
# }
new_el = {
"obj_type": self.obj_type,
"shape": new_rectangle,
"strategy": self.strategy,
"overz": self.over_z
"strategy": self.strategy_button.get_value(),
"overz": self.over_z_button.get_value()
}
self.exclusion_areas_storage.append(new_el)
@ -305,7 +311,7 @@ class ExclusionAreas(QtCore.QObject):
return ""
elif event.button == right_button and self.mouse_is_dragging is False:
shape_type = self.shape_type
shape_type = self.shape_type_button.get_value()
if shape_type == "square":
self.first_click = False
@ -326,17 +332,19 @@ class ExclusionAreas(QtCore.QObject):
pol = Polygon(self.points)
# do not add invalid polygons even if they are drawn by utility geometry
if pol.is_valid:
# {
# "obj_type": string("excellon" or "geometry") < - self.obj_type
# "shape": Shapely polygon
# "strategy": string("over" or "around") < - self.strategy
# "overz": float < - self.over_z
# }
"""
{
"obj_type": string("excellon" or "geometry") < - self.obj_type
"shape": Shapely polygon
"strategy": string("over" or "around") < - self.strategy_button
"overz": float < - self.over_z_button
}
"""
new_el = {
"obj_type": self.obj_type,
"shape": pol,
"strategy": self.strategy,
"overz": self.over_z
"strategy": self.strategy_button.get_value(),
"overz": self.over_z_button.get_value()
}
self.exclusion_areas_storage.append(new_el)
@ -382,9 +390,9 @@ class ExclusionAreas(QtCore.QObject):
if len(self.exclusion_areas_storage) == 0:
return
self.app.inform.emit(
"[success] %s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
# since the exclusion areas should apply to all objects in the app collection, this check is limited to
# only the current object therefore it will not guarantee success
self.app.inform.emit("%s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
for el in self.exclusion_areas_storage:
if el["shape"].intersects(MultiPolygon(self.solid_geometry)):
self.on_clear_area_click()
@ -406,8 +414,6 @@ class ExclusionAreas(QtCore.QObject):
)
self.e_shape_modified.emit()
for k in self.exclusion_areas_storage:
print(k)
def area_disconnect(self):
if self.app.is_legacy is False:
@ -436,7 +442,7 @@ class ExclusionAreas(QtCore.QObject):
# called on mouse move
def on_mouse_move(self, event):
shape_type = self.shape_type
shape_type = self.shape_type_button.get_value()
if self.app.is_legacy is False:
event_pos = event.pos
@ -573,3 +579,194 @@ class ExclusionAreas(QtCore.QObject):
self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
self.app.inform.emit('[success] %s' % _("All exclusion zones deleted."))
def travel_coordinates(self, start_point, end_point, tooldia):
"""
WIll create a path the go around the exclusion areas on the shortest path
:param start_point: X,Y coordinates for the start point of the travel line
:type start_point: tuple
:param end_point: X,Y coordinates for the destination point of the travel line
:type end_point: tuple
:param tooldia: THe tool diameter used and which generates the travel lines
:type tooldia float
:return: A list of x,y tuples that describe the avoiding path
:rtype: list
"""
ret_list = []
# Travel lines: rapids. Should not pass through Exclusion areas
travel_line = LineString([start_point, end_point])
origin_point = Point(start_point)
buffered_storage = []
# add a little something to the half diameter, to make sure that we really don't enter in the exclusion zones
buffered_distance = (tooldia / 2.0) + (0.1 if self.app.defaults['units'] == 'MM' else 0.00393701)
for area in self.exclusion_areas_storage:
new_area = deepcopy(area)
new_area['shape'] = area['shape'].buffer(buffered_distance, join_style=2)
buffered_storage.append(new_area)
# sort the Exclusion areas from the closest to the start_point to the farthest
tmp = []
for area in buffered_storage:
dist = Point(start_point).distance(area['shape'])
tmp.append((dist, area))
tmp.sort(key=lambda k: k[0])
sorted_area_storage = [k[1] for k in tmp]
# process the ordered exclusion areas list
for area in sorted_area_storage:
outline = area['shape'].exterior
if travel_line.intersects(outline):
intersection_pts = travel_line.intersection(outline)
if isinstance(intersection_pts, Point):
# it's just a touch, continue
continue
entry_pt = nearest_point(origin_point, intersection_pts)
exit_pt = farthest_point(origin_point, intersection_pts)
if area['strategy'] == 'around':
full_vertex_points = [Point(x) for x in list(outline.coords)]
# the last coordinate in outline, a LinearRing, is the closing one
# therefore a duplicate of the first one; discard it
vertex_points = full_vertex_points[:-1]
# dist_from_entry = [(entry_pt.distance(vt), vertex_points.index(vt)) for vt in vertex_points]
# closest_point_entry = nsmallest(1, dist_from_entry, key=lambda x: x[0])
# start_idx = closest_point_entry[0][1]
#
# dist_from_exit = [(exit_pt.distance(vt), vertex_points.index(vt)) for vt in vertex_points]
# closest_point_exit = nsmallest(1, dist_from_exit, key=lambda x: x[0])
# end_idx = closest_point_exit[0][1]
pts_line_entry = None
pts_line_exit = None
for i in range(len(full_vertex_points) - 1):
line = LineString(
[
(full_vertex_points[i].x, full_vertex_points[i].y),
(full_vertex_points[i + 1].x, full_vertex_points[i + 1].y)
]
)
if entry_pt.intersects(line) or entry_pt.almost_equals(Point(line.coords[0]), decimal=3) or \
entry_pt.almost_equals(Point(line.coords[1]), decimal=3):
pts_line_entry = [Point(x) for x in line.coords]
if exit_pt.intersects(line) or exit_pt.almost_equals(Point(line.coords[0]), decimal=3) or \
exit_pt.almost_equals(Point(line.coords[1]), decimal=3):
pts_line_exit = [Point(x) for x in line.coords]
closest_point_entry = nearest_point(entry_pt, pts_line_entry)
start_idx = vertex_points.index(closest_point_entry)
closest_point_exit = nearest_point(exit_pt, pts_line_exit)
end_idx = vertex_points.index(closest_point_exit)
# calculate possible paths: one clockwise the other counterclockwise on the exterior of the
# exclusion area outline (Polygon.exterior)
vp_len = len(vertex_points)
if end_idx > start_idx:
path_1 = vertex_points[start_idx:(end_idx + 1)]
path_2 = [vertex_points[start_idx]]
idx = start_idx
for __ in range(vp_len):
idx = idx - 1 if idx > 0 else (vp_len - 1)
path_2.append(vertex_points[idx])
if idx == end_idx:
break
else:
path_1 = vertex_points[end_idx:(start_idx + 1)]
path_2 = [vertex_points[end_idx]]
idx = end_idx
for __ in range(vp_len):
idx = idx - 1 if idx > 0 else (vp_len - 1)
path_2.append(vertex_points[idx])
if idx == start_idx:
break
path_1.reverse()
path_2.reverse()
# choose the one with the lesser length
length_path_1 = 0
for i in range(len(path_1)):
try:
length_path_1 += path_1[i].distance(path_1[i + 1])
except IndexError:
pass
length_path_2 = 0
for i in range(len(path_2)):
try:
length_path_2 += path_2[i].distance(path_2[i + 1])
except IndexError:
pass
path = path_1 if length_path_1 < length_path_2 else path_2
# transform the list of Points into a list of Points coordinates
path_coords = [[None, (p.x, p.y)] for p in path]
ret_list += path_coords
else:
path_coords = [[float(area['overz']), (entry_pt.x, entry_pt.y)], [None, (exit_pt.x, exit_pt.y)]]
ret_list += path_coords
# create a new LineString to test again for possible other Exclusion zones
last_pt_in_path = path_coords[-1][1]
travel_line = LineString([last_pt_in_path, end_point])
ret_list.append([None, end_point])
return ret_list
def farthest_point(origin, points_list):
"""
Calculate the farthest Point in a list from another Point
:param origin: Reference Point
:type origin: Point
:param points_list: List of Points or a MultiPoint
:type points_list: list
:return: Farthest Point
:rtype: Point
"""
old_dist = 0
fartherst_pt = None
for pt in points_list:
dist = abs(origin.distance(pt))
if dist >= old_dist:
fartherst_pt = pt
old_dist = dist
return fartherst_pt
def nearest_point(origin, points_list):
"""
Calculate the nearest Point in a list from another Point
:param origin: Reference Point
:type origin: Point
:param points_list: List of Points or a MultiPoint
:type points_list: list
:return: Nearest Point
:rtype: Point
"""
old_dist = np.Inf
nearest_pt = None
for pt in points_list:
dist = abs(origin.distance(pt))
if dist <= old_dist:
nearest_pt = pt
old_dist = dist
return nearest_pt

159
camlib.py
View File

@ -2630,16 +2630,17 @@ class CNCjob(Geometry):
def generate_from_excellon_by_tool(self, exobj, tools="all", use_ui=False):
"""
Creates gcode for this object from an Excellon object
Creates Gcode for this object from an Excellon object
for the specified tools.
:param exobj: Excellon object to process
:type exobj: Excellon
:param tools: Comma separated tool names
:type: tools: str
:param use_ui: Bool, if True the method will use parameters set in UI
:return: None
:rtype: None
:param exobj: Excellon object to process
:type exobj: Excellon
:param tools: Comma separated tool names
:type tools: str
:param use_ui: if True the method will use parameters set in UI
:type use_ui: bool
:return: None
:rtype: None
"""
# create a local copy of the exobj.drills so it can be used for creating drill CCode geometry
@ -2780,7 +2781,7 @@ class CNCjob(Geometry):
self.app.inform.emit(_("Creating a list of points to drill..."))
# Points (Group by tool)
# Points (Group by tool): a dictionary of shapely Point geo elements grouped by tool number
points = {}
for drill in exobj.drills:
if self.app.abort_flag:
@ -2795,6 +2796,17 @@ class CNCjob(Geometry):
# log.debug("Found %d drills." % len(points))
# check if there are drill points in the exclusion areas.
# If we find any within the exclusion areas return 'fail'
for tool in points:
for pt in points[tool]:
for area in self.app.exc_areas.exclusion_areas_storage:
pt_buf = pt.buffer(exobj.tools[tool]['C'] / 2.0)
if pt_buf.within(area['shape']) or pt_buf.intersects(area['shape']):
self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed. Drill points inside the exclusion zones."))
return 'fail'
# this holds the resulting GCode
self.gcode = []
self.f_plunge = self.app.defaults["excellon_f_plunge"]
@ -3042,7 +3054,41 @@ class CNCjob(Geometry):
locx = locations[k][0]
locy = locations[k][1]
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
end_point=(locx, locy),
tooldia=current_tooldia)
prev_z = None
for travel in travels:
locx = travel[1][0]
locy = travel[1][1]
if travel[0] is not None:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# raise to safe Z (travel[0]) each time because safe Z may be different
self.z_move = travel[0]
gcode += self.doformat(p.lift_code, x=locx, y=locy)
# restore z_move
self.z_move = exobj.tools[tool]['data']['travelz']
else:
if prev_z is not None:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# we assume that previously the z_move was altered therefore raise to
# the travel_z (z_move)
self.z_move = exobj.tools[tool]['data']['travelz']
gcode += self.doformat(p.lift_code, x=locx, y=locy)
else:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# store prev_z
prev_z = travel[0]
# gcode += self.doformat(p.rapid_code, x=locx, y=locy)
if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
doc = deepcopy(self.z_cut)
@ -3260,7 +3306,41 @@ class CNCjob(Geometry):
locx = locations[k][0]
locy = locations[k][1]
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
end_point=(locx, locy),
tooldia=current_tooldia)
prev_z = None
for travel in travels:
locx = travel[1][0]
locy = travel[1][1]
if travel[0] is not None:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# raise to safe Z (travel[0]) each time because safe Z may be different
self.z_move = travel[0]
gcode += self.doformat(p.lift_code, x=locx, y=locy)
# restore z_move
self.z_move = exobj.tools[tool]['data']['travelz']
else:
if prev_z is not None:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# we assume that previously the z_move was altered therefore raise to
# the travel_z (z_move)
self.z_move = exobj.tools[tool]['data']['travelz']
gcode += self.doformat(p.lift_code, x=locx, y=locy)
else:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# store prev_z
prev_z = travel[0]
# gcode += self.doformat(p.rapid_code, x=locx, y=locy)
if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
doc = deepcopy(self.z_cut)
@ -3429,7 +3509,41 @@ class CNCjob(Geometry):
locx = point[0]
locy = point[1]
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
end_point=(locx, locy),
tooldia=current_tooldia)
prev_z = None
for travel in travels:
locx = travel[1][0]
locy = travel[1][1]
if travel[0] is not None:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# raise to safe Z (travel[0]) each time because safe Z may be different
self.z_move = travel[0]
gcode += self.doformat(p.lift_code, x=locx, y=locy)
# restore z_move
self.z_move = exobj.tools[tool]['data']['travelz']
else:
if prev_z is not None:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# we assume that previously the z_move was altered therefore raise to
# the travel_z (z_move)
self.z_move = exobj.tools[tool]['data']['travelz']
gcode += self.doformat(p.lift_code, x=locx, y=locy)
else:
# move to next point
gcode += self.doformat(p.rapid_code, x=locx, y=locy)
# store prev_z
prev_z = travel[0]
# gcode += self.doformat(p.rapid_code, x=locx, y=locy)
if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
doc = deepcopy(self.z_cut)
@ -4827,14 +4941,27 @@ class CNCjob(Geometry):
# plot the geometry of Excellon objects
if self.origin_kind == 'excellon':
try:
poly = Polygon(geo['geom'])
except ValueError:
# if the geos are travel lines it will enter into Exception
poly = geo['geom'].buffer(distance=(tooldia / 1.99999999), resolution=self.steps_per_circle)
if geo['kind'][0] == 'T':
# if the geos are travel lines it will enter into Exception
poly = geo['geom'].buffer(distance=(tooldia / 1.99999999),
resolution=self.steps_per_circle)
else:
poly = Polygon(geo['geom'])
poly = poly.simplify(tool_tolerance)
except Exception:
# deal here with unexpected plot errors due of LineStrings not valid
continue
# try:
# poly = Polygon(geo['geom'])
# except ValueError:
# # if the geos are travel lines it will enter into Exception
# poly = geo['geom'].buffer(distance=(tooldia / 1.99999999), resolution=self.steps_per_circle)
# poly = poly.simplify(tool_tolerance)
# except Exception:
# # deal here with unexpected plot errors due of LineStrings not valid
# continue
else:
# plot the geometry of any objects other than Excellon
poly = geo['geom'].buffer(distance=(tooldia / 1.99999999), resolution=self.steps_per_circle)