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