Files
flatcam-wsl/appHandlers/appEdit.py

716 lines
29 KiB
Python

from PyQt6 import QtCore, QtGui
from appObjects.ObjectCollection import GeometryObject, GerberObject, ExcellonObject
from appGUI.GUIElements import DialogBoxChoice
from copy import deepcopy
from shapely import MultiPolygon, Polygon, LinearRing, LineString, Point, unary_union
# App Translation
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class appEditor(QtCore.QObject):
def __init__(self, app):
super(appEditor, self).__init__()
self.app = app
self.log = self.app.log
self.inform = self.app.inform
self.splash = self.app.splash
self.worker_task = self.app.worker_task
self.options = self.app.options
self.app_units = self.app.app_units
self.defaults = self.app.defaults
self.collection = self.app.collection
self.app_obj = self.app.app_obj
self.decimals = self.app.decimals
def convert_any2geo(self):
"""
Will convert any object out of Gerber, Excellon, Geometry to Geometry object.
:return:
"""
self.defaults.report_usage("convert_any2geo()")
# store here the default data for Geometry Data
default_data = {}
for opt_key, opt_val in self.options.items():
if opt_key.find('geometry' + "_") == 0:
o_name = opt_key[len('geometry') + 1:]
default_data[o_name] = self.options[opt_key]
else:
default_data[opt_key] = self.options[opt_key]
if isinstance(self.options["tools_mill_tooldia"], float):
tools_diameters = [self.options["tools_mill_tooldia"]]
else:
try:
dias = str(self.options["tools_mill_tooldia"]).strip('[').strip(']')
tools_string = dias.split(",")
tools_diameters = [eval(a) for a in tools_string if a != '']
except Exception as e:
self.log.error("appEditor.convert_any2geo() --> %s" % str(e))
return 'fail'
tools = {}
t_id = 0
for tooldia in tools_diameters:
t_id += 1
new_tool = {
'tooldia': tooldia,
'offset': 'Path',
'offset_value': 0.0,
'type': 'Rough',
'tool_type': 'C1',
'data': deepcopy(default_data),
'solid_geometry': []
}
tools[t_id] = deepcopy(new_tool)
def initialize_from_gerber(new_obj, app_obj):
app_obj.log.debug("Gerber converted to Geometry: %s" % str(obj.obj_options["name"]))
new_obj.solid_geometry = deepcopy(obj.solid_geometry)
try:
new_obj.follow_geometry = obj.follow_geometry
except AttributeError:
pass
new_obj.obj_options.update(deepcopy(default_data))
new_obj.obj_options["tools_mill_tooldia"] = tools_diameters[0] if tools_diameters else 0.0
new_obj.tools = deepcopy(tools)
for k in new_obj.tools:
new_obj.tools[k]['solid_geometry'] = deepcopy(obj.solid_geometry)
def initialize_from_excellon(new_obj, app_obj):
app_obj.log.debug("Excellon converted to Geometry: %s" % str(obj.obj_options["name"]))
solid_geo = []
for tool in obj.tools:
for geo in obj.tools[tool]['solid_geometry']:
solid_geo.append(geo)
new_obj.solid_geometry = deepcopy(solid_geo)
if not new_obj.solid_geometry:
app_obj.log("convert_any2geo() failed")
return 'fail'
new_obj.obj_options.update(deepcopy(default_data))
new_obj.obj_options["tools_mill_tooldia"] = tools_diameters[0] if tools_diameters else 0.0
new_obj.tools = deepcopy(tools)
for k in new_obj.tools:
new_obj.tools[k]['solid_geometry'] = deepcopy(obj.solid_geometry)
if not self.collection.get_selected():
self.log.warning("appEditor.convert_any2geo --> No object selected")
self.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
return
for obj in self.collection.get_selected():
out_name = '%s_conv' % obj.obj_options["name"]
try:
if obj.kind == 'excellon':
self.app_obj.new_object("geometry", out_name, initialize_from_excellon)
if obj.kind == 'gerber':
self.app_obj.new_object("geometry", out_name, initialize_from_gerber)
except Exception as e:
self.log.error("Convert any2geo operation failed: %s" % str(e))
def convert_any2gerber(self):
"""
Will convert any object out of Gerber, Excellon, Geometry to Gerber object.
:return:
"""
def initialize_from_geometry(obj_init, app_obj):
apertures = {
0: {
'size': 0.0,
'type': 'REG',
'geometry': []
}
}
for obj_orig in obj.solid_geometry:
new_elem = {'solid': obj_orig}
try:
new_elem['follow'] = obj_orig.exterior
except AttributeError:
pass
apertures[0]['geometry'].append(deepcopy(new_elem))
obj_init.solid_geometry = deepcopy(obj.solid_geometry)
obj_init.tools = deepcopy(apertures)
if not obj_init.tools:
app_obj.log("convert_any2gerber() failed")
return 'fail'
def initialize_from_excellon(obj_init, app_obj):
apertures = {}
aperture_id = 10
for tool in obj.tools:
apertures[aperture_id] = {
'size': float(obj.tools[tool]['tooldia']),
'type': 'C',
'geometry': []
}
for geo in obj.tools[tool]['solid_geometry']:
new_el = {
'solid': geo,
'follow': geo.exterior
}
apertures[aperture_id]['geometry'].append(deepcopy(new_el))
aperture_id += 1
# create solid_geometry
solid_geometry = []
for apid_val in apertures.values():
for geo_el in apid_val['geometry']:
solid_geometry.append(geo_el['solid']) # noqa
solid_geometry = MultiPolygon(solid_geometry)
solid_geometry = solid_geometry.buffer(0.0000001)
obj_init.solid_geometry = deepcopy(solid_geometry)
obj_init.tools = deepcopy(apertures)
if not obj_init.tools:
app_obj.log("convert_any2gerber() failed")
return 'fail'
if not self.collection.get_selected():
self.log.warning("appEditor.convert_any2gerber --> No object selected")
self.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
return
for obj in self.collection.get_selected():
outname = '%s_conv' % obj.obj_options["name"]
try:
if obj.kind == 'excellon':
self.app_obj.new_object("gerber", outname, initialize_from_excellon)
elif obj.kind == 'geometry':
self.app_obj.new_object("gerber", outname, initialize_from_geometry)
else:
self.log.warning("appEditor.convert_any2gerber --> This is no valid object for conversion.")
except Exception as e:
return "Operation failed: %s" % str(e)
def convert_any2excellon(self, conv_obj_name=None):
"""
Will convert any object out of Gerber, Excellon, Geometry to an Excellon object.
:param conv_obj_name: a FlatCAM object
:return:
"""
self.log.debug("Running conversion to Excellon object...")
def initialize_from_geometry(obj_init, app_obj):
tools = {}
tool_uid = 1
obj_init.solid_geometry = []
for tool in obj.tools:
print(obj.tools[tool])
for geo in obj.solid_geometry:
if not isinstance(geo, (Polygon, MultiPolygon, LinearRing)):
continue
minx, miny, maxx, maxy = geo.bounds
new_dia = min([maxx - minx, maxy - miny])
new_drill = geo.centroid
new_drill_geo = new_drill.buffer(new_dia / 2.0)
current_tool_dias = []
if tools:
for tool in tools:
if tools[tool] and 'tooldia' in tools[tool]:
current_tool_dias.append(tools[tool]['tooldia'])
if new_dia in current_tool_dias:
digits = app_obj.decimals
for tool in tools:
if app_obj.dec_format(tools[tool]["tooldia"], digits) == app_obj.dec_format(new_dia, digits):
tools[tool]['drills'].append(new_drill)
tools[tool]['solid_geometry'].append(deepcopy(new_drill_geo))
else:
tools[tool_uid] = {}
tools[tool_uid]['tooldia'] = new_dia
tools[tool_uid]['drills'] = [new_drill]
tools[tool_uid]['slots'] = []
tools[tool_uid]['solid_geometry'] = [new_drill_geo]
tool_uid += 1
try:
obj_init.solid_geometry.append(new_drill_geo)
except (TypeError, AttributeError):
obj_init.solid_geometry = [new_drill_geo]
obj_init.tools = deepcopy(tools)
obj_init.solid_geometry = unary_union(obj_init.solid_geometry)
if not obj_init.solid_geometry:
return 'fail'
def initialize_from_gerber(obj_init, app_obj):
tools = {}
tool_uid = 1
digits = app_obj.decimals
obj_init.solid_geometry = []
for aperture_id in obj.tools:
if 'geometry' in obj.tools[aperture_id]:
for geo_dict in obj.tools[aperture_id]['geometry']:
if 'follow' in geo_dict:
if isinstance(geo_dict['follow'], Point):
geo = geo_dict['solid']
minx, miny, maxx, maxy = geo.bounds
new_dia = min([maxx - minx, maxy - miny])
new_drill = geo.centroid
new_drill_geo = new_drill.buffer(new_dia / 2.0)
current_tool_dias = []
if tools:
for tool in tools:
if tools[tool] and 'tooldia' in tools[tool]:
current_tool_dias.append(
app_obj.dec_format(tools[tool]['tooldia'], digits)
)
formatted_new_dia = app_obj.dec_format(new_dia, digits)
if formatted_new_dia in current_tool_dias:
for tool in tools:
if app_obj.dec_format(tools[tool]["tooldia"], digits) == formatted_new_dia:
if new_drill not in tools[tool]['drills']:
tools[tool]['drills'].append(new_drill)
tools[tool]['solid_geometry'].append(deepcopy(new_drill_geo))
else:
tools[tool_uid] = {
'tooldia': new_dia,
'drills': [new_drill],
'slots': [],
'solid_geometry': [new_drill_geo]
}
tool_uid += 1
try:
obj_init.solid_geometry.append(new_drill_geo)
except (TypeError, AttributeError):
obj_init.solid_geometry = [new_drill_geo]
elif isinstance(geo_dict['follow'], LineString):
geo_coordinates = list(geo_dict['follow'].coords)
# slots can have only a start and stop point and no intermediate points
if len(geo_coordinates) != 2:
continue
geo = geo_dict['solid']
try:
new_dia = obj.tools[aperture_id]['size']
except Exception:
continue
new_slot = (Point(geo_coordinates[0]), Point(geo_coordinates[1]))
new_slot_geo = geo
current_tool_dias = []
if tools:
for tool in tools:
if tools[tool] and 'tooldia' in tools[tool]:
current_tool_dias.append(
float('%.*f' % (self.decimals, tools[tool]['tooldia']))
)
if float('%.*f' % (self.decimals, new_dia)) in current_tool_dias:
for tool in tools:
if float('%.*f' % (self.decimals, tools[tool]["tooldia"])) == float(
'%.*f' % (self.decimals, new_dia)):
if new_slot not in tools[tool]['slots']:
tools[tool]['slots'].append(new_slot)
tools[tool]['solid_geometry'].append(deepcopy(new_slot_geo))
else:
tools[tool_uid] = {}
tools[tool_uid]['tooldia'] = new_dia
tools[tool_uid]['drills'] = []
tools[tool_uid]['slots'] = [new_slot]
tools[tool_uid]['solid_geometry'] = [new_slot_geo]
tool_uid += 1
try:
obj_init.solid_geometry.append(new_slot_geo)
except (TypeError, AttributeError):
obj_init.solid_geometry = [new_slot_geo]
obj_init.tools = deepcopy(tools)
obj_init.solid_geometry = unary_union(obj_init.solid_geometry)
if not obj_init.solid_geometry:
return 'fail'
obj_init.source_file = app_obj.f_handlers.export_excellon(obj_name=out_name, local_use=obj_init,
filename=None, use_thread=False)
if conv_obj_name is None:
if not self.collection.get_selected():
self.log.warning("appEditor.convert_any2excellon--> No object selected")
self.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
return
for obj in self.collection.get_selected():
obj_name = obj.obj_options["name"]
out_name = "%s_conv" % str(obj_name)
try:
if obj.kind == 'gerber':
self.app_obj.new_object("excellon", out_name, initialize_from_gerber)
elif obj.kind == 'geometry':
self.app_obj.new_object("excellon", out_name, initialize_from_geometry)
else:
self.log.warning("appEditor.convert_any2excellon --> This is no valid object for conversion.")
except Exception as e:
return "Operation failed: %s" % str(e)
else:
out_name = conv_obj_name
obj = self.collection.get_by_name(out_name)
try:
if obj.kind == 'gerber':
self.app_obj.new_object("excellon", out_name, initialize_from_gerber)
elif obj.kind == 'geometry':
self.app_obj.new_object("excellon", out_name, initialize_from_geometry)
else:
self.log.warning("appEditor.convert_any2excellon --> This is no valid object for conversion.")
except Exception as e:
self.log.error("appEditor.convert_any2excellon() --> %s" % str(e))
return "Operation failed: %s" % str(e)
def on_convert_singlegeo_to_multigeo(self):
"""
Called for converting a Geometry object from single-geo to multi-geo.
Single-geo Geometry objects store their geometry data into self.solid_geometry.
Multi-geo Geometry objects store their geometry data into the `self.tools` dictionary, each key
(a tool actually) having as a value another dictionary. This value dictionary has
one of its keys 'solid_geometry' which holds the solid-geometry of that tool.
:return: None
"""
self.defaults.report_usage("on_convert_singlegeo_to_multigeo()")
obj = self.collection.get_active()
if obj is None:
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Select a Geometry Object and try again."))
return
if not isinstance(obj, GeometryObject):
self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Expected a GeometryObject, got"), type(obj)))
return
obj.multigeo = True
for tooluid, dict_value in obj.tools.items():
dict_value['solid_geometry'] = deepcopy(obj.solid_geometry)
if not isinstance(obj.solid_geometry, list):
obj.solid_geometry = [obj.solid_geometry]
# obj.solid_geometry[:] = []
obj.plot()
self.app.should_we_save = True
self.inform.emit('[success] %s' % _("A Geometry object was converted to MultiGeo type."))
def on_convert_multigeo_to_singlegeo(self):
"""
Called for converting a Geometry object from multi-geo to single-geo.
Single-geo Geometry objects store their geometry data into self.solid_geometry.
Multi-geo Geometry objects store their geometry data into the self.tools dictionary, each key (a tool actually)
having as a value another dictionary. This value dictionary has one of its keys 'solid_geometry' which holds
the solid-geometry of that tool.
:return: None
"""
self.defaults.report_usage("on_convert_multigeo_to_singlegeo()")
obj = self.collection.get_active()
if obj is None:
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Select a Geometry Object and try again."))
return
if not isinstance(obj, GeometryObject):
self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Expected a GeometryObject, got"), type(obj)))
return
obj.multigeo = False
total_solid_geometry = []
for tool_uid, dict_value in obj.tools.items():
total_solid_geometry += deepcopy(dict_value['solid_geometry'])
# clear the original geometry
if isinstance(dict_value['solid_geometry'], list):
dict_value['solid_geometry'][:] = []
else:
dict_value['solid_geometry'] = []
obj.solid_geometry = deepcopy(total_solid_geometry)
obj.plot()
self.app.should_we_save = True
self.inform.emit('[success] %s' % _("A Geometry object was converted to SingleGeo type."))
def on_edit_join(self, name=None):
"""
Callback for Edit->Join. Joins the selected geometry objects into
a new one.
:return: None
"""
self.defaults.report_usage("on_edit_join()")
obj_name_single = str(name) if name else "Combo_SingleGeo"
obj_name_multi = str(name) if name else "Combo_MultiGeo"
geo_type_set = set()
objs = self.collection.get_selected()
if len(objs) < 2:
self.inform.emit('[ERROR_NOTCL] %s: %d' %
(_("At least two objects are required for join. Objects currently selected"), len(objs)))
return 'fail'
for obj in objs:
geo_type_set.add(obj.multigeo)
# if len(geo_type_list) == 1 means that all list elements are the same
if len(geo_type_set) != 1:
self.inform.emit('[ERROR] %s' %
_("Failed join. The Geometry objects are of different types.\n"
"At least one is MultiGeo type and the other is SingleGeo type. A possibility is to "
"convert from one to another and retry joining \n"
"but in the case of converting from MultiGeo to SingleGeo, informations may be lost and "
"the result may not be what was expected. \n"
"Check the generated GCODE."))
return
fuse_tools = self.options["geometry_merge_fuse_tools"]
# if at least one True object is in the list then due of the previous check, all list elements are True objects
if True in geo_type_set:
def initialize(geo_obj, app):
GeometryObject.merge(geo_list=objs, geo_final=geo_obj, multi_geo=True, fuse_tools=fuse_tools,
log=app.log)
app.inform.emit('[success] %s.' % _("Geometry merging finished"))
# rename all the ['name] key in obj.tools[tool_uid]['data'] to the obj_name_multi
for v in geo_obj.tools.values():
v['data']['name'] = obj_name_multi
self.app_obj.new_object("geometry", obj_name_multi, initialize)
else:
def initialize(geo_obj, app):
GeometryObject.merge(geo_list=objs, geo_final=geo_obj, multi_geo=False, fuse_tools=fuse_tools,
log=app.log)
app.inform.emit('[success] %s.' % _("Geometry merging finished"))
# rename all the ['name] key in obj.tools[tooluid]['data'] to the obj_name_multi
for v in geo_obj.tools.values():
v['data']['name'] = obj_name_single
self.app_obj.new_object("geometry", obj_name_single, initialize)
self.app.should_we_save = True
def on_edit_join_exc(self):
"""
Callback for Edit->Join Excellon. Joins the selected Excellon objects into
a new Excellon.
:return: None
"""
self.defaults.report_usage("on_edit_join_exc()")
objs = self.collection.get_selected()
for obj in objs:
if not isinstance(obj, ExcellonObject):
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Excellon joining works only on Excellon objects."))
return
if len(objs) < 2:
self.inform.emit('[ERROR_NOTCL] %s: %d' %
(_("At least two objects are required for join. Objects currently selected"), len(objs)))
return 'fail'
fuse_tools = self.options["excellon_merge_fuse_tools"]
def initialize(exc_obj, app):
ExcellonObject.merge(exc_list=objs, exc_final=exc_obj, decimals=self.decimals, fuse_tools=fuse_tools,
log=app.log)
app.inform.emit('[success] %s.' % _("Excellon merging finished"))
self.app_obj.new_object("excellon", 'Combo_Excellon', initialize)
self.app.should_we_save = True
def on_edit_join_grb(self):
"""
Callback for Edit->Join Gerber. Joins the selected Gerber objects into
a new Gerber object.
:return: None
"""
self.defaults.report_usage("on_edit_join_grb()")
objs = self.collection.get_selected()
for obj in objs:
if not isinstance(obj, GerberObject):
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Gerber joining works only on Gerber objects."))
return
if len(objs) < 2:
self.inform.emit('[ERROR_NOTCL] %s: %d' %
(_("At least two objects are required for join. Objects currently selected"), len(objs)))
return 'fail'
def initialize(grb_obj, app):
GerberObject.merge(grb_list=objs, grb_final=grb_obj, app=self)
app.inform.emit('[success] %s.' % _("Gerber merging finished"))
self.app_obj.new_object("gerber", 'Combo_Gerber', initialize)
self.app.should_we_save = True
def on_custom_origin(self, use_thread=True):
"""
Move selected objects to be centered in certain standard locations of the object (corners and center).
:param use_thread: Control if to use threaded operation. Boolean.
:return:
"""
obj_list = self.collection.get_selected()
if not obj_list:
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected..."))
return
choices = [
{"label": _("Quadrant 2"), "value": "tl"},
{"label": _("Quadrant 1"), "value": "tr"},
{"label": _("Quadrant 3"), "value": "bl"},
{"label": _("Quadrant 4"), "value": "br"},
{"label": _("Center"), "value": "c"}
]
dia_box = DialogBoxChoice(title='%s:' % _("Custom Origin"),
icon=QtGui.QIcon(self.app.resource_location + '/origin3_32.png'),
choices=choices,
default_choice='c',
parent=self.app.ui)
if dia_box.ok is True:
try:
location_point = dia_box.location_point
except Exception:
return
else:
return
def worker_task():
with self.app.proc_container.new('%s ...' % _("Custom Origin")):
xminlist = []
yminlist = []
xmaxlist = []
ymaxlist = []
# first get a bounding box to fit all
for obj in obj_list:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
xmaxlist.append(xmax)
ymaxlist.append(ymax)
# get the minimum x,y for all objects selected
x0 = min(xminlist)
y0 = min(yminlist)
x1 = max(xmaxlist)
y1 = max(ymaxlist)
if location_point == 'bl':
location = (x0, y0)
elif location_point == 'tl':
location = (x0, y1)
elif location_point == 'br':
location = (x1, y0)
elif location_point == 'tr':
location = (x1, y1)
else:
# center
cx = x0 + abs((x1 - x0) / 2)
cy = y0 + abs((y1 - y0) / 2)
location = (cx, cy)
for obj in obj_list:
obj.offset((-location[0], -location[1]))
self.app_obj.object_changed.emit(obj)
# Update the object bounding box options
a, b, c, d = obj.bounds()
obj.obj_options['xmin'] = a
obj.obj_options['ymin'] = b
obj.obj_options['xmax'] = c
obj.obj_options['ymax'] = d
# make sure to update the Offset field in Properties Tab
try:
obj.set_offset_values()
except AttributeError:
# not all objects have this attribute
pass
for obj in obj_list:
obj.plot()
self.app.plotcanvas.fit_view()
for obj in obj_list:
out_name = obj.obj_options["name"]
if obj.kind == 'gerber':
obj.source_file = self.app.f_handlers.export_gerber(
obj_name=out_name, filename=None, local_use=obj, use_thread=False)
elif obj.kind == 'excellon':
obj.source_file = self.app.f_handlers.export_excellon(
obj_name=out_name, filename=None, local_use=obj, use_thread=False)
elif obj.kind == 'geometry':
obj.source_file = self.app.f_handlers.export_dxf(
obj_name=out_name, filename=None, local_use=obj, use_thread=False)
self.inform.emit('[success] %s...' % _('Origin set'))
if use_thread is True:
self.worker_task.emit({'fcn': worker_task, 'params': []})
else:
worker_task()
self.app.should_we_save = True