- 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.addItems(
[_("All"), _("Area Selection"), _("Reference Object")]
[_("All"), _("Area Selection"), _("Polygon Selection"), _("Reference Object")]
)
self.select_combo.setObjectName("i_selection")

View File

@ -491,6 +491,29 @@ class GerberObject(FlatCAMObj, Gerber):
return 'fail'
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):
if self.muted_ui:
return

View File

@ -507,7 +507,7 @@ class ToolIsolation(AppTool, Gerber):
)
self.select_combo = FCComboBox()
self.select_combo.addItems(
[_("All"), _("Area Selection"), _("Reference Object")]
[_("All"), _("Area Selection"), _("Polygon Selection"), _("Reference Object")]
)
self.select_combo.setObjectName("i_selection")
@ -666,6 +666,11 @@ class ToolIsolation(AppTool, Gerber):
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
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.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)
# 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.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.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.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())
outname = self.app.collection.get_by_name(self.object_combo.get_value()).options['name']
# init the working variables
self.default_data.clear()
self.default_data = {
"name": '_ncc',
"name": outname + '_iso',
"plot": self.app.defaults["geometry_plot"],
"cutz": float(self.cutz_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_radio.show()
# disable rest-machining for area painting
# disable rest-machining for area isolation
self.rest_cb.set_value(False)
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:
self.reference_combo.show()
self.reference_combo_label.show()
@ -1691,60 +1705,89 @@ class ToolIsolation(AppTool, Gerber):
def on_iso_button_click(self, *args):
obj = self.app.collection.get_active()
self.obj_name = self.object_combo.currentText()
self.iso_type = 2
if self.ui.iso_type_radio.get_value() == 'ext':
self.iso_type = 0
if self.ui.iso_type_radio.get_value() == 'int':
self.iso_type = 1
# Get source object.
try:
self.grb_obj = self.app.collection.get_by_name(self.obj_name)
except Exception as e:
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):
with self.app.proc_container.new(_("Isolating...")):
if self.ui.follow_cb.get_value() is True:
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()
self.isolate_handler(iso_obj)
iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single'
self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope)
self.app.worker_task.emit({'fcn': worker_task, 'params': [self.grb_obj, self.app]})
self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
def follow_geo(self, outname=None):
def follow_geo(self, followed_obj, outname):
"""
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
"""
# 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):
# Propagate options
follow_obj.options["cnctooldia"] = str(self.options["isotooldia"])
follow_obj.solid_geometry = self.follow_geometry
follow_obj.options["cnctooldia"] = str(tooldia)
follow_obj.solid_geometry = self.grb_obj.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)
# in the end toggle the visibility of the origin object so we can see the generated Geometry
followed_obj.ui.plot_cb.set_value(False)
follow_name = outname
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':
self.isolate(iso_type=iso_type)
else:
follow_state = self.iso_tools[tool]['data']['tools_iso_follow']
if follow_state:
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
if self.app.ui.grid_snap_btn.isChecked():
self.grid_status_memory = True
@ -1753,146 +1796,149 @@ class ToolIsolation(AppTool, Gerber):
self.grid_status_memory = False
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:
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:
self.app.plotcanvas.graph_event_disconnect(self.app.mr)
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,
milling_type=None, follow=None, plot=True):
def isolate(self, isolated_obj, geometry=None, limited_area=None, plot=True):
"""
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 geometry: specific geometry to isolate
:param dia: Tool diameter
:param passes: Number of tool widths to cut
:param overlap: Overlap between passes in fraction of tool diameter
:param outname: Base name of the output object
:param combine: Boolean: if to combine passes in one resulting object in case of multiple passes
:param milling_type: type of milling: conventional or climbing
:param follow: Boolean: if to generate a 'follow' geometry
:param plot: Boolean: if to plot the resulting geometry object
:param isolated_obj: Gerber object for which to generate the isolating routing geometry
:type isolated_obj: AppObjects.FlatCAMGerber.GerberObject
:param geometry: specific geometry to isolate
:type geometry: List of Shapely polygon
:param limited_area: if not None clear only this area
:type limited_area: Shapely Polygon or a list of them
:param plot: if to plot the resulting geometry object
:type plot: bool
:return: None
"""
if geometry is None:
work_geo = self.follow_geometry if follow is True else self.solid_geometry
else:
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"]
iso_name = isolated_obj.options["name"]
combine = self.combine_passes_cb.get_value()
tools_storage = self.iso_tools
if combine:
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
total_solid_geometry = []
def iso_init(geo_obj, app_obj):
# Propagate options
geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper()
for tool in tools_storage:
tool_dia = tools_storage[tool]['tooldia']
tool_type = tools_storage[tool]['tool_type']
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
if self.ui.tool_type_radio.get_value() == 'v':
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:
new_cutz = self.app.defaults['geometry_cutz']
new_vtipdia = self.app.defaults['geometry_vtipdia']
new_vtipangle = self.app.defaults['geometry_vtipangle']
tool_data.update({
"name": iso_name,
})
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']
})
solid_geo = []
for nr_pass in range(passes):
iso_offset = tool_dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * tool_dia)
geo_obj.tools = {}
geo_obj.tools['1'] = {}
geo_obj.tools.update({
'1': {
'tooldia': float(self.options["isotooldia"]),
# if milling type is climb then the move is counter-clockwise around features
mill_dir = 1 if milling_type == 'cl' else 0
iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
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_value': 0.0,
'type': _('Rough'),
'tool_type': tool_type,
'data': default_data,
'solid_geometry': geo_obj.solid_geometry
'data': tool_data,
'solid_geometry': deepcopy(solid_geo)
}
})
for nr_pass in range(passes):
iso_offset = dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * dia)
total_solid_geometry += solid_geo
# if milling type is climb then the move is counter-clockwise around features
mill_dir = 1 if milling_type == 'cl' else 0
geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
follow=follow, nr_passes=nr_pass)
def iso_init(geo_obj, app_obj):
geo_obj.options["cnctooldia"] = str(tool_dia)
if geom == 'fail':
app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
return 'fail'
geo_obj.solid_geometry.append(geom)
geo_obj.tools = dict(tools_storage)
geo_obj.solid_geometry = total_solid_geometry
# even if combine is checked, one pass is still single-geo
# update the geometry in the tools
geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry
if len(self.iso_tools) > 1:
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"
# 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
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:
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)
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):
# Propagate options
geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
if self.ui.tool_type_radio.get_value() == 'v':
geo_obj.tool_type = 'V'
else:
for tool in tools_storage:
tool_data = tools_storage[tool]['data']
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']
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:
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
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':
fc_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
return 'fail'
geo_obj.solid_geometry = geom
# 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
iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
follow=to_follow, nr_passes=i)
if iso_geo == 'fail':
self.app.inform.emit(
'[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
continue
# ############################################################
# ########## AREA SUBTRACTION ################################
# ############################################################
if self.ui.except_cb.get_value():
if iso_except:
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?
self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
if limited_area:
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
:param geo: target geometry from which to subtract
:param subtractor_geo: geometry that acts as subtractor
:param geo: target geometry from which to subtract
:param subtractor_geo: geometry that acts as subtractor
:return:
"""
new_geometry = []
@ -2070,7 +2112,7 @@ class ToolIsolation(AppTool, Gerber):
if subtractor_geo:
sub_union = cascaded_union(subtractor_geo)
else:
name = self.ui.obj_combo.currentText()
name = self.exc_obj_combo.currentText()
subtractor_obj = self.app.collection.get_by_name(name)
sub_union = cascaded_union(subtractor_obj.solid_geometry)
@ -2115,6 +2157,60 @@ class ToolIsolation(AppTool, Gerber):
new_geometry.append(new_geo)
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):
if self.app.is_legacy is False:
event_pos = event.pos
@ -2139,7 +2235,7 @@ class ToolIsolation(AppTool, Gerber):
curr_pos = (curr_pos[0], curr_pos[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:
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:
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:
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.on_mouse_click_release_over_plot)
@ -2189,7 +2287,7 @@ class ToolIsolation(AppTool, Gerber):
if self.poly_dict:
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()
else:
self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
@ -2383,12 +2481,8 @@ class ToolIsolation(AppTool, Gerber):
return
self.sel_rect = cascaded_union(self.sel_rect)
self.clear_copper(ncc_obj=self.grb_obj,
sel_obj=self.bound_obj,
ncctooldia=self.ncc_dia_list,
isotooldia=self.iso_dia_list,
outname=self.o_name)
self.isolate(isolated_obj=self.grb_obj, limited_area=self.sel_rect, plot=True)
self.sel_rect = []
# called on mouse move
def on_mouse_move(self, event):
@ -2689,7 +2783,7 @@ class ToolIsolation(AppTool, Gerber):
rest_machining_choice = self.rest_cb.get_value()
# 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
# ######################################################################################################
@ -2906,7 +3000,7 @@ class ToolIsolation(AppTool, Gerber):
else:
log.debug("There are no geometries in the cleared polygon.")
# 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)
# delete tools with empty geometry
@ -3221,7 +3315,7 @@ class ToolIsolation(AppTool, Gerber):
geo_obj.options["cnctooldia"] = str(tool)
# 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)
# check to see if geo_obj.tools is empty
@ -3290,40 +3384,61 @@ class ToolIsolation(AppTool, Gerber):
def poly2rings(poly):
return [poly.exterior] + [interior for interior in poly.interiors]
def generate_envelope(self, offset, invert, envelope_iso_type=2, follow=None):
# 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
# the tool needs to travel on the right side of the features (this is called conventional milling)
# the first pass is the one cutting all of the features, so it needs to be reversed
# the other passes overlap preceding ones and cut the left over copper. It is better for them
# to cut on the right side of the left over copper i.e on the left side of the features.
try:
geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow)
except Exception as e:
log.debug('NonCopperClear.generate_envelope() --> %s' % str(e))
return 'fail'
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
the tool needs to travel on the right side of the features (this is called conventional milling)
the first pass is the one cutting all of the features, so it needs to be reversed
the other passes overlap preceding ones and cut the left over copper. It is better for them
to cut on the right side of the left over copper i.e on the left side of the features.
:param offset: Offset distance to be passed to the obj.isolation_geometry() method
:type offset: float
: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:
try:
try:
pl = []
for p in geom:
if p is not None:
if isinstance(p, Polygon):
pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
elif isinstance(p, LinearRing):
pl.append(Polygon(p.coords[::-1]))
geom = MultiPolygon(pl)
except TypeError:
if isinstance(geom, Polygon) and geom is not None:
geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
elif isinstance(geom, LinearRing) and geom is not None:
geom = Polygon(geom.coords[::-1])
else:
log.debug("NonCopperClear.generate_envelope() Error --> Unexpected Geometry %s" %
type(geom))
pl = []
for p in geom:
if p is not None:
if isinstance(p, Polygon):
pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
elif isinstance(p, LinearRing):
pl.append(Polygon(p.coords[::-1]))
geom = MultiPolygon(pl)
except TypeError:
if isinstance(geom, Polygon) and geom is not None:
geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
elif isinstance(geom, LinearRing) and geom is not None:
geom = Polygon(geom.coords[::-1])
else:
log.debug("ToolIsolation.generate_envelope() Error --> Unexpected Geometry %s" %
type(geom))
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 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
- working on Isolation Tool: made to work the tool parameters data to GUI and GUI to data