- working on Isolation Tool: made to work the Isolation with multiple tools without rest machining

This commit is contained in:
Marius Stanciu 2020-05-27 05:11:54 +03:00 committed by Marius
parent 66ceb5a360
commit 90eb581a34
4 changed files with 463 additions and 321 deletions

View File

@ -281,7 +281,7 @@ class ToolsISOPrefGroupUI(OptionsGroupUI):
) )
self.select_combo = FCComboBox() self.select_combo = FCComboBox()
self.select_combo.addItems( self.select_combo.addItems(
[_("All"), _("Area Selection"), _("Reference Object")] [_("All"), _("Area Selection"), _("Polygon Selection"), _("Reference Object")]
) )
self.select_combo.setObjectName("i_selection") self.select_combo.setObjectName("i_selection")

View File

@ -491,6 +491,29 @@ class GerberObject(FlatCAMObj, Gerber):
return 'fail' return 'fail'
return geom return geom
def follow_geo(self, outname=None):
"""
Creates a geometry object "following" the gerber paths.
:return: None
"""
if outname is None:
follow_name = self.options["name"] + "_follow"
else:
follow_name = outname
def follow_init(follow_obj, app):
# Propagate options
follow_obj.options["cnctooldia"] = str(self.options["isotooldia"])
follow_obj.solid_geometry = self.follow_geometry
# TODO: Do something if this is None. Offer changing name?
try:
self.app.app_obj.new_object("geometry", follow_name, follow_init)
except Exception as e:
return "Operation failed: %s" % str(e)
def on_plot_cb_click(self, *args): def on_plot_cb_click(self, *args):
if self.muted_ui: if self.muted_ui:
return return

View File

