Sfoglia il codice sorgente

Merged in sopak/flatcam/kamil_combo1 (pull request #29)

PCB panelizing, aligning and gap geocutout shell commands
jpcgt 10 anni fa
parent
commit
c23450a68e
4 ha cambiato i file con 616 aggiunte e 19 eliminazioni
  1. 472 7
      FlatCAMApp.py
  2. 100 12
      FlatCAMObj.py
  3. 21 0
      ObjectCollection.py
  4. 23 0
      camlib.py

+ 472 - 7
FlatCAMApp.py

@@ -2134,6 +2134,58 @@ class App(QtCore.QObject):
 
 
             return 'Ok'
             return 'Ok'
 
 
+
+        def geocutout(name, *args):
+            """
+                subtract gaps from geometry, this will not create new object
+
+            :param name:
+            :param args:
+            :return:
+            """
+            a, kwa = h(*args)
+            types = {'dia': float,
+                     'gapsize': float,
+                     'gaps': str}
+
+            #way gaps wil be rendered:
+            # lr    - left + right
+            # tb    - top + bottom
+            # 4     - left + right +top + bottom
+            # 2lr   - 2*left + 2*right
+            # 2tb   - 2*top + 2*bottom
+            # 8     - 2*left + 2*right +2*top + 2*bottom
+
+            for key in kwa:
+                if key not in types:
+                    return 'Unknown parameter: %s' % key
+                kwa[key] = types[key](kwa[key])
+
+            try:
+                obj = self.collection.get_by_name(str(name))
+            except:
+                return "Could not retrieve object: %s" % name
+
+
+            #get min and max data for each object as we just cut rectangles across X or Y
+            xmin, ymin, xmax, ymax = obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
+            lenghtx = (xmax - xmin)
+            lenghty = (ymax - ymin)
+            gapsize = kwa['gapsize']+kwa['dia']/2
+            if kwa['gaps'] == '8' or kwa['gaps']=='2lr':
+                subtract_rectangle(name,xmin-gapsize,py-gapsize+lenghty/4,xmax+gapsize,py+gapsize+lenghty/4)
+                subtract_rectangle(name,xmin-gapsize,py-gapsize-lenghty/4,xmax+gapsize,py+gapsize-lenghty/4)
+            if kwa['gaps'] == '8' or kwa['gaps']=='2tb':
+                subtract_rectangle(name,px-gapsize+lenghtx/4,ymin-gapsize,px+gapsize+lenghtx/4,ymax+gapsize)
+                subtract_rectangle(name,px-gapsize-lenghtx/4,ymin-gapsize,px+gapsize-lenghtx/4,ymax+gapsize)
+            if kwa['gaps'] == '4' or kwa['gaps']=='lr':
+                subtract_rectangle(name,xmin-gapsize,py-gapsize,xmax+gapsize,py+gapsize)
+            if kwa['gaps'] == '4' or kwa['gaps']=='tb':
+                subtract_rectangle(name,px-gapsize,ymin-gapsize,px+gapsize,ymax+gapsize)
+            return 'Ok'
+
         def mirror(name, *args):
         def mirror(name, *args):
             a, kwa = h(*args)
             a, kwa = h(*args)
             types = {'box': str,
             types = {'box': str,
@@ -2202,6 +2254,194 @@ class App(QtCore.QObject):
 
 
             return 'Ok'
             return 'Ok'
 
 
+        def aligndrillgrid(outname, *args):
+            a, kwa = h(*args)
+            types = {'gridx': float,
+                     'gridy': float,
+                     'gridoffsetx': float,
+                     'gridoffsety': float,
+                     'columns':int,
+                     'rows':int,
+                     'dia': float
+                     }
+            for key in kwa:
+                if key not in types:
+                    return 'Unknown parameter: %s' % key
+                kwa[key] = types[key](kwa[key])
+
+
+            if 'columns' not in kwa or 'rows' not in kwa:
+                return "ERROR: Specify -columns and -rows"
+
+            if 'gridx' not in kwa or 'gridy' not in kwa:
+                return "ERROR: Specify -gridx and -gridy"
+
+            if 'dia' not in kwa:
+                return "ERROR: Specify -dia"
+
+            if 'gridoffsetx' not in kwa:
+                gridoffsetx=0
+            else:
+                gridoffsetx=kwa['gridoffsetx']
+
+            if 'gridoffsety' not in kwa:
+                gridoffsety=0
+            else:
+                gridoffsety=kwa['gridoffsety']
+
+
+            # Tools
+            tools = {"1": {"C": kwa['dia']}}
+
+            def aligndrillgrid_init_me(init_obj, app_obj):
+                drills = []
+                currenty=0
+                for row in range(kwa['rows']):
+                    currentx=0
+                    for col in range(kwa['columns']):
+                        point = Point(currentx+gridoffsetx,currenty+gridoffsety)
+                        drills.append({"point": point, "tool": "1"})
+                        currentx=currentx+kwa['gridx']
+                    currenty=currenty+kwa['gridy']
+                init_obj.tools = tools
+                init_obj.drills = drills
+                init_obj.create_geometry()
+
+            self.new_object("excellon", outname , aligndrillgrid_init_me)
+
+        def aligndrill(name, *args):
+            a, kwa = h(*args)
+            types = {'box': str,
+                     'axis': str,
+                     'holes': str,
+                     'grid': float,
+                     'minoffset': float,
+                     'gridoffset': float,
+                     'axisoffset': float,
+                     'dia': float,
+                     'dist': float}
+
+            for key in kwa:
+                if key not in types:
+                    return 'Unknown parameter: %s' % key
+                kwa[key] = types[key](kwa[key])
+
+            # Get source object.
+            try:
+                obj = self.collection.get_by_name(str(name))
+            except:
+                return "Could not retrieve object: %s" % name
+
+            if obj is None:
+                return "Object not found: %s" % name
+
+            if not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon):
+                return "ERROR: Only Gerber, Geometry and Excellon objects can be used."
+
+            # Axis
+            try:
+                axis = kwa['axis'].upper()
+            except KeyError:
+                return "ERROR: Specify -axis X or -axis Y"
+
+            if not ('holes' in kwa or ('grid' in kwa and 'gridoffset' in kwa)):
+                    return "ERROR: Specify -holes or -grid with -gridoffset "
+
+            if 'holes' in kwa:
+                try:
+                    holes = eval("[" + kwa['holes'] + "]")
+                except KeyError:
+                    return "ERROR: Wrong -holes format (X1,Y1),(X2,Y2)"
+
+            xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+            # Tools
+            tools = {"1": {"C": kwa['dia']}}
+
+            def alligndrill_init_me(init_obj, app_obj):
+
+                drills = []
+                if 'holes' in kwa:
+                    for hole in holes:
+                        point = Point(hole)
+                        point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+                        drills.append({"point": point, "tool": "1"})
+                        drills.append({"point": point_mirror, "tool": "1"})
+                else:
+                    if not 'box' in kwa:
+                        return "ERROR: -grid can be used only for -box"
+
+                    if 'axisoffset' in kwa:
+                        axisoffset=kwa['axisoffset']
+                    else:
+                        axisoffset=0
+
+                    #this  will align hole  to given aligngridoffset and minimal offset from pcb, based on selected axis
+                    if axis == "X":
+                        firstpoint=kwa['gridoffset']
+                        while (xmin-kwa['minoffset'])<firstpoint:
+                            firstpoint=firstpoint-kwa['grid']
+                        lastpoint=kwa['gridoffset']
+                        while (xmax+kwa['minoffset'])>lastpoint:
+                            lastpoint=lastpoint+kwa['grid']
+                        localHoles=(firstpoint,axisoffset),(lastpoint,axisoffset)
+                    else:
+                        firstpoint=kwa['gridoffset']
+                        while (ymin-kwa['minoffset'])<firstpoint:
+                            firstpoint=firstpoint-kwa['grid']
+                        lastpoint=kwa['gridoffset']
+                        while (ymax+kwa['minoffset'])>lastpoint:
+                            lastpoint=lastpoint+kwa['grid']
+                        localHoles=(axisoffset,firstpoint),(axisoffset,lastpoint)
+
+                    for hole in localHoles:
+                        point = Point(hole)
+                        point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+                        drills.append({"point": point, "tool": "1"})
+                        drills.append({"point": point_mirror, "tool": "1"})
+
+                init_obj.tools = tools
+                init_obj.drills = drills
+                init_obj.create_geometry()
+
+            # Box
+            if 'box' in kwa:
+                try:
+                    box = self.collection.get_by_name(kwa['box'])
+                except:
+                    return "Could not retrieve object box: %s" % kwa['box']
+
+                if box is None:
+                    return "Object box not found: %s" % kwa['box']
+
+                try:
+                    xmin, ymin, xmax, ymax = box.bounds()
+                    px = 0.5 * (xmin + xmax)
+                    py = 0.5 * (ymin + ymax)
+
+                    obj.app.new_object("excellon", name + "_aligndrill", alligndrill_init_me)
+
+                except Exception, e:
+                    return "Operation failed: %s" % str(e)
+
+            else:
+                try:
+                    dist = float(kwa['dist'])
+                except KeyError:
+                    dist = 0.0
+                except ValueError:
+                    return "Invalid distance: %s" % kwa['dist']
+
+                try:
+                    px=dist
+                    py=dist
+                    obj.app.new_object("excellon", name + "_alligndrill", alligndrill_init_me)
+                except Exception, e:
+                    return "Operation failed: %s" % str(e)
+
+            return 'Ok'
+
+
         def drillcncjob(name, *args):
         def drillcncjob(name, *args):
             a, kwa = h(*args)
             a, kwa = h(*args)
             types = {'tools': str,
             types = {'tools': str,
@@ -2230,26 +2470,26 @@ class App(QtCore.QObject):
                 return "ERROR: Only Excellon objects can be drilled."
                 return "ERROR: Only Excellon objects can be drilled."
 
 
             try:
             try:
-              
+
                 # Get the tools from the list
                 # Get the tools from the list
                 job_name = kwa["outname"]
                 job_name = kwa["outname"]
-        
+
                 # Object initialization function for app.new_object()
                 # Object initialization function for app.new_object()
                 def job_init(job_obj, app_obj):
                 def job_init(job_obj, app_obj):
                     assert isinstance(job_obj, FlatCAMCNCjob), \
                     assert isinstance(job_obj, FlatCAMCNCjob), \
                         "Initializer expected FlatCAMCNCjob, got %s" % type(job_obj)
                         "Initializer expected FlatCAMCNCjob, got %s" % type(job_obj)
-                    
+
                     job_obj.z_cut = kwa["drillz"]
                     job_obj.z_cut = kwa["drillz"]
                     job_obj.z_move = kwa["travelz"]
                     job_obj.z_move = kwa["travelz"]
                     job_obj.feedrate = kwa["feedrate"]
                     job_obj.feedrate = kwa["feedrate"]
                     job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None
                     job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None
                     toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False
                     toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False
                     job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange)
                     job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange)
-                    
+
                     job_obj.gcode_parse()
                     job_obj.gcode_parse()
-                    
+
                     job_obj.create_geometry()
                     job_obj.create_geometry()
-        
+
                 obj.app.new_object("cncjob", job_name, job_init)
                 obj.app.new_object("cncjob", job_name, job_init)
 
 
             except Exception, e:
             except Exception, e:
@@ -2374,7 +2614,7 @@ class App(QtCore.QObject):
             types = {'dia': float,
             types = {'dia': float,
                      'passes': int,
                      'passes': int,
                      'overlap': float,
                      'overlap': float,
-                     'outname': str, 
+                     'outname': str,
                      'combine': int}
                      'combine': int}
 
 
             for key in kwa:
             for key in kwa:
@@ -2492,6 +2732,28 @@ class App(QtCore.QObject):
             return add_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y,
             return add_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y,
                             topright_x, topright_y, topright_x, botleft_y)
                             topright_x, topright_y, topright_x, botleft_y)
 
 
+        def subtract_poly(obj_name, *args):
+            if len(args) % 2 != 0:
+                return "Incomplete coordinate."
+
+            points = [[float(args[2*i]), float(args[2*i+1])] for i in range(len(args)/2)]
+
+            try:
+                obj = self.collection.get_by_name(str(obj_name))
+            except:
+                return "Could not retrieve object: %s" % obj_name
+            if obj is None:
+                return "Object not found: %s" % obj_name
+
+            obj.subtract_polygon(points)
+            obj.plot()
+
+            return "OK."
+
+        def subtract_rectangle(obj_name, botleft_x, botleft_y, topright_x, topright_y):
+            return subtract_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y,
+                            topright_x, topright_y, topright_x, botleft_y)
+
         def add_circle(obj_name, center_x, center_y, radius):
         def add_circle(obj_name, center_x, center_y, radius):
             try:
             try:
                 obj = self.collection.get_by_name(str(obj_name))
                 obj = self.collection.get_by_name(str(obj_name))
@@ -2510,6 +2772,8 @@ class App(QtCore.QObject):
 
 
         def delete(obj_name):
         def delete(obj_name):
             try:
             try:
+                #deselect all  to avoid  delete selected object when run  delete  from  shell
+                self.collection.set_all_inactive()
                 self.collection.set_active(str(obj_name))
                 self.collection.set_active(str(obj_name))
                 self.on_delete()
                 self.on_delete()
             except Exception, e:
             except Exception, e:
@@ -2541,6 +2805,121 @@ class App(QtCore.QObject):
             if objs is not None:
             if objs is not None:
                 self.new_object("geometry", obj_name, initialize)
                 self.new_object("geometry", obj_name, initialize)
 
 
+        def join_excellons(obj_name, *obj_names):
+            objs = []
+            for obj_n in obj_names:
+                obj = self.collection.get_by_name(str(obj_n))
+                if obj is None:
+                    return "Object not found: %s" % obj_n
+                else:
+                    objs.append(obj)
+
+            def initialize(obj, app):
+                FlatCAMExcellon.merge(objs, obj)
+
+            if objs is not None:
+                self.new_object("excellon", obj_name, initialize)
+
+        def panelize(name, *args):
+            a, kwa = h(*args)
+            types = {'box': str,
+                     'spacing_columns': float,
+                     'spacing_rows': float,
+                     'columns': int,
+                     'rows': int,
+                     'outname': str}
+
+            for key in kwa:
+                if key not in types:
+                    return 'Unknown parameter: %s' % key
+                kwa[key] = types[key](kwa[key])
+
+            # Get source object.
+            try:
+                obj = self.collection.get_by_name(str(name))
+            except:
+                return "Could not retrieve object: %s" % name
+
+            if obj is None:
+                return "Object not found: %s" % name
+
+            if 'box' in kwa:
+                boxname=kwa['box']
+                try:
+                    box = self.collection.get_by_name(boxname)
+                except:
+                    return "Could not retrieve object: %s" % name
+            else:
+                box=obj
+
+            if 'columns' not in kwa or 'rows' not in kwa:
+                return "ERROR: Specify -columns and -rows"
+
+            if 'outname' in kwa:
+                outname=kwa['outname']
+            else:
+                outname=name+'_panelized'
+
+            if 'spacing_columns' in kwa:
+                spacing_columns=kwa['spacing_columns']
+            else:
+                spacing_columns=5
+
+            if 'spacing_rows' in kwa:
+                spacing_rows=kwa['spacing_rows']
+            else:
+                spacing_rows=5
+
+            xmin, ymin, xmax, ymax = box.bounds()
+            lenghtx = xmax-xmin+spacing_columns
+            lenghty = ymax-ymin+spacing_rows
+
+            currenty=0
+            def initialize_local(obj_init, app):
+                obj_init.solid_geometry = obj.solid_geometry
+                obj_init.offset([float(currentx), float(currenty)]),
+
+            def initialize_local_excellon(obj_init, app):
+                FlatCAMExcellon.merge(obj, obj_init)
+                obj_init.offset([float(currentx), float(currenty)]),
+
+            def initialize_geometry(obj_init, app):
+                FlatCAMGeometry.merge(objs, obj_init)
+
+            def initialize_excellon(obj_init, app):
+                FlatCAMExcellon.merge(objs, obj_init)
+
+            objs=[]
+            if obj is not None:
+
+                for row in range(kwa['rows']):
+                    currentx=0
+                    for col in range(kwa['columns']):
+                        local_outname=outname+".tmp."+str(col)+"."+str(row)
+                        if isinstance(obj, FlatCAMExcellon):
+                            new_obj=self.new_object("excellon", local_outname, initialize_local_excellon)
+                        else:
+                            new_obj=self.new_object("geometry", local_outname, initialize_local)
+                        objs.append(new_obj)
+                        currentx=currentx+lenghtx
+                    currenty=currenty+lenghty
+
+                if isinstance(obj, FlatCAMExcellon):
+                    self.new_object("excellon", outname, initialize_excellon)
+                else:
+                    self.new_object("geometry", outname, initialize_geometry)
+
+                #deselect all  to avoid  delete selected object when run  delete  from  shell
+                self.collection.set_all_inactive()
+                for delobj in objs:
+                    self.collection.set_active(delobj.options['name'])
+                    self.on_delete()
+
+            else:
+                return "ERROR: obj is None"
+
+            return "Ok"
+
         def make_docs():
         def make_docs():
             output = ''
             output = ''
             import collections
             import collections
