diff --git a/CHANGELOG.md b/CHANGELOG.md index addafecd..cb85e2a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ CHANGELOG for FlatCAM Evo beta ================================================= +28.03.2024 + +- major refactoring: started to move the methods connected to the Edit menu to their own class to clean up the App mega class + 20.02.2024 - Punch Gerber Plugin: fixed issues with iterating ovr a MultiPolygon diff --git a/appEditors/AppExcEditor.py b/appEditors/appExcEditor.py similarity index 99% rename from appEditors/AppExcEditor.py rename to appEditors/appExcEditor.py index d4c9b9a9..c1bc4ab9 100644 --- a/appEditors/AppExcEditor.py +++ b/appEditors/appExcEditor.py @@ -17,7 +17,7 @@ from appEditors.exc_plugins.ExcResizePlugin import ExcResizeEditorTool from appEditors.exc_plugins.ExcCopyPlugin import ExcCopyEditorTool from appGUI.GUIElements import FCEntry, FCTable, FCDoubleSpinner, FCButton, FCLabel, GLay -from appEditors.AppGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, AppGeoEditor +from appEditors.appGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, AppGeoEditor from shapely import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, Point from shapely.geometry.base import BaseGeometry diff --git a/appEditors/appGCodeEditor.py b/appEditors/appGCodeEditor.py index 02288524..0a35b806 100644 --- a/appEditors/appGCodeEditor.py +++ b/appEditors/appGCodeEditor.py @@ -8,7 +8,7 @@ from PyQt6 import QtWidgets, QtCore, QtGui from PyQt6.QtCore import Qt -from appEditors.AppTextEditor import AppTextEditor +from appEditors.appTextEditor import AppTextEditor from appObjects.CNCJobObject import CNCJobObject from appGUI.GUIElements import FCTextArea, FCEntry, FCButton, FCTable, GLay, FCLabel diff --git a/appEditors/AppGeoEditor.py b/appEditors/appGeoEditor.py similarity index 100% rename from appEditors/AppGeoEditor.py rename to appEditors/appGeoEditor.py diff --git a/appEditors/AppGerberEditor.py b/appEditors/appGerberEditor.py similarity index 100% rename from appEditors/AppGerberEditor.py rename to appEditors/appGerberEditor.py diff --git a/appEditors/AppTextEditor.py b/appEditors/appTextEditor.py similarity index 100% rename from appEditors/AppTextEditor.py rename to appEditors/appTextEditor.py diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index ceb0070f..2b85af0c 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -21,7 +21,7 @@ from appGUI.preferences.excellon.ExcellonPreferencesUI import ExcellonPreference from appGUI.preferences.general.GeneralPreferencesUI import GeneralPreferencesUI from appGUI.preferences.geometry.GeometryPreferencesUI import GeometryPreferencesUI from appGUI.preferences.gerber.GerberPreferencesUI import GerberPreferencesUI -from appEditors.AppGeoEditor import FCShapeTool +from appEditors.appGeoEditor import FCShapeTool from matplotlib.backend_bases import KeyEvent as mpl_key_event diff --git a/appHandlers/appEdit.py b/appHandlers/appEdit.py new file mode 100644 index 00000000..89ed2ea6 --- /dev/null +++ b/appHandlers/appEdit.py @@ -0,0 +1,715 @@ + +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 diff --git a/appHandlers/AppIO.py b/appHandlers/appIO.py similarity index 99% rename from appHandlers/AppIO.py rename to appHandlers/appIO.py index 53fb8fbf..34f8720a 100644 --- a/appHandlers/AppIO.py +++ b/appHandlers/appIO.py @@ -2,9 +2,9 @@ from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt -from appEditors.AppExcEditor import AppExcEditor -from appEditors.AppGeoEditor import AppGeoEditor -from appEditors.AppGerberEditor import AppGerberEditor +from appEditors.appExcEditor import AppExcEditor +from appEditors.appGeoEditor import AppGeoEditor +from appEditors.appGerberEditor import AppGerberEditor from appGUI.GUIElements import FCFileSaveDialog, FCMessageBox from camlib import to_dict, dict2obj, ET, ParseError @@ -55,7 +55,7 @@ if '_' not in builtins.__dict__: _ = gettext.gettext -class AppIO(QtCore.QObject): +class appIO(QtCore.QObject): def __init__(self, app): """ A class that holds all the menu -> file handlers @@ -1304,7 +1304,7 @@ class AppIO(QtCore.QObject): try: root = ET.fromstring(svg_obj) except Exception as e: - self.log.debug("AppIO.save_pdf() -> Missing root node -> %s" % str(e)) + self.log.debug("appIO.save_pdf() -> Missing root node -> %s" % str(e)) self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed.")) return @@ -1393,7 +1393,7 @@ class AppIO(QtCore.QObject): renderPDF.draw(drawing, my_canvas, 0, 0) my_canvas.save() except Exception as e: - self.log.error("AppIO.save_pdf() --> PDF output --> %s" % str(e)) + self.log.error("appIO.save_pdf() --> PDF output --> %s" % str(e)) return 'fail' self.inform.emit('[success] %s: %s' % (_("PDF file saved to"), file_name)) @@ -2544,7 +2544,7 @@ class AppIO(QtCore.QObject): for obj in d['objs']: if 'cnc_tools' in obj or 'exc_cnc_tools' in obj or 'apertures' in obj: self.app.log.error( - 'AppIO.open_project() --> %s %s. %s' % + 'appIO.open_project() --> %s %s. %s' % ("Failed to open the CNCJob file:", str(obj['options']['name']), "Maybe it is an old project.")) found_older_project = True @@ -2645,7 +2645,7 @@ class AppIO(QtCore.QObject): try: new_obj.from_dict(obj) except Exception as except_error: - app_inst.log.error('AppIO.open_project() --> ' + str(except_error)) + app_inst.log.error('appIO.open_project() --> ' + str(except_error)) return 'fail' # make the 'obj_options' dict a LoudDict @@ -2655,7 +2655,7 @@ class AppIO(QtCore.QObject): except AttributeError: new_obj_options.update(new_obj.options) except Exception as except_error: - app_inst.log.error('AppIO.open_project() make a LoudDict--> ' + str(except_error)) + app_inst.log.error('appIO.open_project() make a LoudDict--> ' + str(except_error)) return 'fail' new_obj.obj_options = new_obj_options @@ -2692,7 +2692,7 @@ class AppIO(QtCore.QObject): float(tool): tool_dict for tool, tool_dict in list(new_obj.tools.items()) } except Exception as other_error_msg: - app_inst.log.error('AppIO.open_project() keys to int--> ' + str(other_error_msg)) + app_inst.log.error('appIO.open_project() keys to int--> ' + str(other_error_msg)) return 'fail' # ############################################################################################# @@ -2761,7 +2761,7 @@ class AppIO(QtCore.QObject): self.app.save_in_progress = True if from_tcl: - self.log.debug("AppIO.save_project() -> Project saved from TCL command.") + self.log.debug("appIO.save_project() -> Project saved from TCL command.") with self.app.proc_container.new(_("Saving Project ...")): # Capture the latest changes diff --git a/appMain.py b/appMain.py index 123b2c12..bf1540fe 100644 --- a/appMain.py +++ b/appMain.py @@ -35,7 +35,7 @@ import platform import re import subprocess -from shapely import Point, MultiPolygon, MultiLineString, Polygon, LinearRing, LineString +from shapely import Point, MultiPolygon, MultiLineString, Polygon from shapely.ops import unary_union from io import StringIO @@ -74,7 +74,8 @@ from appCommon.Common import ExclusionAreas from appCommon.Common import AppLogging from appCommon.RegisterFileKeywords import RegisterFK, Extensions, KeyWords -from appHandlers.AppIO import AppIO +from appHandlers.appIO import appIO +from appHandlers.appEdit import appEditor from Bookmark import BookmarkManager from appDatabase import ToolsDB2 @@ -100,10 +101,10 @@ from camlib import to_dict, Geometry, CNCjob from appPreProcessor import load_preprocessors # App appEditors -from appEditors.AppGeoEditor import AppGeoEditor -from appEditors.AppExcEditor import AppExcEditor -from appEditors.AppGerberEditor import AppGerberEditor -from appEditors.AppTextEditor import AppTextEditor +from appEditors.appGeoEditor import AppGeoEditor +from appEditors.appExcEditor import AppExcEditor +from appEditors.appGerberEditor import AppGerberEditor +from appEditors.appTextEditor import AppTextEditor from appEditors.appGCodeEditor import AppGCodeEditor # App Workers @@ -386,7 +387,7 @@ class App(QtCore.QObject): # store here the mouse cursor self.cursor = None - # while True no canvas context menu will be showen + # while True no canvas context menu will be shown self.inhibit_context_menu = False # Variable to store the GCODE that was edited @@ -986,8 +987,6 @@ class App(QtCore.QObject): # ########################################################################################################### # ############################################# Activity Monitor ############################################ # ########################################################################################################### - # self.activity_view = FlatCAMActivityView(app=self) - # self.ui.infobar.addWidget(self.activity_view) self.proc_container = FCVisibleProcessContainer(self.ui.activity_view) # ########################################################################################################### @@ -1080,7 +1079,7 @@ class App(QtCore.QObject): # Separate thread (Not worker) # Check for updates on startup but only if the user consent and the app is not in Beta version if (self.beta is False or self.beta is None) and self.options["global_version_check"] is True: - self.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version)) + self.log.info("Checking for updates in background (this is version %s)." % str(self.version)) # self.thr2 = QtCore.QThread() self.worker_task.emit({'fcn': self.version_check, 'params': []}) @@ -1126,7 +1125,8 @@ class App(QtCore.QObject): # ###################################### INSTANTIATE CLASSES THAT HOLD THE MENU HANDLERS #################### # ########################################################################################################### # ########################################################################################################### - self.f_handlers = AppIO(app=self) + self.f_handlers = appIO(app=self) + self.edit_class = appEditor(app=self) # this is calculated in the class above (somehow?) self.options["root_folder_path"] = self.app_home @@ -1905,25 +1905,25 @@ class App(QtCore.QObject): self.ui.menueditedit.triggered.connect(lambda: self.on_editing_start()) self.ui.menueditok.triggered.connect(lambda: self.on_editing_finished()) - self.ui.menuedit_join2geo.triggered.connect(self.on_edit_join) - self.ui.menuedit_join_exc2exc.triggered.connect(self.on_edit_join_exc) - self.ui.menuedit_join_grb2grb.triggered.connect(self.on_edit_join_grb) + self.ui.menuedit_join2geo.triggered.connect(self.edit_class.on_edit_join) + self.ui.menuedit_join_exc2exc.triggered.connect(self.edit_class.on_edit_join_exc) + self.ui.menuedit_join_grb2grb.triggered.connect(self.edit_class.on_edit_join_grb) - self.ui.menuedit_convert_sg2mg.triggered.connect(self.on_convert_singlegeo_to_multigeo) - self.ui.menuedit_convert_mg2sg.triggered.connect(self.on_convert_multigeo_to_singlegeo) + self.ui.menuedit_convert_sg2mg.triggered.connect(self.edit_class.on_convert_singlegeo_to_multigeo) + self.ui.menuedit_convert_mg2sg.triggered.connect(self.edit_class.on_convert_multigeo_to_singlegeo) self.ui.menueditdelete.triggered.connect(self.on_delete) self.ui.menueditcopyobject.triggered.connect(self.on_copy_command) - self.ui.menueditconvert_any2geo.triggered.connect(lambda: self.convert_any2geo()) - self.ui.menueditconvert_any2gerber.triggered.connect(lambda: self.convert_any2gerber()) - self.ui.menueditconvert_any2excellon.triggered.connect(lambda: self.convert_any2excellon()) + self.ui.menueditconvert_any2geo.triggered.connect(lambda: self.edit_class.convert_any2geo()) + self.ui.menueditconvert_any2gerber.triggered.connect(lambda: self.edit_class.convert_any2gerber()) + self.ui.menueditconvert_any2excellon.triggered.connect(lambda: self.edit_class.convert_any2excellon()) self.ui.menuedit_numeric_move.triggered.connect(lambda: self.on_numeric_move()) self.ui.menueditorigin.triggered.connect(self.on_set_origin) self.ui.menuedit_move2origin.triggered.connect(self.on_move2origin) - self.ui.menuedit_center_in_origin.triggered.connect(self.on_custom_origin) + self.ui.menuedit_center_in_origin.triggered.connect(self.edit_class.on_custom_origin) self.ui.menueditjump.triggered.connect(self.on_jump_to) self.ui.menueditlocate.triggered.connect(lambda: self.on_locate(obj=self.collection.get_active())) @@ -4029,208 +4029,12 @@ class App(QtCore.QObject): with open(config_file, 'w') as f: f.writelines(data) - 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[tooluid]['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.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.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.should_we_save = True - - 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.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 tooluid, 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.should_we_save = True - - self.inform.emit('[success] %s' % _("A Geometry object was converted to SingleGeo type.")) - def on_defaults_dict_change(self, field): """ - Called whenever a key changed in the self.options dictionary. It will set the required GUI element in the + Called whenever a key changed in the "self.options" dictionary. It will set the required GUI element in the Edit -> Preferences tab window. - :param field: the key of the self.options dictionary that was changed. + :param field: the key of the "self.options" dictionary that was changed. :return: None """ self.preferencesUiManager.defaults_write_form_field(field=field) @@ -4715,118 +4519,6 @@ class App(QtCore.QObject): worker_task() self.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.resource_location + '/origin3_32.png'), - choices=choices, - default_choice='c', - parent=self.ui) - if dia_box.ok is True: - try: - location_point = dia_box.location_point - except Exception: - return - else: - return - - def worker_task(): - with self.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.plotcanvas.fit_view() - - for obj in obj_list: - out_name = obj.obj_options["name"] - - if obj.kind == 'gerber': - obj.source_file = self.f_handlers.export_gerber( - obj_name=out_name, filename=None, local_use=obj, use_thread=False) - elif obj.kind == 'excellon': - obj.source_file = self.f_handlers.export_excellon( - obj_name=out_name, filename=None, local_use=obj, use_thread=False) - elif obj.kind == 'geometry': - obj.source_file = self.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.should_we_save = True - def on_jump_to(self, custom_location=None, fit_center=True): """ Jump to a location by setting the mouse cursor location. @@ -5292,379 +4984,6 @@ class App(QtCore.QObject): self.log.error( "App.on_rename_object() --> Could not rename the object in the list. --> %s" % str(e)) - 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: - oname = opt_key[len('geometry') + 1:] - default_data[oname] = 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("App.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("App.convert_any2geo --> 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("geometry", outname, initialize_from_excellon) - - if obj.kind == 'gerber': - self.app_obj.new_object("geometry", outname, 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 = {} - - apid = 10 - for tool in obj.tools: - apertures[apid] = { - '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[apid]['geometry'].append(deepcopy(new_el)) - - apid += 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("App.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("App.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 = {} - tooluid = 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_tooldias = [] - if tools: - for tool in tools: - if tools[tool] and 'tooldia' in tools[tool]: - current_tooldias.append(tools[tool]['tooldia']) - - if new_dia in current_tooldias: - 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[tooluid] = {} - tools[tooluid]['tooldia'] = new_dia - tools[tooluid]['drills'] = [new_drill] - tools[tooluid]['slots'] = [] - tools[tooluid]['solid_geometry'] = [new_drill_geo] - tooluid += 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 = {} - tooluid = 1 - digits = app_obj.decimals - - obj_init.solid_geometry = [] - - for apid in obj.tools: - if 'geometry' in obj.tools[apid]: - for geo_dict in obj.tools[apid]['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_tooldias = [] - if tools: - for tool in tools: - if tools[tool] and 'tooldia' in tools[tool]: - current_tooldias.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_tooldias: - 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[tooluid] = { - 'tooldia': new_dia, - 'drills': [new_drill], - 'slots': [], - 'solid_geometry': [new_drill_geo] - } - tooluid += 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_coords = list(geo_dict['follow'].coords) - - # slots can have only a start and stop point and no intermediate points - if len(geo_coords) != 2: - continue - - geo = geo_dict['solid'] - try: - new_dia = obj.tools[apid]['size'] - except Exception: - continue - - new_slot = (Point(geo_coords[0]), Point(geo_coords[1])) - new_slot_geo = geo - - current_tooldias = [] - if tools: - for tool in tools: - if tools[tool] and 'tooldia' in tools[tool]: - current_tooldias.append( - float('%.*f' % (self.decimals, tools[tool]['tooldia'])) - ) - - if float('%.*f' % (self.decimals, new_dia)) in current_tooldias: - 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[tooluid] = {} - tools[tooluid]['tooldia'] = new_dia - tools[tooluid]['drills'] = [] - tools[tooluid]['slots'] = [new_slot] - tools[tooluid]['solid_geometry'] = [new_slot_geo] - tooluid += 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=outname, local_use=obj_init, - filename=None, use_thread=False) - - if conv_obj_name is None: - if not self.collection.get_selected(): - self.log.warning("App.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"] - outname = "%s_conv" % str(obj_name) - try: - if obj.kind == 'gerber': - self.app_obj.new_object("excellon", outname, initialize_from_gerber) - elif obj.kind == 'geometry': - self.app_obj.new_object("excellon", outname, initialize_from_geometry) - else: - self.log.warning("App.convert_any2excellon --> This is no valid object for conversion.") - - except Exception as e: - return "Operation failed: %s" % str(e) - else: - outname = conv_obj_name - obj = self.collection.get_by_name(outname) - - try: - if obj.kind == 'gerber': - self.app_obj.new_object("excellon", outname, initialize_from_gerber) - elif obj.kind == 'geometry': - self.app_obj.new_object("excellon", outname, initialize_from_geometry) - else: - self.log.warning("App.convert_any2excellon --> This is no valid object for conversion.") - - except Exception as e: - self.log.error("App.convert_any2excellon() --> %s" % str(e)) - return "Operation failed: %s" % str(e) - def abort_all_tasks(self): """ Executed when a certain key combo is pressed (Ctrl+Alt+X). Will abort current task @@ -6044,7 +5363,7 @@ class App(QtCore.QObject): self.ui.toggle_coords(checked=self.options["global_coords_bar_show"]) self.ui.toggle_delta_coords(checked=self.options["global_delta_coords_bar_show"]) - def on_plot_area_tab_double_clicked(self, index: int): + def on_plot_area_tab_double_clicked(self): # tab_obj_name = self.ui.plot_tab_area.widget(index).objectName() # print(tab_obj_name) self.ui.on_toggle_notebook() diff --git a/appObjects/AppObject.py b/appObjects/AppObject.py index 8a831acd..c07e4d86 100644 --- a/appObjects/AppObject.py +++ b/appObjects/AppObject.py @@ -217,7 +217,9 @@ class AppObject(QtCore.QObject): obj.moveToThread(self.app.main_thread) if return_value == 'drill_gx2': - self.object_created.emit(obj, obj_plot, obj_autoselected, self.app.convert_any2excellon, [name]) + self.object_created.emit(obj, obj_plot, obj_autoselected, + self.app.edit_class.convert_any2excellon, + [name]) self.app.log.warning("Gerber X2 drill file detected. Converted to Excellon object.") self.app.inform.emit('[WARNING] %s' % _("Gerber X2 drill file detected. Converted to Excellon object.")) else: diff --git a/appObjects/CNCJobObject.py b/appObjects/CNCJobObject.py index 02c8e18e..7a412f40 100644 --- a/appObjects/CNCJobObject.py +++ b/appObjects/CNCJobObject.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtWidgets -from appEditors.AppTextEditor import AppTextEditor +from appEditors.appTextEditor import AppTextEditor from appObjects.AppObjectTemplate import FlatCAMObj, ObjectDeleted from appGUI.GUIElements import FCFileSaveDialog, FCCheckBox from appGUI.ObjectUI import CNCObjectUI diff --git a/appObjects/DocumentObject.py b/appObjects/DocumentObject.py index 3092ab0f..e7faf190 100644 --- a/appObjects/DocumentObject.py +++ b/appObjects/DocumentObject.py @@ -12,7 +12,7 @@ from PyQt6.QtCore import Qt -from appEditors.AppTextEditor import AppTextEditor +from appEditors.appTextEditor import AppTextEditor from appObjects.AppObjectTemplate import * from appGUI.ObjectUI import DocumentObjectUI diff --git a/appObjects/ScriptObject.py b/appObjects/ScriptObject.py index f1446abd..41b437c2 100644 --- a/appObjects/ScriptObject.py +++ b/appObjects/ScriptObject.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore -from appEditors.AppTextEditor import AppTextEditor +from appEditors.appTextEditor import AppTextEditor from appObjects.AppObjectTemplate import FlatCAMObj from appGUI.ObjectUI import ScriptObjectUI diff --git a/appPlugins/ToolDistance.py b/appPlugins/ToolDistance.py index 8e11f8f0..d5983774 100644 --- a/appPlugins/ToolDistance.py +++ b/appPlugins/ToolDistance.py @@ -10,7 +10,7 @@ from appTool import AppTool from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCEntry, FCCheckBox from appGUI.VisPyVisuals import ShapeCollection from camlib import AppRTreeStorage -from appEditors.AppGeoEditor import DrawToolShape +from appEditors.appGeoEditor import DrawToolShape import math import logging diff --git a/appPlugins/ToolLevelling.py b/appPlugins/ToolLevelling.py index 85225d72..fa9d2c21 100644 --- a/appPlugins/ToolLevelling.py +++ b/appPlugins/ToolLevelling.py @@ -28,7 +28,7 @@ import builtins from appObjects.AppObjectTemplate import ObjectDeleted from appGUI.VisPyVisuals import * from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy -from appEditors.AppTextEditor import AppTextEditor +from appEditors.appTextEditor import AppTextEditor from camlib import CNCjob diff --git a/appPlugins/ToolSolderPaste.py b/appPlugins/ToolSolderPaste.py index c67a20b7..f4a138b6 100644 --- a/appPlugins/ToolSolderPaste.py +++ b/appPlugins/ToolSolderPaste.py @@ -26,7 +26,7 @@ import builtins from appCommon.Common import LoudDict from camlib import distance -from appEditors.AppTextEditor import AppTextEditor +from appEditors.appTextEditor import AppTextEditor from io import StringIO @@ -223,7 +223,7 @@ class SolderPaste(AppTool): # either originally it was a string or not, xy_end will be made string dias_option = self.app.options["tools_solderpaste_tools"] - dias_option = re.sub('[()\[\]]', '', str(dias_option)) if dias_option else None + dias_option = re.sub(r'[()\[\]]', '', str(dias_option)) if dias_option else None try: dias = [float(eval(dia)) for dia in dias_option.split(",") if dia != ''] except Exception as err: