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

- Gerber Editor - working in conversion to the new data format

Marius Stanciu 6 лет назад
Родитель
Сommit
ce0ed2208f
2 измененных файлов с 362 добавлено и 242 удалено
  1. 4 0
      README.md
  2. 358 242
      flatcamEditors/FlatCAMGrbEditor.py

+ 4 - 0
README.md

@@ -9,6 +9,10 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+10.05.2019
+
+- Gerber Editor - working in conversion to the new data format
+
 9.05.2019
 
 - rework the Gerber parser

+ 358 - 242
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()
@@ -2660,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)
@@ -2914,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
@@ -2983,17 +3098,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 for k, v in self.gerber_obj.apertures[apid].items():
                     try:
                         if k == 'geometry':
-                            for el_dict in v:
-                                if el_dict:
-                                    new_el_dict = dict()
-                                    if 'solid' in el_dict:
-                                        new_el_dict['solid'] =DrawToolShape(deepcopy(el_dict['solid']))
-                                    if 'follow' in el_dict:
-                                        new_el_dict['follow'] =DrawToolShape(deepcopy(el_dict['follow']))
-                                    if 'clear' in el_dict:
-                                        new_el_dict['clear'] =DrawToolShape(deepcopy(el_dict['clear']))
-                                    storage_elem.append(new_el_dict)
-                                    self.add_gerber_shape(DrawToolShape(new_el_dict['solid']))
+                            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] = self.gerber_obj.apertures[apid][k]
@@ -3101,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)
 
@@ -3218,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))
@@ -3230,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
@@ -3243,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
@@ -3254,33 +3362,36 @@ class FlatCAMGrbEditor(QtCore.QObject):
             # Replot and reset tool.
             self.plot_all()
 
-    def add_gerber_shape(self, shape):
+    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:
-                self.add_gerber_shape(subshape)
+        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_element)
 
     def on_canvas_click(self, event):
         """
@@ -3435,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)
@@ -3557,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)
@@ -3586,32 +3700,33 @@ class FlatCAMGrbEditor(QtCore.QObject):
             for storage in self.storage_dict:
                 try:
                     for elem in self.storage_dict[storage]['geometry']:
-                        if elem['solid'].geo is None:
+                        geometric_data = elem.geo['solid']
+                        if geometric_data is None:
                             continue
 
-                        if elem['solid'] in self.selected:
-                            self.plot_shape(geometry=elem['solid'].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=elem['solid'].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 elem in self.utility:
-                self.plot_shape(geometry=elem['solid'].geo, linewidth=1)
+                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 = []
@@ -3667,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.
@@ -3831,15 +3932,16 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 return geoms
             else:
                 if geom_el in selection:
+                    geometric_data = geom_el.geo
                     buffered_geom_el = dict()
                     if 'solid' in geom_el:
-                        buffered_geom_el['solid'] = DrawToolShape(geom_el['solid'].geo.buffer(buff_value,
+                        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(geom_el['follow'].geo.buffer(buff_value,
+                        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(geom_el['clear'].geo.buffer(buff_value,
+                        buffered_geom_el['clear'] = DrawToolShape(geometric_data['clear'].buffer(buff_value,
                                                                                               join_style=join_style))
                     return buffered_geom_el
                 else:
@@ -3888,16 +3990,17 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 return geoms
             else:
                 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(geom_el['solid'].geo, scale_factor, scale_factor, origin='center'))
+                            affinity.scale(geometric_data['solid'], scale_factor, scale_factor, origin='center'))
                     if 'follow' in geom_el:
                         scaled_geom_el['follow'] = DrawToolShape(
-                            affinity.scale(geom_el['follow'].geo, scale_factor, scale_factor, origin='center'))
+                            affinity.scale(geometric_data['follow'], scale_factor, scale_factor, origin='center'))
                     if 'clear' in geom_el:
                         scaled_geom_el['clear'] = DrawToolShape(
-                            affinity.scale(geom_el['clear'].geo, scale_factor, scale_factor, origin='center'))
+                            affinity.scale(geometric_data['clear'], scale_factor, scale_factor, origin='center'))
 
                     return scaled_geom_el
                 else:
@@ -4607,117 +4710,126 @@ class TransformEditorTool(FlatCAMTool):
         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; we use only the 'solids' as those should provide the biggest
-                    # bounding box
-                    for geo_el in elem_list:
-                        xmin, ymin, xmax, ymax = geo_el..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)
@@ -4725,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)
@@ -4744,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)
@@ -4779,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)
@@ -4794,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)