@@ -2721,6 +3100,30 @@ class App(QtCore.QObject):
                         "   gapsize: size of gap\n" +
                         "   gapsize: size of gap\n" +
                         "   gaps: type of gaps"
                         "   gaps: type of gaps"
             },
             },
+            'geocutout': {
+                'fcn': geocutout,
+                'help': "Cut holding gaps from geometry.\n" +
+                        "> geocutout <name> [-dia <3.0 (float)>] [-margin <0.0 (float)>] [-gapsize <0.5 (float)>] [-gaps <lr (8|4|tb|lr|2tb|2lr)>]\n" +
+                        "   name: Name of the geometry object\n" +
+                        "   dia: Tool diameter\n" +
+                        "   margin: Margin over bounds\n" +
+                        "   gapsize: size of gap\n" +
+                        "   gaps: type of gaps\n" +
+                        "\n" +
+                        "   example:\n" +
+                        "\n" +
+                        "      #isolate margin for example from fritzing arduino shield or any svg etc\n" +
+                        "      isolate BCu_margin -dia 3 -overlap 1\n" +
+                        "\n" +
+                        "      #create exteriors from isolated object\n" +
+                        "      exteriors BCu_margin_iso -outname BCu_margin_iso_exterior\n" +
+                        "\n" +
+                        "      #delete isolated object if you dond need id anymore\n" +
+                        "      delete BCu_margin_iso\n" +
+                        "\n" +
+                        "      #finally cut holding gaps\n" +
+                        "      geocutout BCu_margin_iso_exterior -dia 3 -gapsize 0.6 -gaps 4\n"
+            },
             'mirror': {
             'mirror': {
                 'fcn': mirror,
                 'fcn': mirror,
                 'help': "Mirror a layer.\n" +
                 'help': "Mirror a layer.\n" +
@@ -2730,6 +3133,33 @@ class App(QtCore.QObject):
                         "   axis: Mirror axis parallel to the X or Y axis.\n" +
                         "   axis: Mirror axis parallel to the X or Y axis.\n" +
                         "   dist: Distance of the mirror axis to the X or Y axis."
                         "   dist: Distance of the mirror axis to the X or Y axis."
             },
             },
