Просмотр исходного кода

Merge branch 'new_aperture_storage' into beta_8.916

# Conflicts:
#	README.md
Marius Stanciu 6 лет назад
Родитель
Сommit
5287cbd8de
4 измененных файлов с 709 добавлено и 746 удалено
  1. 9 10
      FlatCAMObj.py
  2. 5 0
      README.md
  3. 278 453
      camlib.py
  4. 417 283
      flatcamEditors/FlatCAMGrbEditor.py

+ 9 - 10
FlatCAMObj.py

@@ -1155,17 +1155,16 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 self.app.progress.emit(30)
                 try:
                     if aperture_to_plot_mark in self.apertures:
-                        if type(self.apertures[aperture_to_plot_mark]['solid_geometry']) is not list:
-                            self.apertures[aperture_to_plot_mark]['solid_geometry'] = \
-                                [self.apertures[aperture_to_plot_mark]['solid_geometry']]
-                        for geo in self.apertures[aperture_to_plot_mark]['solid_geometry']:
-                            if type(geo) == Polygon or type(geo) == LineString:
-                                self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color,
-                                                    face_color=color, visible=visibility)
-                            else:
-                                for el in geo:
-                                    self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color,
+                        for elem in self.apertures[aperture_to_plot_mark]['geometry']:
+                            if 'solid' in elem:
+                                geo = elem['solid']
+                                if type(geo) == Polygon or type(geo) == LineString:
+                                    self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color,
                                                         face_color=color, visible=visibility)
+                                else:
+                                    for el in geo:
+                                        self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color,
+                                                            face_color=color, visible=visibility)
 
                     self.mark_shapes[aperture_to_plot_mark].redraw()
                     self.app.progress.emit(100)

+ 5 - 0
README.md

@@ -11,9 +11,14 @@ CAD program, and create G-Code for Isolation routing.
 
 10.05.2019
 
+- Gerber Editor - working in conversion to the new data format
 - made sure that only units toggle done in Edit -> Preferences will toggle the data in Preferences. THe menu entry Edit -> Toggle Units and the shortcut key 'Q' will change only the display units in the app
 - optimized Transform tool
 
+9.05.2019
+
+- rework the Gerber parser
+
 8.05.2019
 
 - added zoom fit for Set Origin command

+ 278 - 453
camlib.py

@@ -1920,14 +1920,19 @@ class Gerber (Geometry):
         '''
         apertures = {
             'id':{
-                'type':chr, 
+                'type':string, 
                 'size':float, 
                 'width':float,
                 'height':float,
-                'solid_geometry': [],
-                'follow_geometry': [],
+                'geometry': [],
             }
         }
+        apertures['geometry'] list elements are dicts
+        dict = {
+            'solid': [],
+            'follow': [],
+            'clear': []
+        }
         '''
 
         # aperture storage
@@ -2181,8 +2186,8 @@ class Gerber (Geometry):
         # store here the follow geometry
         follow_buffer = []
 
-        last_path_aperture = None
-        current_aperture = None
+        last_path_aperture = '0'
+        current_aperture = '0'
 
         # 1,2 or 3 from "G01", "G02" or "G03"
         current_interpolation_mode = None
@@ -2227,7 +2232,7 @@ class Gerber (Geometry):
 
                 ### Cleanup
                 gline = gline.strip(' \r\n')
-                # log.debug("Line=%3s %s" % (line_num, gline))
+                log.debug("Line=%3s %s" % (line_num, gline))
 
                 #### Ignored lines
                 ## Comments
@@ -2244,37 +2249,30 @@ class Gerber (Geometry):
                     new_polarity = match.group(1)
                     # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer))
                     self.is_lpc = True if new_polarity == 'C' else False
+
                     if len(path) > 1 and current_polarity != new_polarity:
 
                         # finish the current path and add it to the storage
                         # --- Buffered ----
                         width = self.apertures[last_path_aperture]["size"]
 
-                        geo = LineString(path)
-                        if not geo.is_empty:
-                            follow_buffer.append(geo)
-                            try:
-                                self.apertures[last_path_aperture]['follow_geometry'].append(geo)
-                            except KeyError:
-                                self.apertures[last_path_aperture]['follow_geometry'] = []
-                                self.apertures[last_path_aperture]['follow_geometry'].append(geo)
-
-                        geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                        if path:
+                            geo_f = LineString(path)
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            follow_buffer.append(geo_f)
+                            poly_buffer.append(geo_s)
 
-                        if not geo.is_empty:
-                            poly_buffer.append(geo)
-                            if self.is_lpc is True:
-                                try:
-                                    self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                except KeyError:
-                                    self.apertures[last_path_aperture]['clear_geometry'] = []
-                                    self.apertures[last_path_aperture]['clear_geometry'].append(geo)
+                            geo_dict = dict()
+                            geo_dict['follow'] = geo_f
+                            if self.is_lpc:
+                                geo_dict['clear'] = geo_s
                             else:
-                                try:
-                                    self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                except KeyError:
-                                    self.apertures[last_path_aperture]['solid_geometry'] = []
-                                    self.apertures[last_path_aperture]['solid_geometry'].append(geo)
+                                geo_dict['solid'] = geo_s
+                            try:
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
+                            except KeyError:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
 
                         path = [path[-1]]
 
@@ -2437,28 +2435,25 @@ class Gerber (Geometry):
                             flash = Gerber.create_flash_geometry(
                                 Point(current_x, current_y), self.apertures[current_aperture],
                                 int(self.steps_per_circle))
+
                             if not flash.is_empty:
-                                poly_buffer.append(flash)
-                                if self.is_lpc is True:
-                                    try:
-                                        self.apertures[current_aperture]['clear_geometry'].append(flash)
-                                    except KeyError:
-                                        self.apertures[current_aperture]['clear_geometry'] = []
-                                        self.apertures[current_aperture]['clear_geometry'].append(flash)
+                                geo_f = Point(current_x, current_y)
+                                geo_s = flash
+                                # follow_buffer.append(geo_f)
+                                poly_buffer.append(geo_s)
+
+                                geo_dict = dict()
+                                geo_dict['follow'] = geo_f
+                                if self.is_lpc:
+                                    geo_dict['clear'] = geo_s
                                 else:
-                                    try:
-                                        self.apertures[current_aperture]['follow_geometry'].append(Point(
-                                            current_x, current_y))
-                                    except KeyError:
-                                        self.apertures[current_aperture]['follow_geometry'] = []
-                                        self.apertures[current_aperture]['follow_geometry'].append(Point(
-                                            current_x, current_y))
+                                    geo_dict['solid'] = geo_s
+                                try:
+                                    self.apertures[current_aperture]['geometry'].append(geo_dict)
+                                except KeyError:
+                                    self.apertures[current_aperture]['geometry'] = []
+                                    self.apertures[current_aperture]['geometry'].append(geo_dict)
 
-                                    try:
-                                        self.apertures[current_aperture]['solid_geometry'].append(flash)
-                                    except KeyError:
-                                        self.apertures[current_aperture]['solid_geometry'] = []
-                                        self.apertures[current_aperture]['solid_geometry'].append(flash)
                         except IndexError:
                             log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
 
@@ -2482,37 +2477,25 @@ class Gerber (Geometry):
 
                     # Take care of the current path with the previous tool
                     if len(path) > 1:
-                        if self.apertures[last_path_aperture]["type"] == 'R':
-                            # do nothing because 'R' type moving aperture is none at once
-                            pass
-                        else:
+                        if self.apertures[last_path_aperture]["type"] != 'R':
+                            width = self.apertures[last_path_aperture]["size"]
 
-                            geo = LineString(path)
-                            if not geo.is_empty:
-                                follow_buffer.append(geo)
-                                try:
-                                    self.apertures[last_path_aperture]['follow_geometry'].append(geo)
-                                except KeyError:
-                                    self.apertures[last_path_aperture]['follow_geometry'] = []
-                                    self.apertures[last_path_aperture]['follow_geometry'].append(geo)
+                            geo_f = LineString(path)
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            follow_buffer.append(geo_f)
+                            poly_buffer.append(geo_s)
 
-                            # --- Buffered ----
-                            width = self.apertures[last_path_aperture]["size"]
-                            geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                            if not geo.is_empty:
-                                poly_buffer.append(geo)
-                                if self.is_lpc is True:
-                                    try:
-                                        self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures[last_path_aperture]['clear_geometry'] = []
-                                        self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                else:
-                                    try:
-                                        self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures[last_path_aperture]['solid_geometry'] = []
-                                        self.apertures[last_path_aperture]['solid_geometry'].append(geo)
+                            geo_dict = dict()
+                            geo_dict['follow'] = geo_f
+                            if self.is_lpc:
+                                geo_dict['clear'] = geo_s
+                            else:
+                                geo_dict['solid'] = geo_s
+                            try:
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
+                            except KeyError:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
 
                             path = [path[-1]]
 
@@ -2522,34 +2505,24 @@ class Gerber (Geometry):
                 if self.regionon_re.search(gline):
                     if len(path) > 1:
                         # Take care of what is left in the path
-
-                        ## --- Buffered ---
                         width = self.apertures[last_path_aperture]["size"]
 