@ -507,7 +507,7 @@ class ToolIsolation(AppTool, Gerber):
) )
self.select_combo = FCComboBox() self.select_combo = FCComboBox()
self.select_combo.addItems( self.select_combo.addItems(
[_("All"), _("Area Selection"), _("Reference Object")] [_("All"), _("Area Selection"), _("Polygon Selection"), _("Reference Object")]
) )
self.select_combo.setObjectName("i_selection") self.select_combo.setObjectName("i_selection")
@ -666,6 +666,11 @@ class ToolIsolation(AppTool, Gerber):
self.kp = None self.kp = None
# store geometry from Polygon selection
self.poly_dict = {}
self.grid_status_memory = self.app.ui.grid_snap_btn.isChecked()
# store here solid_geometry when there are tool with isolation job # store here solid_geometry when there are tool with isolation job
self.solid_geometry = [] self.solid_geometry = []
@ -717,7 +722,7 @@ class ToolIsolation(AppTool, Gerber):
self.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked) self.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
self.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked) self.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
self.generate_iso_button.clicked.connect(self.on_isolate_click) self.generate_iso_button.clicked.connect(self.on_iso_button_click)
self.reset_button.clicked.connect(self.set_tool_ui) self.reset_button.clicked.connect(self.set_tool_ui)
# Cleanup on Graceful exit (CTRL+ALT+X combo key) # Cleanup on Graceful exit (CTRL+ALT+X combo key)
@ -989,7 +994,7 @@ class ToolIsolation(AppTool, Gerber):
self.iso_overlap_entry.set_value(self.app.defaults["tools_iso_overlap"]) self.iso_overlap_entry.set_value(self.app.defaults["tools_iso_overlap"])
self.milling_type_radio.set_value(self.app.defaults["tools_iso_milling_type"]) self.milling_type_radio.set_value(self.app.defaults["tools_iso_milling_type"])
self.combine_passes_cb.set_value(self.app.defaults["tools_iso_combine_passes"]) self.combine_passes_cb.set_value(self.app.defaults["tools_iso_combine_passes"])
self.area_shape_radio.set_value(self.app.defaults["tools_iso_combine_passes"]) self.area_shape_radio.set_value(self.app.defaults["tools_iso_area_shape"])
self.cutz_entry.set_value(self.app.defaults["tools_iso_tool_cutz"]) self.cutz_entry.set_value(self.app.defaults["tools_iso_tool_cutz"])
self.tool_type_radio.set_value(self.app.defaults["tools_iso_tool_type"]) self.tool_type_radio.set_value(self.app.defaults["tools_iso_tool_type"])
@ -999,10 +1004,12 @@ class ToolIsolation(AppTool, Gerber):
self.on_tool_type(val=self.tool_type_radio.get_value()) self.on_tool_type(val=self.tool_type_radio.get_value())
outname = self.app.collection.get_by_name(self.object_combo.get_value()).options['name']
# init the working variables # init the working variables
self.default_data.clear() self.default_data.clear()
self.default_data = { self.default_data = {
"name": '_ncc', "name": outname + '_iso',
"plot": self.app.defaults["geometry_plot"], "plot": self.app.defaults["geometry_plot"],
"cutz": float(self.cutz_entry.get_value()), "cutz": float(self.cutz_entry.get_value()),
"vtipdia": float(self.tipdia_entry.get_value()), "vtipdia": float(self.tipdia_entry.get_value()),
@ -1300,9 +1307,16 @@ class ToolIsolation(AppTool, Gerber):
self.area_shape_label.show() self.area_shape_label.show()
self.area_shape_radio.show() self.area_shape_radio.show()
# disable rest-machining for area painting # disable rest-machining for area isolation
self.rest_cb.set_value(False) self.rest_cb.set_value(False)
self.rest_cb.setDisabled(True) self.rest_cb.setDisabled(True)
elif val == _("Polygon Selection"):
self.reference_combo.hide()
self.reference_combo_label.hide()
self.reference_combo_type.hide()
self.reference_combo_type_label.hide()
self.area_shape_label.hide()
self.area_shape_radio.hide()
else: else:
self.reference_combo.show() self.reference_combo.show()
self.reference_combo_label.show() self.reference_combo_label.show()
@ -1691,60 +1705,89 @@ class ToolIsolation(AppTool, Gerber):
def on_iso_button_click(self, *args): def on_iso_button_click(self, *args):
obj = self.app.collection.get_active() self.obj_name = self.object_combo.currentText()
self.iso_type = 2 # Get source object.
if self.ui.iso_type_radio.get_value() == 'ext': try:
self.iso_type = 0 self.grb_obj = self.app.collection.get_by_name(self.obj_name)
if self.ui.iso_type_radio.get_value() == 'int': except Exception as e:
self.iso_type = 1 self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
if self.grb_obj is None:
self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
return
def worker_task(iso_obj, app_obj): def worker_task(iso_obj, app_obj):
with self.app.proc_container.new(_("Isolating...")): with self.app.proc_container.new(_("Isolating...")):
if self.ui.follow_cb.get_value() is True: self.isolate_handler(iso_obj)
iso_obj.follow_geo()
# in the end toggle the visibility of the origin object so we can see the generated Geometry
iso_obj.ui.plot_cb.toggle()
else:
app_obj.defaults.report_usage("gerber_on_iso_button")
self.read_form()
iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single' self.app.worker_task.emit({'fcn': worker_task, 'params': [self.grb_obj, self.app]})
self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope)
self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) def follow_geo(self, followed_obj, outname):
def follow_geo(self, outname=None):
""" """
Creates a geometry object "following" the gerber paths. Creates a geometry object "following" the gerber paths.
:param followed_obj: Gerber object for which to generate the follow geometry
:type followed_obj: AppObjects.FlatCAMGerber.GerberObject
:param outname: Nme of the resulting Geometry object
:type outname: str
:return: None :return: None
""" """
# default_name = self.options["name"] + "_follow"
# follow_name = outname or default_name
if outname is None:
follow_name = self.options["name"] + "_follow"
else:
follow_name = outname
def follow_init(follow_obj, app): def follow_init(follow_obj, app):
# Propagate options # Propagate options
follow_obj.options["cnctooldia"] = str(self.options["isotooldia"]) follow_obj.options["cnctooldia"] = str(tooldia)
follow_obj.solid_geometry = self.follow_geometry follow_obj.solid_geometry = self.grb_obj.follow_geometry
# TODO: Do something if this is None. Offer changing name? # in the end toggle the visibility of the origin object so we can see the generated Geometry
try: followed_obj.ui.plot_cb.set_value(False)
self.app.app_obj.new_object("geometry", follow_name, follow_init) follow_name = outname
except Exception as e:
return "Operation failed: %s" % str(e)
def isolate_handler(self, iso_type, iso_scope): for tool in self.iso_tools:
tooldia = self.iso_tools[tool]['tooldia']
new_name = "%s_%.*f" % (follow_name, self.decimals, tooldia)
if iso_scope == 'all': follow_state = self.iso_tools[tool]['data']['tools_iso_follow']
self.isolate(iso_type=iso_type) if follow_state:
else: ret = self.app.app_obj.new_object("geometry", new_name, follow_init)
if ret == 'fail':
self.app.inform.emit("[ERROR_NOTCL] %s: %.*f" % (
_("Failed to create Follow Geometry with tool diameter"), self.decimals, tooldia))
else:
self.app.inform.emit("[success] %s: %.*f" % (
_("Follow Geometry was created with tool diameter"), self.decimals, tooldia))
def isolate_handler(self, isolated_obj):
"""
Creates a geometry object with paths around the gerber features.
:param isolated_obj: Gerber object for which to generate the isolating routing geometry
:type isolated_obj: AppObjects.FlatCAMGerber.GerberObject
:return: None
"""
selection = self.select_combo.get_value()
if selection == _("All"):
full_geo = isolated_obj.solid_geometry
self.isolate(isolated_obj=isolated_obj, geometry=full_geo)
elif selection == _("Area Selection"):
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
else:
self.app.plotcanvas.graph_event_disconnect(self.app.mp)
self.app.plotcanvas.graph_event_disconnect(self.app.mm)
self.app.plotcanvas.graph_event_disconnect(self.app.mr)
self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
elif selection == _("Polygon Selection"):
# disengage the grid snapping since it may be hard to click on polygons with grid snapping on # disengage the grid snapping since it may be hard to click on polygons with grid snapping on
if self.app.ui.grid_snap_btn.isChecked(): if self.app.ui.grid_snap_btn.isChecked():
self.grid_status_memory = True self.grid_status_memory = True
@ -1753,146 +1796,149 @@ class ToolIsolation(AppTool, Gerber):
self.grid_status_memory = False self.grid_status_memory = False
self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release) self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
if self.app.is_legacy is False: if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) self.app.plotcanvas.graph_event_disconnect('mouse_release',
self.app.on_mouse_click_release_over_plot)
else: else:
self.app.plotcanvas.graph_event_disconnect(self.app.mr) self.app.plotcanvas.graph_event_disconnect(self.app.mr)
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it.")) self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
elif selection == _("Reference Object"):
ref_obj = self.app.collection.get_by_name(self.reference_combo.get_value())
ref_geo = cascaded_union(ref_obj.solid_geometry)
use_geo = cascaded_union(isolated_obj.solid_geometry).difference(ref_geo)
self.isolate(isolated_obj=isolated_obj, geometry=use_geo)
def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None, def isolate(self, isolated_obj, geometry=None, limited_area=None, plot=True):
milling_type=None, follow=None, plot=True):
""" """
Creates an isolation routing geometry object in the project. Creates an isolation routing geometry object in the project.
:param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both :param isolated_obj: Gerber object for which to generate the isolating routing geometry
:param geometry: specific geometry to isolate :type isolated_obj: AppObjects.FlatCAMGerber.GerberObject
:param dia: Tool diameter :param geometry: specific geometry to isolate
:param passes: Number of tool widths to cut :type geometry: List of Shapely polygon
:param overlap: Overlap between passes in fraction of tool diameter :param limited_area: if not None clear only this area
:param outname: Base name of the output object :type limited_area: Shapely Polygon or a list of them
:param combine: Boolean: if to combine passes in one resulting object in case of multiple passes :param plot: if to plot the resulting geometry object
:param milling_type: type of milling: conventional or climbing :type plot: bool
:param follow: Boolean: if to generate a 'follow' geometry
:param plot: Boolean: if to plot the resulting geometry object
:return: None :return: None
""" """
if geometry is None: iso_name = isolated_obj.options["name"]
work_geo = self.follow_geometry if follow is True else self.solid_geometry combine = self.combine_passes_cb.get_value()
else: tools_storage = self.iso_tools
work_geo = geometry
if dia is None:
dia = float(self.options["isotooldia"])
if passes is None:
passes = int(self.options["isopasses"])
if overlap is None:
overlap = float(self.options["isooverlap"])
overlap /= 100.0
combine = self.options["combine_passes"] if combine is None else bool(combine)
if milling_type is None:
milling_type = self.options["milling_type"]
if iso_type is None:
iso_t = 2
else:
iso_t = iso_type
base_name = self.options["name"]
if combine: if combine:
if outname is None: total_solid_geometry = []
if self.iso_type == 0:
iso_name = base_name + "_ext_iso"
elif self.iso_type == 1:
iso_name = base_name + "_int_iso"
else:
iso_name = base_name + "_iso"
else:
iso_name = outname
def iso_init(geo_obj, app_obj): for tool in tools_storage:
# Propagate options tool_dia = tools_storage[tool]['tooldia']
geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) tool_type = tools_storage[tool]['tool_type']
geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper() tool_data = tools_storage[tool]['data']
geo_obj.solid_geometry = [] to_follow = tool_data['tools_iso_follow']
work_geo = geometry
if work_geo is None:
work_geo = isolated_obj.follow_geometry if to_follow else isolated_obj.solid_geometry
iso_t = {
'ext': 0,
'int': 1,
'full': 2
}[tool_data['tools_iso_isotype']]
passes = tool_data['tools_iso_passes']
overlap = tool_data['tools_iso_overlap']
overlap /= 100.0
milling_type = tool_data['tools_iso_milling_type']
iso_except = tool_data['tools_iso_isoexcept']
outname = "%s_%.*f" % (isolated_obj.options["name"], self.decimals, float(tool_dia))
iso_name = outname + "_iso"
if iso_t == 0:
iso_name = outname + "_ext_iso"
elif iso_t == 1:
iso_name = outname + "_int_iso"
# transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
if self.ui.tool_type_radio.get_value() == 'v': if tool_type.lower() == 'v':
new_cutz = self.ui.cutz_spinner.get_value() new_cutz = self.ui.cutz_spinner.get_value()
new_vtipdia = self.ui.tipdia_spinner.get_value() new_vtipdia = self.ui.tipdia_spinner.get_value()
new_vtipangle = self.ui.tipangle_spinner.get_value() new_vtipangle = self.ui.tipangle_spinner.get_value()
tool_type = 'V' tool_type = 'V'
tool_data.update({
"name": iso_name,
"cutz": new_cutz,
"vtipdia": new_vtipdia,
"vtipangle": new_vtipangle,
})
else: else:
new_cutz = self.app.defaults['geometry_cutz'] tool_data.update({
new_vtipdia = self.app.defaults['geometry_vtipdia'] "name": iso_name,
new_vtipangle = self.app.defaults['geometry_vtipangle'] })
tool_type = 'C1' tool_type = 'C1'
# store here the default data for Geometry Data solid_geo = []
default_data = {} for nr_pass in range(passes):
default_data.update({ iso_offset = tool_dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * tool_dia)
"name": iso_name,
"plot": self.app.defaults['geometry_plot'],
"cutz": new_cutz,
"vtipdia": new_vtipdia,
"vtipangle": new_vtipangle,
"travelz": self.app.defaults['geometry_travelz'],
"feedrate": self.app.defaults['geometry_feedrate'],
"feedrate_z": self.app.defaults['geometry_feedrate_z'],
"feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
"dwell": self.app.defaults['geometry_dwell'],
"dwelltime": self.app.defaults['geometry_dwelltime'],
"multidepth": self.app.defaults['geometry_multidepth'],
"ppname_g": self.app.defaults['geometry_ppname_g'],
"depthperpass": self.app.defaults['geometry_depthperpass'],
"extracut": self.app.defaults['geometry_extracut'],
"extracut_length": self.app.defaults['geometry_extracut_length'],
"toolchange": self.app.defaults['geometry_toolchange'],
"toolchangez": self.app.defaults['geometry_toolchangez'],
"endz": self.app.defaults['geometry_endz'],
"spindlespeed": self.app.defaults['geometry_spindlespeed'],
"toolchangexy": self.app.defaults['geometry_toolchangexy'],
"startz": self.app.defaults['geometry_startz']
})
geo_obj.tools = {} # if milling type is climb then the move is counter-clockwise around features
geo_obj.tools['1'] = {} mill_dir = 1 if milling_type == 'cl' else 0
geo_obj.tools.update({
'1': { iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
'tooldia': float(self.options["isotooldia"]), follow=to_follow, nr_passes=nr_pass)
if iso_geo == 'fail':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
continue
try:
for geo in iso_geo:
solid_geo.append(geo)
except TypeError:
solid_geo.append(iso_geo)
# ############################################################
# ########## AREA SUBTRACTION ################################
# ############################################################
if iso_except:
self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
solid_geo = self.area_subtraction(solid_geo)
if limited_area:
self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo"))
solid_geo = self.area_intersection(solid_geo, intersection_geo=limited_area)
tools_storage.update({
tool: {
'tooldia': float(tool_dia),
'offset': 'Path', 'offset': 'Path',
'offset_value': 0.0, 'offset_value': 0.0,
'type': _('Rough'), 'type': _('Rough'),
'tool_type': tool_type, 'tool_type': tool_type,
'data': default_data, 'data': tool_data,
'solid_geometry': geo_obj.solid_geometry 'solid_geometry': deepcopy(solid_geo)
} }
}) })
for nr_pass in range(passes): total_solid_geometry += solid_geo
iso_offset = dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * dia)
# if milling type is climb then the move is counter-clockwise around features def iso_init(geo_obj, app_obj):
mill_dir = 1 if milling_type == 'cl' else 0 geo_obj.options["cnctooldia"] = str(tool_dia)
geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
follow=follow, nr_passes=nr_pass)
if geom == 'fail': geo_obj.tools = dict(tools_storage)
app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) geo_obj.solid_geometry = total_solid_geometry
return 'fail' # even if combine is checked, one pass is still single-geo
geo_obj.solid_geometry.append(geom)
# update the geometry in the tools if len(self.iso_tools) > 1:
geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry geo_obj.multigeo = True
else:
passes = float(self.iso_tools[0]['data']['tools_iso_passes'])
geo_obj.multigeo = True if passes > 1 else False
# detect if solid_geometry is empty and this require list flattening which is "heavy" # detect if solid_geometry is empty and this require list flattening which is "heavy"
# or just looking in the lists (they are one level depth) and if any is not empty # or just looking in the lists (they are one level depth) and if any is not empty
@ -1910,158 +1956,154 @@ class ToolIsolation(AppTool, Gerber):
empty_cnt += 1 empty_cnt += 1
if empty_cnt == len(geo_obj.solid_geometry): if empty_cnt == len(geo_obj.solid_geometry):
raise ValidationError("Empty Geometry", None) app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Empty Geometry in"), geo_obj.options["name"]))
return 'fail'
else: else:
app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"])) app_obj.inform.emit('[success] %s: %s' % (_("Isolation geometry created"), geo_obj.options["name"]))
# even if combine is checked, one pass is still single-geo
geo_obj.multigeo = True if passes > 1 else False
# ############################################################
# ########## AREA SUBTRACTION ################################
# ############################################################
if self.ui.except_cb.get_value():
self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
# TODO: Do something if this is None. Offer changing name?
self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot) self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
else:
for i in range(passes):
offset = dia * ((2 * i + 1) / 2.0000001) - (i * overlap * dia)
if passes > 1:
if outname is None:
if self.iso_type == 0:
iso_name = base_name + "_ext_iso" + str(i + 1)
elif self.iso_type == 1:
iso_name = base_name + "_int_iso" + str(i + 1)
else:
iso_name = base_name + "_iso" + str(i + 1)
else:
iso_name = outname
else:
if outname is None:
if self.iso_type == 0:
iso_name = base_name + "_ext_iso"
elif self.iso_type == 1:
iso_name = base_name + "_int_iso"
else:
iso_name = base_name + "_iso"
else:
iso_name = outname
def iso_init(geo_obj, fc_obj): else:
# Propagate options for tool in tools_storage:
geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) tool_data = tools_storage[tool]['data']
if self.ui.tool_type_radio.get_value() == 'v': to_follow = tool_data['tools_iso_follow']
geo_obj.tool_type = 'V'
work_geo = geometry
if work_geo is None:
work_geo = isolated_obj.follow_geometry if to_follow else isolated_obj.solid_geometry
iso_t = {
'ext': 0,
'int': 1,
'full': 2
}[tool_data['tools_iso_isotype']]
passes = tool_data['tools_iso_passes']
overlap = tool_data['tools_iso_overlap']
overlap /= 100.0
milling_type = tool_data['tools_iso_milling_type']
iso_except = tool_data['tools_iso_isoexcept']
for i in range(passes):
tool_dia = tools_storage[tool]['tooldia']
tool_type = tools_storage[tool]['tool_type']
iso_offset = tool_dia * ((2 * i + 1) / 2.0000001) - (i * overlap * tool_dia)
outname = "%s_%.*f" % (isolated_obj.options["name"], self.decimals, float(tool_dia))
if passes > 1:
iso_name = outname + "_iso" + str(i + 1)
if iso_t == 0:
iso_name = outname + "_ext_iso" + str(i + 1)
elif iso_t == 1:
iso_name = outname + "_int_iso" + str(i + 1)
else: else:
geo_obj.tool_type = 'C1' iso_name = outname + "_iso"
if iso_t == 0:
iso_name = outname + "_ext_iso"
elif iso_t == 1:
iso_name = outname + "_int_iso"
# if milling type is climb then the move is counter-clockwise around features # if milling type is climb then the move is counter-clockwise around features
mill_dir = 1 if milling_type == 'cl' else 0 mill_dir = 1 if milling_type == 'cl' else 0
geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
follow=follow,
nr_passes=i)
if geom == 'fail': iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
fc_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) follow=to_follow, nr_passes=i)
return 'fail' if iso_geo == 'fail':
self.app.inform.emit(
geo_obj.solid_geometry = geom '[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
continue
# transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
# even if the resulting geometry is not multigeo we add the tools dict which will hold the data
# required to be transfered to the Geometry object
if self.ui.tool_type_radio.get_value() == 'v':
new_cutz = self.ui.cutz_spinner.get_value()
new_vtipdia = self.ui.tipdia_spinner.get_value()
new_vtipangle = self.ui.tipangle_spinner.get_value()
tool_type = 'V'
else:
new_cutz = self.app.defaults['geometry_cutz']
new_vtipdia = self.app.defaults['geometry_vtipdia']
new_vtipangle = self.app.defaults['geometry_vtipangle']
tool_type = 'C1'
# store here the default data for Geometry Data
default_data = {}
default_data.update({
"name": iso_name,
"plot": self.app.defaults['geometry_plot'],
"cutz": new_cutz,
"vtipdia": new_vtipdia,
"vtipangle": new_vtipangle,
"travelz": self.app.defaults['geometry_travelz'],
"feedrate": self.app.defaults['geometry_feedrate'],
"feedrate_z": self.app.defaults['geometry_feedrate_z'],
"feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
"dwell": self.app.defaults['geometry_dwell'],
"dwelltime": self.app.defaults['geometry_dwelltime'],
"multidepth": self.app.defaults['geometry_multidepth'],
"ppname_g": self.app.defaults['geometry_ppname_g'],
"depthperpass": self.app.defaults['geometry_depthperpass'],
"extracut": self.app.defaults['geometry_extracut'],
"extracut_length": self.app.defaults['geometry_extracut_length'],
"toolchange": self.app.defaults['geometry_toolchange'],
"toolchangez": self.app.defaults['geometry_toolchangez'],
"endz": self.app.defaults['geometry_endz'],
"spindlespeed": self.app.defaults['geometry_spindlespeed'],
"toolchangexy": self.app.defaults['geometry_toolchangexy'],
"startz": self.app.defaults['geometry_startz']
})
geo_obj.tools = {}
geo_obj.tools['1'] = {}
geo_obj.tools.update({
'1': {
'tooldia': float(self.options["isotooldia"]),
'offset': 'Path',
'offset_value': 0.0,
'type': _('Rough'),
'tool_type': tool_type,
'data': default_data,
'solid_geometry': geo_obj.solid_geometry
}
})
# detect if solid_geometry is empty and this require list flattening which is "heavy"
# or just looking in the lists (they are one level depth) and if any is not empty
# proceed with object creation, if there are empty and the number of them is the length
# of the list then we have an empty solid_geometry which should raise a Custom Exception
empty_cnt = 0
if not isinstance(geo_obj.solid_geometry, list):
geo_obj.solid_geometry = [geo_obj.solid_geometry]
for g in geo_obj.solid_geometry:
if g:
break
else:
empty_cnt += 1
if empty_cnt == len(geo_obj.solid_geometry):
raise ValidationError("Empty Geometry", None)
else:
fc_obj.inform.emit('[success] %s: %s' %
(_("Isolation geometry created"), geo_obj.options["name"]))
geo_obj.multigeo = False
# ############################################################ # ############################################################
# ########## AREA SUBTRACTION ################################ # ########## AREA SUBTRACTION ################################
# ############################################################ # ############################################################
if self.ui.except_cb.get_value(): if iso_except:
self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry) iso_geo = self.area_subtraction(iso_geo)
# TODO: Do something if this is None. Offer changing name? if limited_area:
self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot) self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo"))
iso_geo = self.area_intersection(iso_geo, intersection_geo=limited_area)
def area_subtraction(self, geo, subtractor_geo=None): # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in
# Gerber UI
if tool_type.lower() == 'v':
new_cutz = self.ui.cutz_spinner.get_value()
new_vtipdia = self.ui.tipdia_spinner.get_value()
new_vtipangle = self.ui.tipangle_spinner.get_value()
tool_type = 'V'
tool_data.update({
"name": iso_name,
"cutz": new_cutz,
"vtipdia": new_vtipdia,
"vtipangle": new_vtipangle,
})
else:
tool_data.update({
"name": iso_name,
})
tool_type = 'C1'
def iso_init(geo_obj, fc_obj):
# Propagate options
geo_obj.options["cnctooldia"] = str(tool_dia)
geo_obj.solid_geometry = deepcopy(iso_geo)
# ############################################################
# ########## AREA SUBTRACTION ################################
# ############################################################
if self.except_cb.get_value():
self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
geo_obj.tools = {}
geo_obj.tools['1'] = {}
geo_obj.tools.update({
'1': {
'tooldia': float(tool_dia),
'offset': 'Path',
'offset_value': 0.0,
'type': _('Rough'),
'tool_type': tool_type,
'data': tool_data,
'solid_geometry': geo_obj.solid_geometry
}
})
# detect if solid_geometry is empty and this require list flattening which is "heavy"
# or just looking in the lists (they are one level depth) and if any is not empty
# proceed with object creation, if there are empty and the number of them is the length
# of the list then we have an empty solid_geometry which should raise a Custom Exception
empty_cnt = 0
if not isinstance(geo_obj.solid_geometry, list):
geo_obj.solid_geometry = [geo_obj.solid_geometry]
for g in geo_obj.solid_geometry:
if g:
break
else:
empty_cnt += 1
if empty_cnt == len(geo_obj.solid_geometry):
fc_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (
_("Empty Geometry in"), geo_obj.options["name"]))
return 'fail'
else:
fc_obj.inform.emit('[success] %s: %s' %
(_("Isolation geometry created"), geo_obj.options["name"]))
geo_obj.multigeo = False
# TODO: Do something if this is None. Offer changing name?
self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
def area_subtraction(self, geo, subtractor_geo):
""" """
Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo
:param geo: target geometry from which to subtract :param geo: target geometry from which to subtract
:param subtractor_geo: geometry that acts as subtractor :param subtractor_geo: geometry that acts as subtractor
:return: :return:
""" """
new_geometry = [] new_geometry = []
@ -2070,7 +2112,7 @@ class ToolIsolation(AppTool, Gerber):
if subtractor_geo: if subtractor_geo:
sub_union = cascaded_union(subtractor_geo) sub_union = cascaded_union(subtractor_geo)
else: else:
name = self.ui.obj_combo.currentText() name = self.exc_obj_combo.currentText()
subtractor_obj = self.app.collection.get_by_name(name) subtractor_obj = self.app.collection.get_by_name(name)
sub_union = cascaded_union(subtractor_obj.solid_geometry) sub_union = cascaded_union(subtractor_obj.solid_geometry)
@ -2115,6 +2157,60 @@ class ToolIsolation(AppTool, Gerber):
new_geometry.append(new_geo) new_geometry.append(new_geo)
return new_geometry return new_geometry
def area_intersection(self, geo, intersection_geo=None):
"""
Return the intersection geometry between geo and intersection_geo
:param geo: target geometry
:param intersection_geo: second geometry
:return:
"""
new_geometry = []
target_geo = geo
intersect_union = cascaded_union(intersection_geo)
try:
for geo_elem in target_geo:
if isinstance(geo_elem, Polygon):
for ring in self.poly2rings(geo_elem):
new_geo = ring.intersection(intersect_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo_elem, MultiPolygon):
for poly in geo_elem:
for ring in self.poly2rings(poly):
new_geo = ring.intersection(intersect_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo_elem, LineString):
new_geo = geo_elem.intersection(intersect_union)
if new_geo:
if not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(geo_elem, MultiLineString):
for line_elem in geo_elem:
new_geo = line_elem.intersection(intersect_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
except TypeError:
if isinstance(target_geo, Polygon):
for ring in self.poly2rings(target_geo):
new_geo = ring.intersection(intersect_union)
if new_geo:
if not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(target_geo, LineString):
new_geo = target_geo.intersection(intersect_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
elif isinstance(target_geo, MultiLineString):
for line_elem in target_geo:
new_geo = line_elem.intersection(intersect_union)
if new_geo and not new_geo.is_empty:
new_geometry.append(new_geo)
return new_geometry
def on_mouse_click_release(self, event): def on_mouse_click_release(self, event):
if self.app.is_legacy is False: if self.app.is_legacy is False:
event_pos = event.pos event_pos = event.pos
@ -2139,7 +2235,7 @@ class ToolIsolation(AppTool, Gerber):
curr_pos = (curr_pos[0], curr_pos[1]) curr_pos = (curr_pos[0], curr_pos[1])
if event.button == 1: if event.button == 1:
clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1])) clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]), geoset=self.grb_obj.solid_geometry)
if self.app.selection_type is not None: if self.app.selection_type is not None:
self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type) self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type)
@ -2179,8 +2275,10 @@ class ToolIsolation(AppTool, Gerber):
if self.app.is_legacy is False: if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_pres)
else: else:
self.app.plotcanvas.graph_event_disconnect(self.mr) self.app.plotcanvas.graph_event_disconnect(self.mr)
self.app.plotcanvas.graph_event_disconnect(self.kp)
self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
self.app.on_mouse_click_release_over_plot) self.app.on_mouse_click_release_over_plot)
@ -2189,7 +2287,7 @@ class ToolIsolation(AppTool, Gerber):
if self.poly_dict: if self.poly_dict:
poly_list = deepcopy(list(self.poly_dict.values())) poly_list = deepcopy(list(self.poly_dict.values()))
self.isolate(iso_type=self.iso_type, geometry=poly_list) self.isolate(isolated_obj=self.grb_obj, geometry=poly_list)
self.poly_dict.clear() self.poly_dict.clear()
else: else:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting.")) self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
@ -2383,12 +2481,8 @@ class ToolIsolation(AppTool, Gerber):
return return
self.sel_rect = cascaded_union(self.sel_rect) self.sel_rect = cascaded_union(self.sel_rect)
self.isolate(isolated_obj=self.grb_obj, limited_area=self.sel_rect, plot=True)
self.clear_copper(ncc_obj=self.grb_obj, self.sel_rect = []
sel_obj=self.bound_obj,
ncctooldia=self.ncc_dia_list,
isotooldia=self.iso_dia_list,
outname=self.o_name)
# called on mouse move # called on mouse move
def on_mouse_move(self, event): def on_mouse_move(self, event):
@ -2689,7 +2783,7 @@ class ToolIsolation(AppTool, Gerber):
rest_machining_choice = self.rest_cb.get_value() rest_machining_choice = self.rest_cb.get_value()
# determine if to use the progressive plotting # determine if to use the progressive plotting
prog_plot = True if self.app.defaults["tools_ncc_plotting"] == 'progressive' else False prog_plot = True if self.app.defaults["tools_iso_plotting"] == 'progressive' else False
tools_storage = tools_storage if tools_storage is not None else self.iso_tools tools_storage = tools_storage if tools_storage is not None else self.iso_tools
# ###################################################################################################### # ######################################################################################################
@ -2906,7 +3000,7 @@ class ToolIsolation(AppTool, Gerber):
else: else:
log.debug("There are no geometries in the cleared polygon.") log.debug("There are no geometries in the cleared polygon.")
# clean the progressive plotted shapes if it was used # clean the progressive plotted shapes if it was used
if self.app.defaults["tools_ncc_plotting"] == 'progressive': if self.app.defaults["tools_iso_plotting"] == 'progressive':
self.temp_shapes.clear(update=True) self.temp_shapes.clear(update=True)
# delete tools with empty geometry # delete tools with empty geometry
@ -3221,7 +3315,7 @@ class ToolIsolation(AppTool, Gerber):
geo_obj.options["cnctooldia"] = str(tool) geo_obj.options["cnctooldia"] = str(tool)
# clean the progressive plotted shapes if it was used # clean the progressive plotted shapes if it was used
if self.app.defaults["tools_ncc_plotting"] == 'progressive': if self.app.defaults["tools_iso_plotting"] == 'progressive':
self.temp_shapes.clear(update=True) self.temp_shapes.clear(update=True)
# check to see if geo_obj.tools is empty # check to see if geo_obj.tools is empty
@ -3290,40 +3384,61 @@ class ToolIsolation(AppTool, Gerber):
def poly2rings(poly): def poly2rings(poly):
return [poly.exterior] + [interior for interior in poly.interiors] return [poly.exterior] + [interior for interior in poly.interiors]
def generate_envelope(self, offset, invert, envelope_iso_type=2, follow=None): def generate_envelope(self, offset, invert, geometry=None, env_iso_type=2, follow=None, nr_passes=0):
# isolation_geometry produces an envelope that is going on the left of the geometry """
# (the copper features). To leave the least amount of burrs on the features Isolation_geometry produces an envelope that is going on the left of the geometry
# the tool needs to travel on the right side of the features (this is called conventional milling) (the copper features). To leave the least amount of burrs on the features
# the first pass is the one cutting all of the features, so it needs to be reversed the tool needs to travel on the right side of the features (this is called conventional milling)
# the other passes overlap preceding ones and cut the left over copper. It is better for them the first pass is the one cutting all of the features, so it needs to be reversed
# to cut on the right side of the left over copper i.e on the left side of the features. the other passes overlap preceding ones and cut the left over copper. It is better for them
try: to cut on the right side of the left over copper i.e on the left side of the features.
geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow)
except Exception as e: :param offset: Offset distance to be passed to the obj.isolation_geometry() method
log.debug('NonCopperClear.generate_envelope() --> %s' % str(e)) :type offset: float
return 'fail' :param invert: If to invert the direction of geometry (CW to CCW or reverse)
:type invert: int
:param geometry: Shapely Geometry for which t ogenerate envelope
:type geometry:
:param env_iso_type: type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
:type env_iso_type: int
:param follow: If the kind of isolation is a "follow" one
:type follow: bool
:param nr_passes: Number of passes
:type nr_passes: int
:return: The buffered geometry
:rtype: MultiPolygon or Polygon
"""
if follow:
geom = self.grb_obj.isolation_geometry(offset, geometry=geometry, follow=follow)
else:
try:
geom = self.grb_obj.isolation_geometry(offset, geometry=geometry, iso_type=env_iso_type,
passes=nr_passes)
except Exception as e:
log.debug('ToolIsolation.isolate().generate_envelope() --> %s' % str(e))
return 'fail'
if invert: if invert:
try: try:
try: pl = []
pl = [] for p in geom:
for p in geom: if p is not None:
if p is not None: if isinstance(p, Polygon):
if isinstance(p, Polygon): pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) elif isinstance(p, LinearRing):
elif isinstance(p, LinearRing): pl.append(Polygon(p.coords[::-1]))
pl.append(Polygon(p.coords[::-1])) geom = MultiPolygon(pl)
geom = MultiPolygon(pl) except TypeError:
except TypeError: if isinstance(geom, Polygon) and geom is not None:
if isinstance(geom, Polygon) and geom is not None: geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
geom = Polygon(geom.exterior.coords[::-1], geom.interiors) elif isinstance(geom, LinearRing) and geom is not None:
elif isinstance(geom, LinearRing) and geom is not None: geom = Polygon(geom.coords[::-1])
geom = Polygon(geom.coords[::-1]) else:
else: log.debug("ToolIsolation.generate_envelope() Error --> Unexpected Geometry %s" %
log.debug("NonCopperClear.generate_envelope() Error --> Unexpected Geometry %s" % type(geom))
type(geom))
except Exception as e: except Exception as e:
log.debug("NonCopperClear.generate_envelope() Error --> %s" % str(e)) log.debug("ToolIsolation.generate_envelope() Error --> %s" % str(e))
return 'fail' return 'fail'
return geom return geom

View File

@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta
================================================= =================================================
27.05.2020
- working on Isolation Tool: made to work the Isolation with multiple tools without rest machining
26.05.2020 26.05.2020
- working on Isolation Tool: made to work the tool parameters data to GUI and GUI to data - working on Isolation Tool: made to work the tool parameters data to GUI and GUI to data