+            'aligndrillgrid': {
+                'fcn': aligndrillgrid,
+                'help': "Create excellon with drills for aligment grid.\n" +
+                        "> aligndrillgrid <outname> [-dia <3.0 (float)>] -gridx <float> [-gridoffsetx <0 (float)>] -gridy <float> [-gridoffsety <0 (float)>] -columns <int> -rows <int>\n" +
+                        "   outname: Name of the object to create.\n" +
+                        "   dia: Tool diameter\n" +
+                        "   gridx: grid size in X axis\n" +
+                        "   gridoffsetx: move grid  from origin\n" +
+                        "   gridy: grid size in Y axis\n" +
+                        "   gridoffsety: move grid  from origin\n" +
+                        "   colums: grid holes on X axis\n" +
+                        "   rows: grid holes on Y axis\n"
+            },
+            'aligndrill': {
+                'fcn': aligndrill,
+                'help': "Create excellon with drills for aligment.\n" +
+                        "> aligndrill <name> [-dia <3.0 (float)>] -axis <X|Y> [-box <nameOfBox> -minoffset <float> [-grid <10 (float)> -gridoffset <5 (float)> [-axisoffset <0 (float)>]] | -dist <number>]\n" +
+                        "   name: Name of the object (Gerber or Excellon) to mirror.\n" +
+                        "   dia: Tool diameter\n" +
+                        "   box: Name of object which act as box (cutout for example.)\n" +
+                        "   grid: aligning  to grid, for thouse, who have aligning pins inside table in grid (-5,0),(5,0),(15,0)..." +
+                        "   gridoffset: offset of grid from 0 position" +
+                        "   minoffset: min and max distance between align hole and pcb" +
+                        "   axisoffset: offset on second axis before aligment holes" +
+                        "   axis: Mirror axis parallel to the X or Y axis.\n" +
+                        "   dist: Distance of the mirror axis to the X or Y axis."
+            },
             'exteriors': {
             'exteriors': {
                 'fcn': exteriors,
                 'fcn': exteriors,
                 'help': "Get exteriors of polygons.\n" +
                 'help': "Get exteriors of polygons.\n" +
@@ -2827,6 +3257,13 @@ class App(QtCore.QObject):
                         '   name: Name of the geometry object to which to append the polygon.\n' +
                         '   name: Name of the geometry object to which to append the polygon.\n' +
                         '   xi, yi: Coordinates of points in the polygon.'
                         '   xi, yi: Coordinates of points in the polygon.'
             },
             },