-                        geo = LineString(path)
-                        if not geo.is_empty:
-                            follow_buffer.append(geo)
-                            try:
-                                self.apertures[current_aperture]['follow_geometry'].append(geo)
-                            except KeyError:
-                                self.apertures[current_aperture]['follow_geometry'] = []
-                                self.apertures[current_aperture]['follow_geometry'].append(geo)
+                        geo_f = LineString(path)
+                        geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                        follow_buffer.append(geo_f)
+                        poly_buffer.append(geo_s)
 
-                        geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4))
-                        if not geo.is_empty:
-                            poly_buffer.append(geo)
-                            if self.is_lpc is True:
-                                try:
-                                    self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                except KeyError:
-                                    self.apertures[last_path_aperture]['clear_geometry'] = []
-                                    self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                            else:
-                                try:
-                                    self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                except KeyError:
-                                    self.apertures[last_path_aperture]['solid_geometry'] = []
-                                    self.apertures[last_path_aperture]['solid_geometry'].append(geo)
+                        geo_dict = dict()
+                        geo_dict['follow'] = geo_f
+                        if self.is_lpc:
+                            geo_dict['clear'] = geo_s
+                        else:
+                            geo_dict['solid'] = geo_s
+                        try:
+                            self.apertures[last_path_aperture]['geometry'].append(geo_dict)
+                        except KeyError:
+                            self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(geo_dict)
 
                         path = [path[-1]]
 
@@ -2564,94 +2537,61 @@ class Gerber (Geometry):
                         self.apertures['0'] = {}
                         self.apertures['0']['type'] = 'REG'
                         self.apertures['0']['size'] = 0.0
-                        self.apertures['0']['solid_geometry'] = []
+                        self.apertures['0']['geometry'] = []
 
                     # if D02 happened before G37 we now have a path with 1 element only so we have to add the current
                     # geo to the poly_buffer otherwise we loose it
