- 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

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