+            'subtract_poly': {
+                'fcn': subtract_poly,
+                'help': 'Subtract polygon from the given Geometry object.\n' +
+                        '> subtract_poly <name> <x0> <y0> <x1> <y1> <x2> <y2> [x3 y3 [...]]\n' +
+                        '   name: Name of the geometry object, which will be  sutracted.\n' +
+                        '   xi, yi: Coordinates of points in the polygon.'
+            },
             'delete': {
             'delete': {
                 'fcn': delete,
                 'fcn': delete,
                 'help': 'Deletes the give object.\n' +
                 'help': 'Deletes the give object.\n' +
@@ -2850,6 +3287,34 @@ class App(QtCore.QObject):
                         '   out_name: Name of the new geometry object.' +
                         '   out_name: Name of the new geometry object.' +
                         '   obj_name_0... names of the objects to join'
                         '   obj_name_0... names of the objects to join'
             },
             },
+            'join_excellons': {
+                'fcn': join_excellons,
+                'help': 'Runs a merge operation (join) on the excellon ' +
+                        'objects.' +
+                        '> join_excellons <out_name> <obj_name_0>....\n' +
+                        '   out_name: Name of the new excellon object.' +
+                        '   obj_name_0... names of the objects to join'
+            },
+            'panelize': {
+                'fcn': panelize,
+                'help': "Simple panelize geometries.\n" +
+                        "> panelize <name> [-box <nameOfBox>]  [-spacing_columns <5 (float)>] [-spacing_rows <5 (float)>] -columns <int> -rows <int>  [-outname <n>]\n" +
+                        "   name: Name of the object to panelize.\n" +
+                        "   box: Name of object which act as box (cutout for example.) for cutout boundary. Object from name is used if not specified.\n" +
+                        "   spacing_columns: spacing between columns\n"+
+                        "   spacing_rows: spacing between rows\n"+
+                        "   columns: number of columns\n"+
+                        "   rows: number of rows\n"+
+                        "   outname: Name of the new geometry object."
+            },
+            'subtract_rect': {
+                'fcn': subtract_rectangle,
+                'help': 'Subtract rectange from the given Geometry object.\n' +
+                        '> subtract_rect <name> <botleft_x> <botleft_y> <topright_x> <topright_y>\n' +
+                        '   name: Name of the geometry object, which will be subtracted.\n' +
+                        '   botleft_x, botleft_y: Coordinates of the bottom left corner.\n' +
+                        '   topright_x, topright_y Coordinates of the top right corner.'
+            },
             'add_rect': {
             'add_rect': {
                 'fcn': add_rectangle,
                 'fcn': add_rectangle,
                 'help': 'Creates a rectange in the given Geometry object.\n' +
                 'help': 'Creates a rectange in the given Geometry object.\n' +

+ 100 - 12
FlatCAMObj.py

@@ -123,8 +123,12 @@ class FlatCAMObj(QtCore.QObject):
 
 
         :return: None
         :return: None
         """
         """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.to_form()")
         for option in self.options:
         for option in self.options:
-            self.set_form_item(option)
+            try:
+                self.set_form_item(option)
+            except:
+                self.app.log.warning("Unexpected error:", sys.exc_info())
 
 
     def read_form(self):
     def read_form(self):
         """
         """
@@ -135,7 +139,11 @@ class FlatCAMObj(QtCore.QObject):
         """
         """
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
         for option in self.options:
         for option in self.options:
-            self.read_form_item(option)
+            try:
+                self.read_form_item(option)
+            except:
+                self.app.log.warning("Unexpected error:", sys.exc_info())
+
 
 
     def build_ui(self):
     def build_ui(self):
         """
         """
@@ -191,11 +199,16 @@ class FlatCAMObj(QtCore.QObject):
         :type option: str
         :type option: str
         :return: None
         :return: None
         """
         """
-
-        try:
-            self.options[option] = self.form_fields[option].get_value()
-        except KeyError:
-            self.app.log.warning("Failed to read option from field: %s" % option)
+        #try read field only when option have equivalent in form_fields
+        if option in self.form_fields:
+            option_type=type(self.options[option])
+            try:
+                value=self.form_fields[option].get_value()
+            #catch per option as it was ignored anyway, also when syntax error (probably uninitialized field),don't read either.
+            except (KeyError,SyntaxError):
+                self.app.log.warning("Failed to read option from field: %s" % option)
+        else:
+            self.app.log.warning("Form fied does not exists: %s" % option)
 
 
     def plot(self):
     def plot(self):
         """
         """
@@ -630,6 +643,76 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # from predecessors.
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
         self.ser_attrs += ['options', 'kind']
 
 
+    @staticmethod
+    def merge(exc_list, exc_final):
+        """
+        Merge excellons in exc_list into exc_final.
+        Options are allways copied from source .
+
+        Tools are also merged, if  name for  tool is same and   size differs, then as name is used next available  number from both lists
+
+        if only one object is  specified in exc_list then this acts  as copy only
+
+        :param exc_list: List or one object of FlatCAMExcellon Objects to join.
+        :param exc_final: Destination FlatCAMExcellon object.
+        :return: None
+        """
+
+        if type(exc_list) is not list:
+            exc_list_real= list()
+            exc_list_real.append(exc_list)
+        else:
+            exc_list_real=exc_list
+
+        for exc in exc_list_real:
+            # Expand lists
+            if type(exc) is list:
+                FlatCAMExcellon.merge(exc, exc_final)
+            # If not list, merge excellons
+            else:
+
+                #    TODO: I realize forms does not save values into options , when  object is deselected
+                #    leave this  here for future use
+                #    this  reinitialize options based on forms, all steps may not be necessary
+                #    exc.app.collection.set_active(exc.options['name'])
+                #    exc.to_form()
+                #    exc.read_form()
+                for option in exc.options:
+                    if option is not 'name':
+                        try:
+                            exc_final.options[option] = exc.options[option]
+                        except:
+                            exc.app.log.warning("Failed to copy option.",option)
+
+                #deep copy of all drills,to avoid any references
+                for drill in exc.drills:
+                    point = Point(drill['point'].x,drill['point'].y)
+                    exc_final.drills.append({"point": point, "tool": drill['tool']})
+                toolsrework=dict()
+                max_numeric_tool=0
+                for toolname in exc.tools.iterkeys():
+                    numeric_tool=int(toolname)
+                    if numeric_tool>max_numeric_tool:
+                        max_numeric_tool=numeric_tool
+                    toolsrework[exc.tools[toolname]['C']]=toolname
+
+                #exc_final as last because names from final tools will be used
+                for toolname in exc_final.tools.iterkeys():
+                    numeric_tool=int(toolname)
+                    if numeric_tool>max_numeric_tool:
+                        max_numeric_tool=numeric_tool
+                    toolsrework[exc_final.tools[toolname]['C']]=toolname
+
+                for toolvalues in toolsrework.iterkeys():
+                    if toolsrework[toolvalues] in exc_final.tools:
+                        if exc_final.tools[toolsrework[toolvalues]]!={"C": toolvalues}:
+                            exc_final.tools[str(max_numeric_tool+1)]={"C": toolvalues}
+                    else:
+                        exc_final.tools[toolsrework[toolvalues]]={"C": toolvalues}
+                #this value  was not co
+                exc_final.zeros=exc.zeros
+                exc_final.create_geometry()
+
     def build_ui(self):
     def build_ui(self):
         FlatCAMObj.build_ui(self)
         FlatCAMObj.build_ui(self)
 
 
@@ -1264,11 +1347,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
         dx, dy = vect
         dx, dy = vect
 
 
-        if type(self.solid_geometry) == list:
-            self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
-                                   for g in self.solid_geometry]
-        else:
-            self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
+        def translate_recursion(geom):
+            if type(geom) == list:
+                geoms=list()
+                for local_geom in geom:
+                    geoms.append(translate_recursion(local_geom))
+                return geoms
+            else:
+                return  affinity.translate(geom, xoff=dx, yoff=dy)
+
+        self.solid_geometry=translate_recursion(self.solid_geometry)
 
 
     def convert_units(self, units):
     def convert_units(self, units):
         factor = Geometry.convert_units(self, units)
         factor = Geometry.convert_units(self, units)

+ 21 - 0
ObjectCollection.py

@@ -244,6 +244,27 @@ class ObjectCollection(QtCore.QAbstractListModel):
         iobj = self.createIndex(self.get_names().index(name), 0)  # Column 0
         iobj = self.createIndex(self.get_names().index(name), 0)  # Column 0
         self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Select)
         self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Select)
 
 