-                    if current_operation_code == 2:
-                        if geo:
-                            if not geo.is_empty:
-                                follow_buffer.append(geo)
-                                try:
-                                    self.apertures['0']['follow_geometry'].append(geo)
-                                except KeyError:
-                                    self.apertures['0']['follow_geometry'] = []
-                                    self.apertures['0']['follow_geometry'].append(geo)
-
-                                poly_buffer.append(geo)
-                                if self.is_lpc is True:
-                                    try:
-                                        self.apertures['0']['clear_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures['0']['clear_geometry'] = []
-                                        self.apertures['0']['clear_geometry'].append(geo)
-                                else:
-                                    try:
-                                        self.apertures['0']['solid_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures['0']['solid_geometry'] = []
-                                        self.apertures['0']['solid_geometry'].append(geo)
-                            continue
+                    # if current_operation_code == 2:
+                    #     if geo:
+                    #         if not geo.is_empty:
+                    #             follow_buffer.append(geo)
+                    #             try:
+                    #                 self.apertures['0']['follow_geometry'].append(geo)
+                    #             except KeyError:
+                    #                 self.apertures['0']['follow_geometry'] = []
+                    #                 self.apertures['0']['follow_geometry'].append(geo)
+                    #
+                    #             poly_buffer.append(geo)
+                    #             if self.is_lpc is True:
+                    #                 try:
+                    #                     self.apertures['0']['clear_geometry'].append(geo)
+                    #                 except KeyError:
+                    #                     self.apertures['0']['clear_geometry'] = []
+                    #                     self.apertures['0']['clear_geometry'].append(geo)
+                    #             else:
+                    #                 try:
+                    #                     self.apertures['0']['solid_geometry'].append(geo)
+                    #                 except KeyError:
+                    #                     self.apertures['0']['solid_geometry'] = []
+                    #                     self.apertures['0']['solid_geometry'].append(geo)
+                    #         continue
 
                     # Only one path defines region?
                     # This can happen if D02 happened before G37 and
                     # is not and error.
                     if len(path) < 3:
-                        # print "ERROR: Path contains less than 3 points:"
-                        # print path
-                        # print "Line (%d): " % line_num, gline
-                        # path = []
-                        #path = [[current_x, current_y]]
                         continue
 
-                    # For regions we may ignore an aperture that is None
-                    # self.regions.append({"polygon": Polygon(path),
-                    #                      "aperture": last_path_aperture})
-
-                    # --- Buffered ---
-
-                    region = Polygon()
-                    if not region.is_empty:
-                        follow_buffer.append(region)
-                        try:
-                            self.apertures['0']['follow_geometry'].append(region)
-                        except KeyError:
-                            self.apertures['0']['follow_geometry'] = []
-                            self.apertures['0']['follow_geometry'].append(region)
-
-                    region = Polygon(path)
-                    if not region.is_valid:
-                        region = region.buffer(0, int(self.steps_per_circle / 4))
-
-                    if not region.is_empty:
-                        poly_buffer.append(region)
-
-                        # we do this for the case that a region is done without having defined any aperture
-                        # Allegro does that
-                        # if current_aperture:
-                        #     used_aperture = current_aperture
-                        # elif last_path_aperture:
-                        #     used_aperture = last_path_aperture
-                        # else:
-                        #     if '0' not in self.apertures:
-                        #         self.apertures['0'] = {}
-                        #         self.apertures['0']['size'] = 0.0
-                        #         self.apertures['0']['type'] = 'REG'
-                        #         self.apertures['0']['solid_geometry'] = []
-                        #     used_aperture = '0'
-                        used_aperture = '0'
-                        if self.is_lpc is True:
-                            try:
-                                self.apertures[used_aperture]['clear_geometry'].append(region)
-                            except KeyError:
-                                self.apertures[used_aperture]['clear_geometry'] = []
-                                self.apertures[used_aperture]['clear_geometry'].append(region)
+                    geo_f = LineString(path)
+                    geo_s = Polygon(path)
+                    if not geo_s.is_valid:
+                        geo_s = geo_s.buffer(0, int(self.steps_per_circle / 4))
+                    follow_buffer.append(geo_f)
+                    if not geo_s.is_empty:
+                        poly_buffer.append(geo_s)
+
+                    geo_dict = dict()
+                    geo_dict['follow'] = geo_f
+                    if not geo_s.is_empty:
+                        if self.is_lpc:
+                            geo_dict['clear'] = geo_s
                         else:
-                            try:
-                                self.apertures[used_aperture]['solid_geometry'].append(region)
-                            except KeyError:
-                                self.apertures[used_aperture]['solid_geometry'] = []
-                                self.apertures[used_aperture]['solid_geometry'].append(region)
+                            geo_dict['solid'] = geo_s
+                    try:
+                        self.apertures['0']['geometry'].append(geo_dict)
+                    except KeyError:
+                        self.apertures['0']['geometry'] = []
+                        self.apertures['0']['geometry'].append(geo_dict)
 
                     path = [[current_x, current_y]]  # Start new path
                     continue
@@ -2680,6 +2620,14 @@ class Gerber (Geometry):
                     # NOTE: Letting it continue allows it to react to the
                     #       operation code.
 
+                    if current_aperture is None:
+                        if '0' not in self.apertures:
+                            self.apertures['0'] = {}
+                            self.apertures['0']['type'] = 'REG'
+                            self.apertures['0']['size'] = 0.0
+                            self.apertures['0']['geometry'] = []
+                        current_aperture = '0'
+
                     # Parse coordinates
                     if match.group(2) is not None:
                         linear_x = parse_gerber_number(match.group(2),
@@ -2705,6 +2653,7 @@ class Gerber (Geometry):
                             # only add the point if it's a new one otherwise skip it (harder to process)
                             if path[-1] != [current_x, current_y]:
                                 path.append([current_x, current_y])
+
                             if making_region is False:
                                 # if the aperture is rectangle then add a rectangular shape having as parameters the
                                 # coordinates of the start and end point and also the width and height
@@ -2717,136 +2666,79 @@ class Gerber (Geometry):
                                         maxx = max(path[0][0], path[1][0]) + width / 2
                                         miny = min(path[0][1], path[1][1]) - height / 2
                                         maxy = max(path[0][1], path[1][1]) + height / 2
-                                        log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
-
-                                        geo = shply_box(minx, miny, maxx, maxy)
-                                        poly_buffer.append(geo)
-                                        if self.is_lpc is True:
-                                            try:
-                                                self.apertures[current_aperture]['clear_geometry'].append(geo)
-                                            except KeyError:
-                                                self.apertures[current_aperture]['clear_geometry'] = []
-                                                self.apertures[current_aperture]['clear_geometry'].append(geo)
+                                        # log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
+
+                                        r_x = maxx - minx
+                                        r_y = maxy - miny
+                                        geo_f = Point(r_x, r_y)
+                                        geo_s = shply_box(minx, miny, maxx, maxy)
+                                        follow_buffer.append(geo_f)
+                                        poly_buffer.append(geo_s)
+
+                                        geo_dict = dict()
+                                        geo_dict['follow'] = geo_f
+                                        if self.is_lpc:
+                                            geo_dict['clear'] = geo_s
                                         else:
-                                            try:
-                                                self.apertures[current_aperture]['solid_geometry'].append(geo)
-                                            except KeyError:
-                                                self.apertures[current_aperture]['solid_geometry'] = []
-                                                self.apertures[current_aperture]['solid_geometry'].append(geo)
+                                            geo_dict['solid'] = geo_s
+                                        try:
+                                            self.apertures[current_aperture]['geometry'].append(geo_dict)
+                                        except KeyError:
+                                            self.apertures[current_aperture]['geometry'] = []
+                                            self.apertures[current_aperture]['geometry'].append(geo_dict)
                                 except:
                                     pass
                             last_path_aperture = current_aperture
-                            # we do this for the case that a region is done without having defined any aperture
-                            # Allegro does that
-                            if last_path_aperture is None:
-                                if '0' not in self.apertures:
-                                    self.apertures['0'] = {}
-                                    self.apertures['0']['type'] = 'REG'
-                                    self.apertures['0']['size'] = 0.0
-                                    self.apertures['0']['solid_geometry'] = []
-                                last_path_aperture = '0'
+
                         else:
                             self.app.inform.emit(_("[WARNING] Coordinates missing, line ignored: %s") % str(gline))
                             self.app.inform.emit(_("[WARNING_NOTCL] GERBER file might be CORRUPT. Check the file !!!"))
 
                     elif current_operation_code == 2:
+                        # finish current path
                         if len(path) > 1:
-                            geo = None
 
-                            # --- BUFFERED ---
-                            # this treats the case when we are storing geometry as paths only
-                            if making_region:
-                                # we do this for the case that a region is done without having defined any aperture
-                                # Allegro does that
-                                if last_path_aperture is None:
-                                    if '0' not in self.apertures:
-                                        self.apertures['0'] = {}
-                                        self.apertures['0']['type'] = 'REG'
-                                        self.apertures['0']['size'] = 0.0
-                                        self.apertures['0']['solid_geometry'] = []
-                                    last_path_aperture = '0'
-                                geo = Polygon()
+                            if last_path_aperture is None:
+                                if '0' not in self.apertures:
+                                    self.apertures['0'] = {}
+                                    self.apertures['0']['type'] = 'REG'
+                                    self.apertures['0']['size'] = 0.0
+                                    self.apertures['0']['geometry'] = []
+                                last_path_aperture = '0'
+                                width = 0
                             else:
-                                geo = LineString(path)
+                                width = self.apertures[last_path_aperture]["size"]
 
-                            try:
-                                if self.apertures[last_path_aperture]["type"] != 'R':
-                                    if not geo.is_empty:
-                                        follow_buffer.append(geo)
-                                        try:
-                                            self.apertures[current_aperture]['follow_geometry'].append(geo)
-                                        except KeyError:
-                                            self.apertures[current_aperture]['follow_geometry'] = []
-                                            self.apertures[current_aperture]['follow_geometry'].append(geo)
-                            except Exception as e:
-                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
-                                if not geo.is_empty:
-                                    follow_buffer.append(geo)
-                                    try:
-                                        self.apertures[current_aperture]['follow_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures[current_aperture]['follow_geometry'] = []
-                                        self.apertures[current_aperture]['follow_geometry'].append(geo)
-
-                            # this treats the case when we are storing geometry as solids
-                            if making_region:
-                                # we do this for the case that a region is done without having defined any aperture
-                                # Allegro does that
-                                if last_path_aperture is None:
-                                    if '0' not in self.apertures:
-                                        self.apertures['0'] = {}
-                                        self.apertures['0']['type'] = 'REG'
-                                        self.apertures['0']['size'] = 0.0
-                                        self.apertures['0']['solid_geometry'] = []
-                                    last_path_aperture = '0'
-                                # elem = [current_x, current_y]
-                                # if elem != path[-1]:
-                                #     path.append([current_x, current_y])
-
-                                try:
-                                    geo = Polygon(path)
-                                except ValueError:
-                                    log.warning("Problem %s %s" % (gline, line_num))
-                                    self.app.inform.emit(_("[ERROR] Region does not have enough points. "
-                                                         "File will be processed but there are parser errors. "
-                                                         "Line number: %s") % str(line_num))
-                            else:
-                                if last_path_aperture is None:
-                                    log.warning("No aperture defined for curent path. (%d)" % line_num)
-                                width = self.apertures[last_path_aperture]["size"]  # TODO: WARNING this should fail!
-                                geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            if path and self.apertures[last_path_aperture]["type"] != 'R':
+                                geo_f = LineString(path)
+                                geo_s = None
 
-                            try:
-                                if self.apertures[last_path_aperture]["type"] != 'R':
-                                    if not geo.is_empty:
-                                        poly_buffer.append(geo)
-                                        if self.is_lpc is True:
-                                            try:
-                                                self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                            except KeyError:
-                                                self.apertures[last_path_aperture]['clear_geometry'] = []
-                                                self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                        else:
-                                            try:
-                                                self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                            except KeyError:
-                                                self.apertures[last_path_aperture]['solid_geometry'] = []
-                                                self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                            except Exception as e:
-                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
-                                poly_buffer.append(geo)
-                                if self.is_lpc is True:
+                                if making_region:
                                     try:
-                                        self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures[last_path_aperture]['clear_geometry'] = []
-                                        self.apertures[last_path_aperture]['clear_geometry'].append(geo)
+                                        geo_s = Polygon(path)
+                                        poly_buffer.append(geo_s)
+                                    except ValueError:
+                                        log.warning("Problem %s %s" % (gline, line_num))
+                                        self.app.inform.emit(_("[ERROR] Region does not have enough points. "
+                                                               "File will be processed but there are parser errors. "
+                                                               "Line number: %s") % str(line_num))
                                 else:
-                                    try:
-                                        self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures[last_path_aperture]['solid_geometry'] = []
-                                        self.apertures[last_path_aperture]['solid_geometry'].append(geo)
+                                    geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                                    poly_buffer.append(geo_s)
+                                follow_buffer.append(geo_f)
+
+                                geo_dict = dict()
+                                geo_dict['follow'] = geo_f
+                                if geo_s:
+                                    if self.is_lpc:
+                                        geo_dict['clear'] = geo_s
+                                    else:
+                                        geo_dict['solid'] = geo_s
+                                try:
+                                    self.apertures[last_path_aperture]['geometry'].append(geo_dict)
+                                except KeyError:
+                                    self.apertures[last_path_aperture]['geometry'] = []
+                                    self.apertures[last_path_aperture]['geometry'].append(geo_dict)
 
                         # if linear_x or linear_y are None, ignore those
                         if linear_x is not None and linear_y is not None:
@@ -2859,98 +2751,53 @@ class Gerber (Geometry):
                     # Not allowed in region mode.
                     elif current_operation_code == 3:
 
-                        # Create path draw so far.
-                        if len(path) > 1:
-                            # --- Buffered ----
-
-                            # this treats the case when we are storing geometry as paths
-                            geo = LineString(path)
-                            if not geo.is_empty:
-                                try:
-                                    if self.apertures[last_path_aperture]["type"] != 'R':
-                                        follow_buffer.append(geo)
-                                        try:
-                                            self.apertures[last_path_aperture]['follow_geometry'].append(geo)
-                                        except KeyError:
-                                            self.apertures[last_path_aperture]['follow_geometry'] = []
-                                            self.apertures[last_path_aperture]['follow_geometry'].append(geo)
-                                except Exception as e:
-                                    log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e))
-                                    follow_buffer.append(geo)
-                                    try:
-                                        self.apertures[last_path_aperture]['follow_geometry'].append(geo)
-                                    except KeyError:
-                                        self.apertures[last_path_aperture]['follow_geometry'] = []
-                                        self.apertures[last_path_aperture]['follow_geometry'].append(geo)
-
-                            # this treats the case when we are storing geometry as solids
-                            width = self.apertures[last_path_aperture]["size"]
-                            geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                            if not geo.is_empty:
-                                try:
-                                    if self.apertures[last_path_aperture]["type"] != 'R':
-                                        poly_buffer.append(geo)
-                                        if self.is_lpc is True:
-                                            try:
-                                                self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                            except KeyError:
-                                                self.apertures[last_path_aperture]['clear_geometry'] = []
-                                                self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                        else:
-                                            try:
-                                                self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                            except KeyError:
-                                                self.apertures[last_path_aperture]['solid_geometry'] = []
-                                                self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                except:
-                                    poly_buffer.append(geo)
-                                    if self.is_lpc is True:
-                                        try:
-                                            self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                        except KeyError:
-                                            self.apertures[last_path_aperture]['clear_geometry'] = []
-                                            self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                                    else:
-                                        try:
-                                            self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                                        except KeyError:
-                                            self.apertures[last_path_aperture]['solid_geometry'] = []
-                                            self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-
+                        width = self.apertures[last_path_aperture]["size"]
+                        # finish the path draw until now
+                        if len(path) > 1 and self.apertures[last_path_aperture]["type"] != 'R':
+
+                            geo_f = LineString(path)
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            follow_buffer.append(geo_f)
+                            poly_buffer.append(geo_s)
+
+                            geo_dict = dict()
+                            geo_dict['follow'] = geo_f
+                            if self.is_lpc:
+                                geo_dict['clear'] = geo_s
+                            else:
+                                geo_dict['solid'] = geo_s
+                            try:
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
+                            except KeyError:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
                         # Reset path starting point
                         path = [[linear_x, linear_y]]
 
-                        # --- BUFFERED ---
-                        # Draw the flash
-                        # this treats the case when we are storing geometry as paths
-                        geo_flash = Point([linear_x, linear_y])
-                        follow_buffer.append(geo_flash)
-                        try:
-                            self.apertures[current_aperture]['follow_geometry'].append(geo_flash)
-                        except KeyError:
-                            self.apertures[current_aperture]['follow_geometry'] = []
-                            self.apertures[current_aperture]['follow_geometry'].append(geo_flash)
 
-                        # this treats the case when we are storing geometry as solids
-                        flash = Gerber.create_flash_geometry(
-                            Point( [linear_x, linear_y]),
+                        # Draw the flash
+                        geo_f = Point(linear_x, linear_y)
+                        geo_s = Gerber.create_flash_geometry(
+                            Point([linear_x, linear_y]),
                             self.apertures[current_aperture],
                             int(self.steps_per_circle)
                         )
-                        if not flash.is_empty:
-                            poly_buffer.append(flash)
-                            if self.is_lpc is True:
-                                try:
-                                    self.apertures[current_aperture]['clear_geometry'].append(flash)
-                                except KeyError:
-                                    self.apertures[current_aperture]['clear_geometry'] = []
-                                    self.apertures[current_aperture]['clear_geometry'].append(flash)
+                        follow_buffer.append(geo_f)
+                        if not geo_s.is_empty:
+                            poly_buffer.append(geo_s)
+
+                        geo_dict = dict()
+                        geo_dict['follow'] = geo_f
+                        if not geo_s.is_empty:
+                            if self.is_lpc:
+                                geo_dict['clear'] = geo_s
                             else:
-                                try:
-                                    self.apertures[current_aperture]['solid_geometry'].append(flash)
-                                except KeyError:
-                                    self.apertures[current_aperture]['solid_geometry'] = []
-                                    self.apertures[current_aperture]['solid_geometry'].append(flash)
+                                geo_dict['solid'] = geo_s
+                        try:
+                            self.apertures[current_aperture]['geometry'].append(geo_dict)
+                        except KeyError:
+                            self.apertures[current_aperture]['geometry'] = []
+                            self.apertures[current_aperture]['geometry'].append(geo_dict)
 
                     # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
                     # are used in case that circular interpolation is encountered within the Gerber file
@@ -3024,6 +2871,8 @@ class Gerber (Geometry):
                     # Nothing created! Pen Up.
                     if current_operation_code == 2:
                         log.warning("Arc with D2. (%d)" % line_num)
+
+                        # if we have something drawn until this moment, add it
                         if len(path) > 1:
                             if last_path_aperture is None:
                                 log.warning("No aperture defined for curent path. (%d)" % line_num)
@@ -3031,32 +2880,24 @@ class Gerber (Geometry):
                             # --- BUFFERED ---
                             width = self.apertures[last_path_aperture]["size"]
 
-                            # this treats the case when we are storing geometry as paths
-                            geo = LineString(path)
-                            if not geo.is_empty:
-                                follow_buffer.append(geo)
-                                try:
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
-                                except KeyError:
-                                    self.apertures[current_aperture]['follow_geometry'] = []
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
-
-                            # this treats the case when we are storing geometry as solids
-                            buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
-                            if not buffered.is_empty:
-                                poly_buffer.append(buffered)
-                                if self.is_lpc is True:
-                                    try:
-                                        self.apertures[last_path_aperture]['clear_geometry'].append(buffered)
-                                    except KeyError:
-                                        self.apertures[last_path_aperture]['clear_geometry'] = []
-                                        self.apertures[last_path_aperture]['clear_geometry'].append(buffered)
+                            geo_f = LineString(path)
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            if not geo_s.is_empty:
+                                follow_buffer.append(geo_f)
+                            poly_buffer.append(geo_s)
+
+                            geo_dict = dict()
+                            geo_dict['follow'] = geo_f
+                            if not geo_s.is_empty:
+                                if self.is_lpc:
+                                    geo_dict['clear'] = geo_s
                                 else:
-                                    try:
-                                        self.apertures[last_path_aperture]['solid_geometry'].append(buffered)
-                                    except KeyError:
-                                        self.apertures[last_path_aperture]['solid_geometry'] = []
-                                        self.apertures[last_path_aperture]['solid_geometry'].append(buffered)
+                                    geo_dict['solid'] = geo_s
+                            try:
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
+                            except KeyError:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                                self.apertures[last_path_aperture]['geometry'].append(geo_dict)
 
                         current_x = circular_x
                         current_y = circular_y
@@ -3168,39 +3009,28 @@ class Gerber (Geometry):
                 # In case that G01 (moving) aperture is rectangular, there is no need to still create
                 # another geo since we already created a shapely box using the start and end coordinates found in
                 # path variable. We do it only for other apertures than 'R' type
-                if self.apertures[last_path_aperture]["type"] == 'R':
-                    pass
-                else:
+                if self.apertures[last_path_aperture]["type"] != 'R':
                     # EOF, create shapely LineString if something still in path
-                    ## --- Buffered ---
-
-                    # this treats the case when we are storing geometry as paths
-                    geo = LineString(path)
-                    if not geo.is_empty:
-                        follow_buffer.append(geo)
-                        try:
-                            self.apertures[current_aperture]['follow_geometry'].append(geo)
-                        except KeyError:
-                            self.apertures[current_aperture]['follow_geometry'] = []
-                            self.apertures[current_aperture]['follow_geometry'].append(geo)
-
-                    # this treats the case when we are storing geometry as solids
                     width = self.apertures[last_path_aperture]["size"]
-                    geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                    if not geo.is_empty:
-                        poly_buffer.append(geo)
-                        if self.is_lpc is True:
-                            try:
-                                self.apertures[last_path_aperture]['clear_geometry'].append(geo)
-                            except KeyError:
-                                self.apertures[last_path_aperture]['clear_geometry'] = []
-                                self.apertures[last_path_aperture]['clear_geometry'].append(geo)
+
+                    geo_f = LineString(path)
+                    geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                    follow_buffer.append(geo_f)
+                    if not geo_s.is_empty:
+                        poly_buffer.append(geo_s)
+
+                    geo_dict = dict()
+                    geo_dict['follow'] = geo_f
+                    if not geo_s.is_empty:
+                        if self.is_lpc:
+                            geo_dict['clear'] = geo_s
                         else:
-                            try:
-                                self.apertures[last_path_aperture]['solid_geometry'].append(geo)
-                            except KeyError:
-                                self.apertures[last_path_aperture]['solid_geometry'] = []
-                                self.apertures[last_path_aperture]['solid_geometry'].append(geo)
+                            geo_dict['solid'] = geo_s
+                    try:
+                        self.apertures[last_path_aperture]['geometry'].append(geo_dict)
+                    except KeyError:
+                        self.apertures[last_path_aperture]['geometry'] = []
+                        self.apertures[last_path_aperture]['geometry'].append(geo_dict)
 
             # TODO: make sure to keep track of units changes because right now it seems to happen in a weird way
             # find out the conversion factor used to convert inside the self.apertures keys: size, width, height
@@ -3215,31 +3045,23 @@ class Gerber (Geometry):
             global_clear_geo = []
 
             for apid in self.apertures:
-                # first check if we have any clear_geometry (LPC) and if yes added it to the global_clear_geo
-                if 'clear_geometry' in self.apertures[apid]:
-                    for pol in self.apertures[apid]['clear_geometry']:
-                        global_clear_geo.append(pol)
-                self.apertures[apid].pop('clear_geometry', None)
+                if 'geometry' in self.apertures[apid]:
+                    for elem in self.apertures[apid]['geometry']:
+                        if 'clear' in elem:
+                            global_clear_geo.append(elem['clear'])
             log.warning("Found %d clear polygons." % len(global_clear_geo))
 
-            temp_geo = []
             for apid in self.apertures:
-                if 'solid_geometry' in self.apertures[apid]:
-                    for solid_geo in self.apertures[apid]['solid_geometry']:
-                        for clear_geo in global_clear_geo:
-                            # Make sure that the clear_geo is within the solid_geo otherwise we loose
-                            # the solid_geometry. We want for clear_geometry just to cut into solid_geometry not to
-                            # delete it
-                            if clear_geo.within(solid_geo):
-                                solid_geo = solid_geo.difference(clear_geo)
-                        try:
-                            for poly in solid_geo:
-                                temp_geo.append(poly)
-                        except TypeError:
-                            temp_geo.append(solid_geo)
+                if 'geometry' in self.apertures[apid]:
+                    for elem in self.apertures[apid]['geometry']:
+                        if 'solid' in elem:
+                            for clear_geo in global_clear_geo:
+                                # Make sure that the clear_geo is within the solid_geo otherwise we loose
+                                # the solid_geometry. We want for clear_geometry just to cut into solid_geometry not to
+                                # delete it
+                                if clear_geo.within(elem['solid']):
+                                    elem['solid'] = elem['solid'].difference(clear_geo)
 
-                    self.apertures[apid]['solid_geometry'] = deepcopy(temp_geo)
-                    temp_geo = []
             log.warning("Polygon difference done for %d apertures." % len(self.apertures))
 
             for apid in self.apertures:
@@ -3249,6 +3071,9 @@ class Gerber (Geometry):
                         self.apertures[apid][k] = v * conversion_factor
             # -------------------------------------------------------------
 
+            # for t in self.apertures:
+            #     print(t, self.apertures[t])
+
             # --- Apply buffer ---
             # this treats the case when we are storing geometry as paths
             self.follow_geometry = follow_buffer

+ 417 - 283
flatcamEditors/FlatCAMGrbEditor.py

@@ -14,7 +14,6 @@ from copy import copy, deepcopy
 from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
     SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
-from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from FlatCAMObj import FlatCAMGerber
 from FlatCAMTool import FlatCAMTool
 
@@ -32,6 +31,148 @@ if '_' not in builtins.__dict__:
     _ = gettext.gettext
 
 
+class DrawToolShape(object):
+    """
+    Encapsulates "shapes" under a common class.
+    """
+
+    tolerance = None
+
+    @staticmethod
+    def get_pts(o):
+        """
+        Returns a list of all points in the object, where
+        the object can be a Polygon, Not a polygon, or a list
+        of such. Search is done recursively.
+
+        :param: geometric object
+        :return: List of points
+        :rtype: list
+        """
+        pts = []
+
+        ## Iterable: descend into each item.
+        try:
+            for subo in o:
+                pts += DrawToolShape.get_pts(subo)
+
+        ## Non-iterable
+        except TypeError:
+            if o is not None:
+                ## DrawToolShape: descend into .geo.
+                if isinstance(o, DrawToolShape):
+                    pts += DrawToolShape.get_pts(o.geo)
+
+                ## Descend into .exerior and .interiors
+                elif type(o) == Polygon:
+                    pts += DrawToolShape.get_pts(o.exterior)
+                    for i in o.interiors:
+                        pts += DrawToolShape.get_pts(i)
+                elif type(o) == MultiLineString:
+                    for line in o:
+                        pts += DrawToolShape.get_pts(line)
+                ## Has .coords: list them.
+                else:
+                    if DrawToolShape.tolerance is not None:
+                        pts += list(o.simplify(DrawToolShape.tolerance).coords)
+                    else:
+                        pts += list(o.coords)
+            else:
+                return
+        return pts
+
+    def __init__(self, geo={}):
+
+        # Shapely type or list of such
+        self.geo = geo
+        self.utility = False
+
+class DrawToolUtilityShape(DrawToolShape):
+    """
+    Utility shapes are temporary geometry in the editor
+    to assist in the creation of shapes. For example it
+    will show the outline of a rectangle from the first
+    point to the current mouse pointer before the second
+    point is clicked and the final geometry is created.
+    """
+
+    def __init__(self, geo={}):
+        super(DrawToolUtilityShape, self).__init__(geo=geo)
+        self.utility = True
+
+
+class DrawTool(object):
+    """
+    Abstract Class representing a tool in the drawing
+    program. Can generate geometry, including temporary
+    utility geometry that is updated on user clicks
+    and mouse motion.
+    """
+
+    def __init__(self, draw_app):
+        self.draw_app = draw_app
+        self.complete = False
+        self.points = []
+        self.geometry = None  # DrawToolShape or None
+
+    def click(self, point):
+        """
+        :param point: [x, y] Coordinate pair.
+        """
+        return ""
+
+    def click_release(self, point):
+        """
+        :param point: [x, y] Coordinate pair.
+        """
+        return ""
+
+    def on_key(self, key):
+        return None
+
+    def utility_geometry(self, data=None):
+        return None
+
+    def bounds(self, obj):
+        def bounds_rec(o):
+            if type(o) is list:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in o:
+                    try:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                    except Exception as e:
+                        log.debug("camlib.Gerber.bounds() --> %s" % str(e))
+                        return
+
+                    minx = min(minx, minx_)
+                    miny = min(miny, miny_)
+                    maxx = max(maxx, maxx_)
+                    maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return o.geo.bounds
+
+        bounds_coords = bounds_rec(obj)
+        return bounds_coords
+
+
+class FCShapeTool(DrawTool):
+    """
+    Abstract class for tools that create a shape.
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+
+    def make(self):
+        pass
+
+
 class FCPad(FCShapeTool):
     """
     Resulting type: Polygon
@@ -2159,9 +2300,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # this var will store the state of the toolbar before starting the editor
         self.toolbar_old_state = False
 
-        # holds flattened geometry
-        self.flat_geometry = []
-
         # Init GUI
         self.apdim_lbl.hide()
         self.apdim_entry.hide()
@@ -2180,7 +2318,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.shapes.enabled = False
         self.tool_shape.enabled = False
 
-        ## List of selected shapes.
+        ## List of selected geometric elements.
         self.selected = []
 
         self.key = None  # Currently pressed key
@@ -2493,8 +2631,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 self.apsize_entry.set_value(size_val)
                 self.storage_dict[ap_id]['size'] = size_val
 
-                self.storage_dict[ap_id]['solid_geometry'] = []
-                self.storage_dict[ap_id]['follow_geometry'] = []
+                self.storage_dict[ap_id]['geometry'] = []
 
                 # self.olddia_newdia dict keeps the evidence on current aperture codes as keys and gets updated on values
                 # each time a aperture code is edited or added
@@ -2535,8 +2672,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                             return
                 self.storage_dict[ap_id]['size'] = size_val
 
-                self.storage_dict[ap_id]['solid_geometry'] = []
-                self.storage_dict[ap_id]['follow_geometry'] = []
+                self.storage_dict[ap_id]['geometry'] = []
 
                 # self.olddia_newdia dict keeps the evidence on current aperture codes as keys and gets updated on values
                 # each time a aperture code is edited or added
@@ -2662,11 +2798,19 @@ class FlatCAMGrbEditor(QtCore.QObject):
         else:
             # aperture code is already in use so we move the pads from the prior tool to the new tool
             factor = current_table_dia_edited / dia_changed
-            for shape in self.storage_dict[dia_changed].get_objects():
-                geometry.append(DrawToolShape(
-                    MultiLineString([affinity.scale(subgeo, xfact=factor, yfact=factor) for subgeo in shape.geo])))
+            geometry = []
+            for geo_el in self.storage_dict[dia_changed]:
+                geometric_data = geo_el.geo
+                new_geo_el = dict()
+                if 'solid' in geometric_data:
+                    new_geo_el['solid'] = deepcopy(geometric_data['solid'])
+                if 'follow' in geometric_data:
+                    new_geo_el['follow'] = deepcopy(geometric_data['follow'])
+                if 'clear' in geometric_data:
+                    new_geo_el['clear'] = deepcopy(geometric_data['clear'])
+                # geometry.append(DrawToolShape(
+                #     MultiLineString([affinity.scale(subgeo, xfact=factor, yfact=factor) for subgeo in shape.geo])))
 
-                self.points_edit[current_table_dia_edited].append((0, 0))
             self.add_gerber_shape(geometry, self.storage_dict[current_table_dia_edited])
 
             self.on_aperture_delete(apid=dia_changed)
@@ -2916,37 +3060,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.shapes.clear(update=True)
         self.tool_shape.clear(update=True)
 
-    def flatten(self, geometry=None, reset=True, pathonly=False):
-        """
-        Creates a list of non-iterable linear geometry objects.
-        Polygons are expanded into its exterior pathonly param if specified.
-
-        Results are placed in flat_geometry
-
-        :param geometry: Shapely type or list or list of list of such.
-        :param reset: Clears the contents of self.flat_geometry.
-        :param pathonly: Expands polygons into linear elements from the exterior attribute.
-        """
-
-        if reset:
-            self.flat_geometry = []
-        ## If iterable, expand recursively.
-        try:
-            for geo in geometry:
-                if geo is not None:
-                    self.flatten(geometry=geo, reset=False, pathonly=pathonly)
-
-        ## Not iterable, do the actual indexing and add.
-        except TypeError:
-            if pathonly and type(geometry) == Polygon:
-                self.flat_geometry.append(geometry.exterior)
-                self.flatten(geometry=geometry.interiors,
-                             reset=False,
-                             pathonly=True)
-            else:
-                self.flat_geometry.append(geometry)
-        return self.flat_geometry
-
     def edit_fcgerber(self, orig_grb_obj):
         """
         Imports the geometry found in self.apertures from the given FlatCAM Gerber object
@@ -2978,28 +3091,19 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         def job_thread(self, apid):
             with self.app.proc_container.new(_("Adding aperture: %s geo ...") % str(apid)):
-                solid_storage_elem = []
-                follow_storage_elem = []
-
+                storage_elem = []
                 self.storage_dict[apid] = {}
 
                 # add the Gerber geometry to editor storage
                 for k, v in self.gerber_obj.apertures[apid].items():
                     try:
-                        if k == 'solid_geometry':
-                            for geo in v:
-                                if geo:
-                                    self.add_gerber_shape(DrawToolShape(geo), solid_storage_elem)
-                            self.storage_dict[apid][k] = solid_storage_elem
-                        elif k == 'follow_geometry':
-                            for geo in v:
-                                if geo is not None:
-                                    self.add_gerber_shape(DrawToolShape(geo), follow_storage_elem)
-                            self.storage_dict[apid][k] = follow_storage_elem
-                        elif k == 'clear_geometry':
-                            continue
+                        if k == 'geometry':
+                            for geo_el in v:
+                                if geo_el:
+                                    self.add_gerber_shape(DrawToolShape(geo_el), storage_elem)
+                            self.storage_dict[apid][k] = storage_elem
                         else:
-                            self.storage_dict[apid][k] = v
+                            self.storage_dict[apid][k] = self.gerber_obj.apertures[apid][k]
                     except Exception as e:
                         log.debug("FlatCAMGrbEditor.edit_fcgerber().job_thread() --> %s" % str(e))
                 # Check promises and clear if exists
@@ -3104,26 +3208,27 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 grb_obj.apertures[storage_apid] = {}
 
                 for k, v in storage_val.items():
-                    if k == 'solid_geometry':
+                    if k == 'geometry':
                         grb_obj.apertures[storage_apid][k] = []
-                        for geo in v:
-                            new_geo = deepcopy(geo.geo)
-                            grb_obj.apertures[storage_apid][k].append(new_geo)
-                            poly_buffer.append(new_geo)
+                        for geo_el in v:
+                            new_geo = dict()
+                            geometric_data = geo_el.geo
+                            for key in geometric_data:
+                                if key == 'solid':
+                                    new_geo[key] = geometric_data['solid']
+                                    poly_buffer.append(deepcopy(new_geo['solid']))
+                                if key == 'follow':
+                                    if isinstance(geometric_data[key], Polygon):
+                                        buff_val = -(int(storage_apid) / 2)
+                                        geo_f = geo_el.geo.buffer(buff_val).exterior
+                                        new_geo[key] = geo_f
+                                    else:
+                                        new_geo[key] = geometric_data[key]
+                                    follow_buffer.append(deepcopy(new_geo['follow']))
+                                if key == 'clear':
+                                    new_geo[key] = geometric_data['clear']
 
-                    elif k == 'follow_geometry':
-                        grb_obj.apertures[storage_apid][k] = []
-                        for geo_f in v:
-                            if isinstance(geo_f.geo, Polygon):
-                                buff_val = -(int(storage_apid) / 2)
-                                geo_f = geo_f.geo.buffer(buff_val).exterior
-                                new_geo = deepcopy(geo_f)
-                            else:
-                                new_geo = deepcopy(geo_f.geo)
-                            grb_obj.apertures[storage_apid][k].append(new_geo)
-                            follow_buffer.append(new_geo)
-                    else:
-                        grb_obj.apertures[storage_apid][k] = deepcopy(v)
+                            grb_obj.apertures[storage_apid][k].append(deepcopy(new_geo))
 
             grb_obj.aperture_macros = deepcopy(self.gerber_obj.aperture_macros)
 
@@ -3221,7 +3326,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 selected_apid = self.apertures_table.item(row, 1).text()
                 self.last_aperture_selected = copy(selected_apid)
 
-                for obj in self.storage_dict[selected_apid]['solid_geometry']:
+                for obj in self.storage_dict[selected_apid]['geometry']:
                     self.selected.append(obj)
             except Exception as e:
                 self.app.log.debug(str(e))
@@ -3233,7 +3338,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         return self.options[key]
 
     def on_grb_shape_complete(self, storage=None, specific_shape=None, noplot=False):
-        self.app.log.debug("on_shape_complete()")
+        self.app.log.debug("on_grb_shape_complete()")
 
         if specific_shape:
             geo = specific_shape
@@ -3246,7 +3351,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             # Add shape
             self.add_gerber_shape(geo, storage)
         else:
-            stora = self.storage_dict[self.last_aperture_selected]['solid_geometry']
+            stora = self.storage_dict[self.last_aperture_selected]['geometry']
             self.add_gerber_shape(geo, storage=stora)
 
         # Remove any utility shapes
@@ -3257,35 +3362,36 @@ class FlatCAMGrbEditor(QtCore.QObject):
             # Replot and reset tool.
             self.plot_all()
 
-    def add_gerber_shape(self, shape, storage):
+    def add_gerber_shape(self, shape_element, storage):
         """
         Adds a shape to the shape storage.
 
-        :param shape: Shape to be added.
-        :type shape: DrawToolShape
+        :param shape_element: Shape to be added.
+        :type shape_element: DrawToolShape or DrawToolUtilityShape Geometry is stored as a dict with keys: solid,
+        follow, clear, each value being a list of Shapely objects. The dict can have at least one of the mentioned keys
         :return: None
         """
         # List of DrawToolShape?
 
-        if isinstance(shape, list):
-            for subshape in shape:
+        if isinstance(shape_element, list):
+            for subshape in shape_element:
                 self.add_gerber_shape(subshape, storage)
             return
 
-        assert isinstance(shape, DrawToolShape), \
-            "Expected a DrawToolShape, got %s" % str(type(shape))
+        assert isinstance(shape_element, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % str(type(shape_element))
 
-        assert shape.geo is not None, \
+        assert shape_element.geo is not None, \
             "Shape object has empty geometry (None)"
 
-        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or \
-               not isinstance(shape.geo, list), \
+        assert (isinstance(shape_element.geo, list) and len(shape_element.geo) > 0) or \
+               not isinstance(shape_element.geo, list), \
             "Shape objects has empty geometry ([])"
 
-        if isinstance(shape, DrawToolUtilityShape):
-            self.utility.append(shape)
+        if isinstance(shape_element, DrawToolUtilityShape):
+            self.utility.append(shape_element)
         else:
-            storage.append(shape)  # TODO: Check performance
+            storage.append(shape_element)
 
     def on_canvas_click(self, event):
         """
@@ -3440,9 +3546,10 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.app.delete_selection_shape()
         for storage in self.storage_dict:
             try:
-                for obj in self.storage_dict[storage]['solid_geometry']:
-                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
-                            (sel_type is False and poly_selection.intersects(obj.geo)):
+                for obj in self.storage_dict[storage]['geometry']:
+                    geometric_data = obj.geo['solid']
+                    if (sel_type is True and poly_selection.contains(geometric_data)) or \
+                            (sel_type is False and poly_selection.intersects(geometric_data)):
                         if self.key == self.app.defaults["global_mselect_key"]:
                             if obj in self.selected:
                                 self.selected.remove(obj)
@@ -3562,15 +3669,17 @@ class FlatCAMGrbEditor(QtCore.QObject):
     def draw_utility_geometry(self, geo):
         if type(geo.geo) == list:
             for el in geo.geo:
+                geometric_data = el['solid']
                 # Add the new utility shape
                 self.tool_shape.add(
-                    shape=el, color=(self.app.defaults["global_draw_color"] + '80'),
+                    shape=geometric_data, color=(self.app.defaults["global_draw_color"] + '80'),
                     # face_color=self.app.defaults['global_alt_sel_fill'],
                     update=False, layer=0, tolerance=None)
         else:
+            geometric_data = geo.geo['solid']
             # Add the new utility shape
             self.tool_shape.add(
-                shape=geo.geo,
+                shape=geometric_data,
                 color=(self.app.defaults["global_draw_color"] + '80'),
                 # face_color=self.app.defaults['global_alt_sel_fill'],
                 update=False, layer=0, tolerance=None)
@@ -3590,32 +3699,34 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
             for storage in self.storage_dict:
                 try:
-                    for shape in self.storage_dict[storage]['solid_geometry']:
-                        if shape.geo is None:
+                    for elem in self.storage_dict[storage]['geometry']:
+                        geometric_data = elem.geo['solid']
+                        if geometric_data is None:
                             continue
 
-                        if shape in self.selected:
-                            self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_sel_draw_color'],
-                                            linewidth=2)
+                        if elem in self.selected:
+                            self.plot_shape(geometry=geometric_data,
+                                            color=self.app.defaults['global_sel_draw_color'])
                             continue
-                        self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_draw_color'])
+                        self.plot_shape(geometry=geometric_data,
+                                        color=self.app.defaults['global_draw_color'])
                 except KeyError:
                     pass
 
-            for shape in self.utility:
-                self.plot_shape(geometry=shape.geo, linewidth=1)
+            for elem in self.utility:
+                geometric_data = elem.geo['solid']
+                self.plot_shape(geometry=geometric_data)
                 continue
 
             self.shapes.redraw()
 
-    def plot_shape(self, geometry=None, color='black', linewidth=1):
+    def plot_shape(self, geometry=None, color='black'):
         """
         Plots a geometric object or list of objects without rendering. Plotted objects
         are returned as a list. This allows for efficient/animated rendering.
 
         :param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
         :param color: Shape color
-        :param linewidth: Width of lines in # of pixels.
         :return: List of plotted elements.
         """
         # plot_elements = []
@@ -3671,20 +3782,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         except Exception:
             traceback.print_exc()
 
-    def on_shape_complete(self):
-        self.app.log.debug("on_shape_complete()")
-
-        # Add shape
-        self.add_gerber_shape(self.active_tool.geometry)
-
-        # Remove any utility shapes
-        self.delete_utility_geometry()
-        self.tool_shape.clear(update=True)
-
-        # Replot and reset tool.
-        self.plot_all()
-        # self.active_tool = type(self.active_tool)(self)
-
     def get_selected(self):
         """
         Returns list of shapes that are selected in the editor.
@@ -3708,28 +3805,28 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.build_ui()
         self.app.inform.emit(_("[success] Done. Apertures geometry deleted."))
 
-    def delete_shape(self, shape):
+    def delete_shape(self, geo_el):
         self.is_modified = True
 
-        if shape in self.utility:
-            self.utility.remove(shape)
+        if geo_el in self.utility:
+            self.utility.remove(geo_el)
             return
 
         for storage in self.storage_dict:
             try:
-                if shape in self.storage_dict[storage]['solid_geometry']:
-                    self.storage_dict[storage]['solid_geometry'].remove(shape)
+                if geo_el in self.storage_dict[storage]['geometry']:
+                    self.storage_dict[storage]['geometry'].remove(geo_el)
             except KeyError:
                 pass
-        if shape in self.selected:
-            self.selected.remove(shape)  # TODO: Check performance
+        if geo_el in self.selected:
+            self.selected.remove(geo_el)  # TODO: Check performance
 
     def delete_utility_geometry(self):
         # for_deletion = [shape for shape in self.shape_buffer if shape.utility]
         # for_deletion = [shape for shape in self.storage.get_objects() if shape.utility]
-        for_deletion = [shape for shape in self.utility]
-        for shape in for_deletion:
-            self.delete_shape(shape)
+        for_deletion = [geo_el for geo_el in self.utility]
+        for geo_el in for_deletion:
+            self.delete_shape(geo_el)
 
         self.tool_shape.clear(update=True)
         self.tool_shape.redraw()
@@ -3748,17 +3845,17 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.tools_gerber[toolname]["button"].setChecked(True)
         self.on_tool_select(toolname)
 
-    def set_selected(self, shape):
+    def set_selected(self, geo_el):
 
         # Remove and add to the end.
-        if shape in self.selected:
-            self.selected.remove(shape)
+        if geo_el in self.selected:
+            self.selected.remove(geo_el)
 
-        self.selected.append(shape)
+        self.selected.append(geo_el)
 
-    def set_unselected(self, shape):
-        if shape in self.selected:
-            self.selected.remove(shape)
+    def set_unselected(self, geo_el):
+        if geo_el in self.selected:
+            self.selected.remove(geo_el)
 
     def on_array_type_combo(self):
         if self.array_type_combo.currentIndex() == 0:
@@ -3827,17 +3924,28 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
         join_style = self.buffer_corner_cb.currentIndex() + 1
 
-        def buffer_recursion(geom, selection):
-            if type(geom) == list or type(geom) is MultiPolygon:
+        def buffer_recursion(geom_el, selection):
+            if type(geom_el) == list:
                 geoms = list()
-                for local_geom in geom:
+                for local_geom in geom_el:
                     geoms.append(buffer_recursion(local_geom, selection=selection))
                 return geoms
             else:
-                if geom in selection:
-                    return DrawToolShape(geom.geo.buffer(buff_value, join_style=join_style))
+                if geom_el in selection:
+                    geometric_data = geom_el.geo
+                    buffered_geom_el = dict()
+                    if 'solid' in geom_el:
+                        buffered_geom_el['solid'] = DrawToolShape(geometric_data['solid'].buffer(buff_value,
+                                                                                              join_style=join_style))
+                    if 'follow' in geom_el:
+                        buffered_geom_el['follow'] = DrawToolShape(geometric_data['follow'].buffer(buff_value,
+                                                                                              join_style=join_style))
+                    if 'clear' in geom_el:
+                        buffered_geom_el['clear'] = DrawToolShape(geometric_data['clear'].buffer(buff_value,
+                                                                                              join_style=join_style))
+                    return buffered_geom_el
                 else:
-                    return geom
+                    return geom_el
 
         if not self.apertures_table.selectedItems():
             self.app.inform.emit(_(
@@ -3849,9 +3957,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
             try:
                 apid = self.apertures_table.item(x.row(), 1).text()
 
-                temp_storage = deepcopy(buffer_recursion(self.storage_dict[apid]['solid_geometry'], self.selected))
-                self.storage_dict[apid]['solid_geometry'] = []
-                self.storage_dict[apid]['solid_geometry'] = temp_storage
+                temp_storage = deepcopy(buffer_recursion(self.storage_dict[apid]['geometry'], self.selected))
+                self.storage_dict[apid]['geometry'] = []
+                self.storage_dict[apid]['geometry'] = temp_storage
 
             except Exception as e:
                 log.debug("FlatCAMGrbEditor.buffer() --> %s" % str(e))
@@ -3874,17 +3982,29 @@ class FlatCAMGrbEditor(QtCore.QObject):
                                      "Add it and retry."))
                 return
 
-        def scale_recursion(geom, selection):
-            if type(geom) == list or type(geom) is MultiPolygon:
+        def scale_recursion(geom_el, selection):
+            if type(geom_el) == list:
                 geoms = list()
-                for local_geom in geom:
+                for local_geom in geom_el:
                     geoms.append(scale_recursion(local_geom, selection=selection))
                 return geoms
             else:
-                if geom in selection:
-                    return DrawToolShape(affinity.scale(geom.geo, scale_factor, scale_factor, origin='center'))
+                if geom_el in selection:
+                    geometric_data = geom_el.geo
+                    scaled_geom_el = dict()
+                    if 'solid' in geom_el:
+                        scaled_geom_el['solid'] = DrawToolShape(
+                            affinity.scale(geometric_data['solid'], scale_factor, scale_factor, origin='center'))
+                    if 'follow' in geom_el:
+                        scaled_geom_el['follow'] = DrawToolShape(
+                            affinity.scale(geometric_data['follow'], scale_factor, scale_factor, origin='center'))
+                    if 'clear' in geom_el:
+                        scaled_geom_el['clear'] = DrawToolShape(
+                            affinity.scale(geometric_data['clear'], scale_factor, scale_factor, origin='center'))
+
+                    return scaled_geom_el
                 else:
-                    return geom
+                    return geom_el
 
         if not self.apertures_table.selectedItems():
             self.app.inform.emit(_(
@@ -3896,9 +4016,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
             try:
                 apid = self.apertures_table.item(x.row(), 1).text()
 
-                temp_storage = deepcopy(scale_recursion(self.storage_dict[apid]['solid_geometry'], self.selected))
-                self.storage_dict[apid]['solid_geometry'] = []
-                self.storage_dict[apid]['solid_geometry'] = temp_storage
+                temp_storage = deepcopy(scale_recursion(self.storage_dict[apid]['geometry'], self.selected))
+                self.storage_dict[apid]['geometry'] = []
+                self.storage_dict[apid]['geometry'] = temp_storage
 
             except Exception as e:
                 log.debug("FlatCAMGrbEditor.on_scale() --> %s" % str(e))
@@ -4581,125 +4701,135 @@ class TransformEditorTool(FlatCAMTool):
         return
 
     def on_rotate_action(self, num):
-        shape_list = self.draw_app.selected
+        elem_list = self.draw_app.selected
         xminlist = []
         yminlist = []
         xmaxlist = []
         ymaxlist = []
 
-        if not shape_list:
+        if not elem_list:
             self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to rotate!"))
             return
-        else:
-            with self.app.proc_container.new(_("Appying Rotate")):
-                try:
-                    # first get a bounding box to fit all
-                    for sha in shape_list:
-                        xmin, ymin, xmax, ymax = sha.bounds()
+
+        with self.app.proc_container.new(_("Appying Rotate")):
+            try:
+                # first get a bounding box to fit all; we use only the 'solids' as those should provide the biggest
+                # bounding box
+                for el in elem_list:
+                    if 'solid' in el:
+                        xmin, ymin, xmax, ymax = el['solid'].bounds()
                         xminlist.append(xmin)
                         yminlist.append(ymin)
                         xmaxlist.append(xmax)
                         ymaxlist.append(ymax)
 
-                    # get the minimum x,y and maximum x,y for all objects selected
-                    xminimal = min(xminlist)
-                    yminimal = min(yminlist)
-                    xmaximal = max(xmaxlist)
-                    ymaximal = max(ymaxlist)
-
-                    self.app.progress.emit(20)
-
-                    for sel_sha in shape_list:
-                        px = 0.5 * (xminimal + xmaximal)
-                        py = 0.5 * (yminimal + ymaximal)
-
-                        sel_sha.rotate(-num, point=(px, py))
-                        self.draw_app.plot_all()
-                        # self.draw_app.add_shape(DrawToolShape(sel_sha.geo))
-
-                    # self.draw_app.transform_complete.emit()
-
-                    self.app.inform.emit(_("[success] Done. Rotate completed."))
-
-                    self.app.progress.emit(100)
-
-                except Exception as e:
-                    self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, rotation movement was not executed.") % str(e))
-                    return
+                # get the minimum x,y and maximum x,y for all objects selected
+                xminimal = min(xminlist)
+                yminimal = min(yminlist)
+                xmaximal = max(xmaxlist)
+                ymaximal = max(ymaxlist)
+
+                self.app.progress.emit(20)
+                px = 0.5 * (xminimal + xmaximal)
+                py = 0.5 * (yminimal + ymaximal)
+
+                for sel_el in elem_list:
+                    if 'solid' in sel_el:
+                        sel_el['solid'].rotate(-num, point=(px, py))
+                    if 'follow' in sel_el:
+                        sel_el['follow'].rotate(-num, point=(px, py))
+                    if 'clear' in sel_el:
+                        sel_el['clear'].rotate(-num, point=(px, py))
+                self.draw_app.plot_all()
+
+                self.app.inform.emit(_("[success] Done. Rotate completed."))
+                self.app.progress.emit(100)
+            except Exception as e:
+                self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, rotation movement was not executed.") % str(e))
+                return
 
     def on_flip(self, axis):
-        shape_list = self.draw_app.selected
+        elem_list = self.draw_app.selected
         xminlist = []
         yminlist = []
         xmaxlist = []
         ymaxlist = []
 
-        if not shape_list:
+        if not elem_list:
             self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to flip!"))
             return
-        else:
-            with self.app.proc_container.new(_("Applying Flip")):
-                try:
-                    # get mirroring coords from the point entry
-                    if self.flip_ref_cb.isChecked():
-                        px, py = eval('{}'.format(self.flip_ref_entry.text()))
-                    # get mirroing coords from the center of an all-enclosing bounding box
-                    else:
-                        # first get a bounding box to fit all
-                        for sha in shape_list:
-                            xmin, ymin, xmax, ymax = sha.bounds()
+
+        with self.app.proc_container.new(_("Applying Flip")):
+            try:
+                # get mirroring coords from the point entry
+                if self.flip_ref_cb.isChecked():
+                    px, py = eval('{}'.format(self.flip_ref_entry.text()))
+                # get mirroing coords from the center of an all-enclosing bounding box
+                else:
+                    # first get a bounding box to fit all; we use only the 'solids' as those should provide the biggest
+                    # bounding box
+                    for el in elem_list:
+                        if 'solid' in el:
+                            xmin, ymin, xmax, ymax = el['solid'].bounds()
                             xminlist.append(xmin)
                             yminlist.append(ymin)
                             xmaxlist.append(xmax)
                             ymaxlist.append(ymax)
 
-                        # get the minimum x,y and maximum x,y for all objects selected
-                        xminimal = min(xminlist)
-                        yminimal = min(yminlist)
-                        xmaximal = max(xmaxlist)
-                        ymaximal = max(ymaxlist)
-
-                        px = 0.5 * (xminimal + xmaximal)
-                        py = 0.5 * (yminimal + ymaximal)
-
-                    self.app.progress.emit(20)
-
-                    # execute mirroring
-                    for sha in shape_list:
-                        if axis is 'X':
-                            sha.mirror('X', (px, py))
-                            self.app.inform.emit(_('[success] Flip on the Y axis done ...'))
-                        elif axis is 'Y':
-                            sha.mirror('Y', (px, py))
-                            self.app.inform.emit(_('[success] Flip on the X axis done ...'))
-                        self.draw_app.plot_all()
-
-                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
-                    #
-                    # self.draw_app.transform_complete.emit()
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    xmaximal = max(xmaxlist)
+                    ymaximal = max(ymaxlist)
 
-                    self.app.progress.emit(100)
+                    px = 0.5 * (xminimal + xmaximal)
+                    py = 0.5 * (yminimal + ymaximal)
+
+                self.app.progress.emit(20)
+
+                # execute mirroring
+                for sel_el in elem_list:
+                    if axis is 'X':
+                        if 'solid' in sel_el:
+                            sel_el['solid'].mirror('X', (px, py))
+                        if 'follow' in sel_el:
+                            sel_el['follow'].mirror('X', (px, py))
+                        if 'clear' in sel_el:
+                            sel_el['clear'].mirror('X', (px, py))
+                        self.app.inform.emit(_('[success] Flip on the Y axis done ...'))
+                    elif axis is 'Y':
+                        if 'solid' in sel_el:
+                            sel_el['solid'].mirror('Y', (px, py))
+                        if 'follow' in sel_el:
+                            sel_el['follow'].mirror('Y', (px, py))
+                        if 'clear' in sel_el:
+                            sel_el['clear'].mirror('Y', (px, py))
+                        self.app.inform.emit(_('[success] Flip on the X axis done ...'))
+                self.draw_app.plot_all()
+                self.app.progress.emit(100)
 
-                except Exception as e:
-                    self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, Flip action was not executed.") % str(e))
-                    return
+            except Exception as e:
+                self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, Flip action was not executed.") % str(e))
+                return
 
     def on_skew(self, axis, num):
-        shape_list = self.draw_app.selected
+        elem_list = self.draw_app.selected
         xminlist = []
         yminlist = []
 
-        if not shape_list:
+        if not elem_list:
             self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to shear/skew!"))
             return
         else:
             with self.app.proc_container.new(_("Applying Skew")):
                 try:
-                    # first get a bounding box to fit all
-                    for sha in shape_list:
-                        xmin, ymin, xmax, ymax = sha.bounds()
-                        xminlist.append(xmin)
-                        yminlist.append(ymin)
+                    # first get a bounding box to fit all; we use only the 'solids' as those should provide the biggest
+                    # bounding box
+                    for el in elem_list:
+                        if 'solid' in el:
+                            xmin, ymin, xmax, ymax = el['solid'].bounds()
+                            xminlist.append(xmin)
+                            yminlist.append(ymin)
 
                     # get the minimum x,y and maximum x,y for all objects selected
                     xminimal = min(xminlist)
@@ -4707,16 +4837,22 @@ class TransformEditorTool(FlatCAMTool):
 
                     self.app.progress.emit(20)
 
-                    for sha in shape_list:
+                    for sel_el in elem_list:
                         if axis is 'X':
-                            sha.skew(num, 0, point=(xminimal, yminimal))
+                            if 'solid' in sel_el:
+                                sel_el['solid'].skew(num, 0, point=(xminimal, yminimal))
+                            if 'follow' in sel_el:
+                                sel_el['follow'].skew(num, 0, point=(xminimal, yminimal))
+                            if 'clear' in sel_el:
+                                sel_el['clear'].skew(num, 0, point=(xminimal, yminimal))
                         elif axis is 'Y':
-                            sha.skew(0, num, point=(xminimal, yminimal))
-                        self.draw_app.plot_all()
-
-                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
-                    #
-                    # self.draw_app.transform_complete.emit()
+                            if 'solid' in sel_el:
+                                sel_el['solid'].skew(0, num, point=(xminimal, yminimal))
+                            if 'follow' in sel_el:
+                                sel_el['follow'].skew(0, num, point=(xminimal, yminimal))
+                            if 'clear' in sel_el:
+                                sel_el['clear'].skew(0, num, point=(xminimal, yminimal))
+                    self.draw_app.plot_all()
 
                     self.app.inform.emit(_('[success] Skew on the %s axis done ...') % str(axis))
                     self.app.progress.emit(100)
@@ -4726,25 +4862,27 @@ class TransformEditorTool(FlatCAMTool):
                     return
 
     def on_scale(self, axis, xfactor, yfactor, point=None):
-        shape_list = self.draw_app.selected
+        elem_list = self.draw_app.selected
         xminlist = []
         yminlist = []
         xmaxlist = []
         ymaxlist = []
 
-        if not shape_list:
+        if not elem_list:
             self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to scale!"))
             return
         else:
             with self.app.proc_container.new(_("Applying Scale")):
                 try:
-                    # first get a bounding box to fit all
-                    for sha in shape_list:
-                        xmin, ymin, xmax, ymax = sha.bounds()
-                        xminlist.append(xmin)
-                        yminlist.append(ymin)
-                        xmaxlist.append(xmax)
-                        ymaxlist.append(ymax)
+                    # first get a bounding box to fit all; we use only the 'solids' as those should provide the biggest
+                    # bounding box
+                    for el in elem_list:
+                        if 'solid' in el:
+                            xmin, ymin, xmax, ymax = el['solid'].bounds()
+                            xminlist.append(xmin)
+                            yminlist.append(ymin)
+                            xmaxlist.append(xmax)
+                            ymaxlist.append(ymax)
 
                     # get the minimum x,y and maximum x,y for all objects selected
                     xminimal = min(xminlist)
@@ -4761,13 +4899,14 @@ class TransformEditorTool(FlatCAMTool):
                         px = 0
                         py = 0
 
-                    for sha in shape_list:
-                        sha.scale(xfactor, yfactor, point=(px, py))
-                        self.draw_app.plot_all()
-
-                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
-                    #
-                    # self.draw_app.transform_complete.emit()
+                    for sel_el in elem_list:
+                        if 'solid' in sel_el:
+                            sel_el['solid'].scale(xfactor, yfactor, point=(px, py))
+                        if 'follow' in sel_el:
+                            sel_el['follow'].scale(xfactor, yfactor, point=(px, py))
+                        if 'clear' in sel_el:
+                            sel_el['clear'].scale(xfactor, yfactor, point=(px, py))
+                    self.draw_app.plot_all()
 
                     self.app.inform.emit(_('[success] Scale on the %s axis done ...') % str(axis))
                     self.app.progress.emit(100)
@@ -4776,38 +4915,33 @@ class TransformEditorTool(FlatCAMTool):
                     return
 
     def on_offset(self, axis, num):
-        shape_list = self.draw_app.selected
-        xminlist = []
-        yminlist = []
+        elem_list = self.draw_app.selected
 
-        if not shape_list:
+        if not elem_list:
             self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to offset!"))
             return
         else:
             with self.app.proc_container.new(_("Applying Offset")):
                 try:
-                    # first get a bounding box to fit all
-                    for sha in shape_list:
-                        xmin, ymin, xmax, ymax = sha.bounds()
-                        xminlist.append(xmin)
-                        yminlist.append(ymin)
-
-                    # get the minimum x,y and maximum x,y for all objects selected
-                    xminimal = min(xminlist)
-                    yminimal = min(yminlist)
                     self.app.progress.emit(20)
 
-                    for sha in shape_list:
+                    for sel_el in elem_list:
                         if axis is 'X':
-                            sha.offset((num, 0))
+                            if 'solid' in sel_el:
+                                sel_el['solid'].offset((num, 0))
+                            if 'follow' in sel_el:
+                                sel_el['follow'].offset((num, 0))
+                            if 'clear' in sel_el:
+                                sel_el['clear'].offset((num, 0))
                         elif axis is 'Y':
-                            sha.offset((0, num))
+                            if 'solid' in sel_el:
+                                sel_el['solid'].offset((0, num))
+                            if 'follow' in sel_el:
+                                sel_el['follow'].offset((0, num))
+                            if 'clear' in sel_el:
+                                sel_el['clear'].offset((0, num))
                         self.draw_app.plot_all()
 
-                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
-                    #
-                    # self.draw_app.transform_complete.emit()
-
                     self.app.inform.emit(_('[success] Offset on the %s axis done ...') % str(axis))
                     self.app.progress.emit(100)