+    def set_inactive(self, name):
+        """
+        Unselect object by name from the project list. This triggers the
+        list_selection_changed event and call on_list_selection_changed.
+
+        :param name: Name of the FlatCAM Object
+        :return: None
+        """
+        iobj = self.createIndex(self.get_names().index(name), 0)  # Column 0
+        self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Deselect)
+
+    def set_all_inactive(self):
+        """
+        Unselect all objects from the project list. This triggers the
+        list_selection_changed event and call on_list_selection_changed.
+
+        :return: None
+        """
+        for name in self.get_names():
+            self.set_inactive(name)
+
     def on_list_selection_change(self, current, previous):
     def on_list_selection_change(self, current, previous):
         FlatCAMApp.App.log.debug("on_list_selection_change()")
         FlatCAMApp.App.log.debug("on_list_selection_change()")
         FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
         FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))

+ 23 - 0
camlib.py

@@ -136,6 +136,29 @@ class Geometry(object):
             log.error("Failed to run union on polygons.")
             log.error("Failed to run union on polygons.")
             raise
             raise
 
 
+    def subtract_polygon(self, points):
+        """
+        Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths.
+
+        :param points: The vertices of the polygon.
+        :return: none
+        """
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        #pathonly should be allways True, otherwise polygons are not subtracted
+        flat_geometry = self.flatten(pathonly=True)
+        log.debug("%d paths" % len(flat_geometry))
+        polygon=Polygon(points)
+        toolgeo=cascaded_union(polygon)
+        diffs=[]
+        for target in flat_geometry:
+            if type(target) == LineString or type(target) == LinearRing:
+                diffs.append(target.difference(toolgeo))
+            else:
+                log.warning("Not implemented.")
+        self.solid_geometry=cascaded_union(diffs)
+
     def bounds(self):
     def bounds(self):
         """
         """
         Returns coordinates of rectangular bounds
         Returns coordinates of rectangular bounds