ソースを参照

Excellon parser bug fixed, improved Gerber parse, added icons, added some tooltips.

Juan Pablo Caram 12 年 前
コミット
68a275e042
7 ファイル変更405 行追加148 行削除
  1. 108 23
      FlatCAM.py
  2. 27 3
      FlatCAM.ui
  3. 270 122
      camlib.py
  4. BIN
      share/flatcam_icon16.png
  5. BIN
      share/flatcam_icon256.png
  6. BIN
      share/flatcam_icon32.png
  7. BIN
      share/flatcam_icon48.png

+ 108 - 23
FlatCAM.py

@@ -11,6 +11,7 @@ import threading
 # TODO: Bundle together. This is just for debugging.
 from gi.repository import Gtk
 from gi.repository import Gdk
+from gi.repository import GdkPixbuf
 from gi.repository import GLib
 from gi.repository import GObject
 import simplejson as json
@@ -23,6 +24,7 @@ from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCan
 
 from camlib import *
 import sys
+import urllib
 
 
 ########################################
@@ -129,7 +131,7 @@ class FlatCAMObj:
 
     def set_form_item(self, option):
         """
-        Copies the specified options to the UI form.
+        Copies the specified option to the UI form.
 
         :param option: Name of the option (Key in ``self.options``).
         :type option: str
@@ -150,6 +152,13 @@ class FlatCAMObj:
         print "Unknown kind of form item:", fkind
 
     def read_form_item(self, option):
+        """
+        Reads the specified option from the UI form into ``self.options``.
+
+        :param option: Name of the option.
+        :type option: str
+        :return: None
+        """
         fkind = self.form_kinds[option]
         fname = fkind + "_" + self.kind + "_" + option
 
@@ -175,25 +184,23 @@ class FlatCAMObj:
         # Creates the axes if necessary and sets them up.
         self.setup_axes(figure)
 
-        # Clear axes.
-        # self.axes.cla()
-        # return
-
     def serialize(self):
         """
         Returns a representation of the object as a dictionary so
         it can be later exported as JSON. Override this method.
-        @return: Dictionary representing the object
-        @rtype: dict
+
+        :return: Dictionary representing the object
+        :rtype: dict
         """
         return
 
     def deserialize(self, obj_dict):
         """
         Re-builds an object from its serialized version.
-        @param obj_dict: Dictionary representing a FlatCAMObj
-        @type obj_dict: dict
-        @return None
+
+        :param obj_dict: Dictionary representing a FlatCAMObj
+        :type obj_dict: dict
+        :return None
         """
         return
 
@@ -257,6 +264,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         :return: None
         :rtype: None
         """
+
         factor = Gerber.convert_units(self, units)
 
         self.options['isotooldia'] *= factor
@@ -317,7 +325,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 class FlatCAMExcellon(FlatCAMObj, Excellon):
     """
-    Represents Excellon code.
+    Represents Excellon/Drill code.
     """
 
     def __init__(self, name):
@@ -367,7 +375,12 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         if not self.options["plot"]:
             return
 
-        # Plot excellon
+        try:
+            _ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
+        # Plot excellon (All polygons?)
         for geo in self.solid_geometry:
             x, y = geo.exterior.coords.xy
             self.axes.plot(x, y, 'r-')
@@ -446,6 +459,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         print "FlatCAMCNCjob.convert_units()"
         self.options["tooldia"] *= factor
 
+
 class FlatCAMGeometry(FlatCAMObj, Geometry):
     """
     Geometric object not associated with a specific
@@ -596,12 +610,11 @@ class App:
         # Needed to interact with the GUI from other threads.
         GObject.threads_init()
 
-        ## GUI ##
+        #### GUI ####
         self.gladefile = "FlatCAM.ui"
         self.builder = Gtk.Builder()
         self.builder.add_from_file(self.gladefile)
         self.window = self.builder.get_object("window1")
-        self.window.set_title("FlatCAM - Alpha 1 UNSTABLE - Check for updates!")
         self.position_label = self.builder.get_object("label3")
         self.grid = self.builder.get_object("grid1")
         self.notebook = self.builder.get_object("notebook1")
@@ -613,22 +626,23 @@ class App:
         # White (transparent) background on the "Options" tab.
         self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
                                                                         Gdk.RGBA(1, 1, 1, 1))
-
         # Combo box to choose between project and application options.
         self.combo_options = self.builder.get_object("combo_options")
         self.combo_options.set_active(1)
 
-        ## Event handling ##
+        self.setup_project_list()  # The "Project" tab
+        self.setup_component_editor()  # The "Selected" tab
+
+        #### Event handling ####
         self.builder.connect_signals(self)
 
-        ## Make plot area ##
+        #### Make plot area ####
         self.figure = None
         self.axes = None
         self.canvas = None
         self.setup_plot()
 
-        self.setup_project_list()  # The "Project" tab
-        self.setup_component_editor()  # The "Selected" tab
+        self.setup_tooltips()
 
         #### DATA ####
         self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
@@ -663,8 +677,6 @@ class App:
         self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
                            "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
 
-        # self.combos = []
-
         # Options for each kind of FlatCAMObj.
         # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
         for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
@@ -677,22 +689,34 @@ class App:
 
         self.plot_click_subscribers = {}
 
-        # Initialization
+        #### Initialization ####
         self.load_defaults()
         self.options.update(self.defaults)  # Copy app defaults to project options
         self.options2form()  # Populate the app defaults form
         self.units_label.set_text("[" + self.options["units"] + "]")
 
-        # For debugging only
+        #### Check for updates ####
+        self.version = 1
+        t1 = threading.Thread(target=self.versionCheck)
+        t1.daemon = True
+        t1.start()
+
+        #### For debugging only ###
         def someThreadFunc(self):
             print "Hello World!"
 
         t = threading.Thread(target=someThreadFunc, args=(self,))
+        t.daemon = True
         t.start()
 
         ########################################
         ##              START                 ##
         ########################################
+        self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
+        self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
+        self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
+        Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
+        self.window.set_title("FlatCAM - Alpha 2 UNSTABLE - Check for updates!")
         self.window.set_default_size(900, 600)
         self.window.show_all()
 
@@ -1309,6 +1333,67 @@ class App:
         for obj in self.stuff:
             combo.append_text(obj)
 
+    def versionCheck(self):
+        """
+        Checks for the latest version of the program. Alerts the
+        user if theirs is outdated. This method is meant to be run
+        in a saeparate thread.
+
+        :return: None
+        """
+
+        try:
+            f = urllib.urlopen("http://caram.cl/flatcam/VERSION")  # TODO: Hardcoded.
+        except:
+            GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
+            return
+
+        try:
+            data = json.load(f)
+        except:
+            GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
+            f.close()
+            return
+
+        f.close()
+
+        if self.version >= data["version"]:
+            GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
+            return
+
+        label = Gtk.Label("There is a newer version of FlatCAM\n" +
+                          "available for download:\n\n" +
+                          data["name"] + "\n\n" + data["message"])
+        dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
+                            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                             Gtk.STOCK_OK, Gtk.ResponseType.OK))
+        dialog.set_default_size(150, 100)
+        dialog.set_modal(True)
+        box = dialog.get_content_area()
+        box.set_border_width(10)
+        box.add(label)
+
+        def do_dialog():
+            dialog.show_all()
+            response = dialog.run()
+            dialog.destroy()
+
+        GLib.idle_add(lambda: do_dialog())
+
+        return
+
+    def setup_tooltips(self):
+        tooltips = {
+            "cb_gerber_plot": "Plot this object on the main window.",
+            "cb_gerber_mergepolys": "Show overlapping polygons as single.",
+            "cb_gerber_solid": "Paint inside polygons.",
+            "cb_gerber_multicolored": "Draw polygons with different polygons.",
+            "button1": ""
+        }
+
+        for widget in tooltips:
+            self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
+
     ########################################
     ##         EVENT HANDLERS             ##
     ########################################

+ 27 - 3
FlatCAM.ui

@@ -3045,7 +3045,8 @@ to application defaults.</property>
               <object class="GtkToolButton" id="zoomfit_toolbutton">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="tooltip_text" translatable="yes">Zoom Fit</property>
+                <property name="tooltip_markup" translatable="yes">Zoom Fit.
+(Click on plot and hit &lt;b&gt;1&lt;/b&gt;)</property>
                 <property name="label" translatable="yes">Fit</property>
                 <property name="use_underline">True</property>
                 <property name="stock_id">gtk-zoom-100</property>
@@ -3060,7 +3061,9 @@ to application defaults.</property>
               <object class="GtkToolButton" id="zoomin_toolbutton">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="tooltip_text" translatable="yes">Zoom+</property>
+                <property name="tooltip_markup" translatable="yes">Zoom in.
+(Click on plot and hit &lt;b&gt;3&lt;/b&gt;
+to zoom around a point)</property>
                 <property name="label" translatable="yes">Zoom+</property>
                 <property name="use_underline">True</property>
                 <property name="stock_id">gtk-zoom-in</property>
@@ -3075,7 +3078,9 @@ to application defaults.</property>
               <object class="GtkToolButton" id="zoomout_toolbutton">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="tooltip_text" translatable="yes">Zoom-</property>
+                <property name="tooltip_markup" translatable="yes">Zoom Out.
+(Click on plot and hit &lt;b&gt;2&lt;/b&gt;
+to zoom around a point)</property>
                 <property name="label" translatable="yes">Zoom-</property>
                 <property name="use_underline">True</property>
                 <property name="stock_id">gtk-zoom-out</property>
@@ -3090,6 +3095,7 @@ to application defaults.</property>
               <object class="GtkToolButton" id="Clear">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
+                <property name="tooltip_markup" translatable="yes">Clear Plot</property>
                 <property name="label" translatable="yes">Clear Plots</property>
                 <property name="use_underline">True</property>
                 <property name="stock_id">gtk-stop</property>
@@ -3104,6 +3110,7 @@ to application defaults.</property>
               <object class="GtkToolButton" id="toolbutton1">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
+                <property name="tooltip_markup" translatable="yes">Re-plot all</property>
                 <property name="label" translatable="yes">Re-plot</property>
                 <property name="use_underline">True</property>
                 <property name="stock_id">gtk-redo</property>
@@ -3118,6 +3125,8 @@ to application defaults.</property>
               <object class="GtkToolButton" id="toolbutton2">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
+                <property name="tooltip_markup" translatable="yes">Delete selected
+object.</property>
                 <property name="label" translatable="yes">Delete Object</property>
                 <property name="use_underline">True</property>
                 <property name="stock_id">gtk-delete</property>
@@ -3165,6 +3174,7 @@ to application defaults.</property>
                   <object class="GtkLabel" id="label1">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
+                    <property name="tooltip_markup" translatable="yes">Objects in the project.</property>
                     <property name="label" translatable="yes">Project</property>
                   </object>
                   <packing>
@@ -3190,6 +3200,8 @@ to application defaults.</property>
                   <object class="GtkLabel" id="label2">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
+                    <property name="tooltip_markup" translatable="yes">Options and action
+for the current object.</property>
                     <property name="label" translatable="yes">Selected</property>
                   </object>
                   <packing>
@@ -3229,6 +3241,15 @@ to application defaults.</property>
                                   <object class="GtkComboBoxText" id="combo_options">
                                     <property name="visible">True</property>
                                     <property name="can_focus">False</property>
+                                    <property name="tooltip_markup" translatable="yes">Application defaults get transfered
+to every new project. Project options
+get inherited by new project objects.
+
+&lt;b&gt;Save&lt;/b&gt; application defaults
+by choosing &lt;i&gt;File + Save defaults&lt;/i&gt;.
+
+Project obtions are saved with the
+project.</property>
                                     <property name="margin_left">10</property>
                                     <property name="margin_right">10</property>
                                     <property name="margin_top">5</property>
@@ -4444,6 +4465,8 @@ to application defaults.</property>
                   <object class="GtkLabel" id="label5">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
+                    <property name="tooltip_markup" translatable="yes">Project and application
+defaults.</property>
                     <property name="label" translatable="yes">Options</property>
                   </object>
                   <packing>
@@ -4468,6 +4491,7 @@ to application defaults.</property>
                   <object class="GtkLabel" id="label88">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
+                    <property name="tooltip_markup" translatable="yes">Active tool</property>
                     <property name="label" translatable="yes">Tool</property>
                   </object>
                   <packing>

+ 270 - 122
camlib.py

@@ -348,6 +348,9 @@ class Gerber (Geometry):
         # LP - Level polarity
         self.lpol_re = re.compile(r'^%LP([DC])\*%$')
 
+        # TODO: This is bad.
+        self.steps_per_circ = 40
+
     def scale(self, factor):
         """
         Scales the objects' geometry on the XY plane by a given factor.
@@ -453,8 +456,12 @@ class Gerber (Geometry):
 
         self.buffered_paths = []
         for path in self.paths:
-            width = self.apertures[path["aperture"]]["size"]
-            self.buffered_paths.append(path["linestring"].buffer(width/2))
+            try:
+                width = self.apertures[path["aperture"]]["size"]
+                self.buffered_paths.append(path["linestring"].buffer(width/2))
+            except KeyError:
+                print "ERROR: Failed to buffer path: ", path
+                print "Apertures: ", self.apertures
     
     def aperture_parse(self, gline):
         """
@@ -523,7 +530,7 @@ class Gerber (Geometry):
         :rtype: None
         """
 
-        path = []  # Coordinates of the current path
+        path = []  # Coordinates of the current path, each is [x, y]
 
         last_path_aperture = None
         current_aperture = None
@@ -540,11 +547,25 @@ class Gerber (Geometry):
         current_x = None
         current_y = None
 
+        # How to interprest circular interpolation: SINGLE or MULTI
+        quadrant_mode = None
+
+        line_num = 0
         for gline in glines:
+            line_num += 1
 
-            # Linear interpolation plus flashes
+            ## G01 - Linear interpolation plus flashes
+            # Operation code (D0x) missing is deprecated... oh well I will support it.
             match = self.lin_re.search(gline)
             if match:
+                # Dxx alone? Will ignore for now.
+                if match.group(1) is None and match.group(2) is None and match.group(3) is None:
+                    try:
+                        current_operation_code = int(match.group(4))
+                    except:
+                        pass  # A line with just * will match too.
+                    continue
+
                 # Parse coordinates
                 if match.group(2) is not None:
                     current_x = parse_gerber_number(match.group(2), self.frac_digits)
@@ -553,69 +574,141 @@ class Gerber (Geometry):
 
                 # Parse operation code
                 if match.group(4) is not None:
-                    current_operation_code = match.group(4)
+                    current_operation_code = int(match.group(4))
 
                 # Pen down: add segment
-                if current_operation_code == '1':
+                if current_operation_code == 1:
                     path.append([current_x, current_y])
                     last_path_aperture = current_aperture
 
                 # Pen up: finish path
-                elif current_operation_code == '2':
+                elif current_operation_code == 2:
                     if len(path) > 1:
+                        if last_path_aperture is None:
+                            print "Warning: No aperture defined for curent path. (%d)" % line_num
                         self.paths.append({"linestring": LineString(path),
                                            "aperture": last_path_aperture})
-                    path = [[current_x, current_y]]
+                    path = [[current_x, current_y]]  # Start new path
 
                 # Flash
-                elif current_operation_code == '3':
+                elif current_operation_code == 3:
                     self.flashes.append({"loc": [current_x, current_y],
                                          "aperture": current_aperture})
 
                 continue
 
-            # if gline.find("D01*") != -1:  # pen down
-            #     path.append(parse_gerber_coords(gline, self.int_digits, self.frac_digits))
-            #     last_path_aperture = current_aperture
-            #     continue
-            #
-            # if gline.find("D02*") != -1:  # pen up
-            #     if len(path) > 1:
-            #         # Path completed, create shapely LineString
-            #         self.paths.append({"linestring": LineString(path),
-            #                            "aperture": last_path_aperture})
-            #     path = [parse_gerber_coords(gline, self.int_digits, self.frac_digits)]
-            #     continue
-            #
-            # indexd3 = gline.find("D03*")
-            # if indexd3 > 0:  # Flash
-            #     self.flashes.append({"loc": parse_gerber_coords(gline, self.int_digits, self.frac_digits),
-            #                          "aperture": current_aperture})
-            #     continue
-            # if indexd3 == 0:  # Flash?
-            #     print "WARNING: Uninplemented flash style:", gline
-            #     continue
-
-            # End region
+            ## G02/3 - Circular interpolation
+            # 2-clockwise, 3-counterclockwise
+            match = self.circ_re.search(gline)
+            if match:
+
+                mode, x, y, i, j, d = match.groups()
+                try:
+                    x = parse_gerber_number(x, self.frac_digits)
+                except:
+                    x = current_x
+                try:
+                    y = parse_gerber_number(y, self.frac_digits)
+                except:
+                    y = current_y
+                try:
+                    i = parse_gerber_number(i, self.frac_digits)
+                except:
+                    i = 0
+                try:
+                    j = parse_gerber_number(j, self.frac_digits)
+                except:
+                    j = 0
+
+                if quadrant_mode is None:
+                    print "ERROR: Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num
+                    print gline
+                    continue
+
+                if mode is None and current_interpolation_mode not in [2, 3]:
+                    print "ERROR: Found arc without circular interpolation mode defined. (%d)" % line_num
+                    print gline
+                    continue
+                elif mode is not None:
+                    current_interpolation_mode = int(mode)
+
+                # Set operation code if provided
+                if d is not None:
+                    current_operation_code = int(d)
+
+                # Nothing created! Pen Up.
+                if current_operation_code == 2:
+                    print "Warning: Arc with D2. (%d)" % line_num
+                    if len(path) > 1:
+                        if last_path_aperture is None:
+                            print "Warning: No aperture defined for curent path. (%d)" % line_num
+                        self.paths.append({"linestring": LineString(path),
+                                           "aperture": last_path_aperture})
+                    current_x = x
+                    current_y = y
+                    path = [[current_x, current_y]]  # Start new path
+                    continue
+
+                # Flash should not happen here
+                if current_operation_code == 3:
+                    print "ERROR: Trying to flash within arc. (%d)" % line_num
+                    continue
+
+                if quadrant_mode == 'MULTI':
+                    center = [i + current_x, j + current_y]
+                    radius = sqrt(i**2 + j**2)
+                    start = arctan2(-j, -i)
+                    stop = arctan2(-center[1] + y, -center[0] + x)
+                    arcdir = [None, None, "cw", "ccw"]
+                    this_arc = arc(center, radius, start, stop,
+                                   arcdir[current_interpolation_mode],
+                                   self.steps_per_circ)
+
+                    # Last point in path is current point
+                    current_x = this_arc[-1][0]
+                    current_y = this_arc[-1][1]
+
+                    # Append
+                    path += this_arc
+
+                    last_path_aperture = current_aperture
+
+                    continue
+
+                if quadrant_mode == 'SINGLE':
+                    print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num
+
+            ## G74/75* - Single or multiple quadrant arcs
+            match = self.quad_re.search(gline)
+            if match:
+                if match.group(1) == '4':
+                    quadrant_mode = 'SINGLE'
+                else:
+                    quadrant_mode = 'MULTI'
+                continue
+
+            ## G37* - End region
             if self.regionoff_re.search(gline):
                 # Only one path defines region?
+                if len(path) < 3:
+                    print "ERROR: Path contains less than 3 points:"
+                    print path
+                    print "Line (%d): " % line_num, gline
+                    path = []
+                    continue
+
+                # For regions we may ignore an aperture that is None
                 self.regions.append({"polygon": Polygon(path),
                                      "aperture": last_path_aperture})
-                path = []
+                #path = []
+                path = [[current_x, current_y]]  # Start new path
                 continue
-
-            # if gline.find("G37*") != -1:  # end region
-            #     # Only one path defines region?
-            #     self.regions.append({"polygon": Polygon(path),
-            #                          "aperture": last_path_aperture})
-            #     path = []
-            #     continue
             
             if gline.find("%ADD") != -1:  # aperture definition
                 self.aperture_parse(gline)  # adds element to apertures
                 continue
 
-            # Interpolation mode change
+            ## G01/2/3* - Interpolation mode change
             # Can occur along with coordinates and operation code but
             # sometimes by itself (handled here).
             # Example: G01*
@@ -624,22 +717,15 @@ class Gerber (Geometry):
                 current_interpolation_mode = int(match.group(1))
                 continue
 
-            # Tool/aperture change
+            ## Tool/aperture change
             # Example: D12*
             match = self.tool_re.search(gline)
             if match:
                 current_aperture = match.group(1)
                 continue
 
-            # indexstar = gline.find("*")
-            # if gline.find("D") == 0:  # Aperture change
-            #     current_aperture = gline[1:indexstar]
-            #     continue
-            # if gline.find("G54D") == 0:  # Aperture change (deprecated)
-            #     current_aperture = gline[4:indexstar]
-            #     continue
-
-            # Number format
+            ## Number format
+            # Example: %FSLAX24Y24*%
             # TODO: This is ignoring most of the format. Implement the rest.
             match = self.fmt_re.search(gline)
             if match:
@@ -647,30 +733,20 @@ class Gerber (Geometry):
                 self.frac_digits = int(match.group(4))
                 continue
 
-            # if gline.find("%FS") != -1:  # Format statement
-            #     indexx = gline.find("X")
-            #     self.int_digits = int(gline[indexx + 1])
-            #     self.frac_digits = int(gline[indexx + 2])
-            #     continue
-
-            # Mode (IN/MM)
+            ## Mode (IN/MM)
+            # Example: %MOIN*%
             match = self.mode_re.search(gline)
             if match:
                 self.units = match.group(1)
                 continue
 
-            print "WARNING: Line ignored:", gline
+            print "WARNING: Line ignored (%d):" % line_num, gline
         
         if len(path) > 1:
             # EOF, create shapely LineString if something still in path
             self.paths.append({"linestring": LineString(path),
                                "aperture": last_path_aperture})
 
-        # if len(path) > 1:
-        #     # EOF, create shapely LineString if something still in path
-        #     self.paths.append({"linestring": LineString(path),
-        #                        "aperture": current_aperture})
-
     def do_flashes(self):
         """
         Creates geometry for Gerber flashes (aperture on a single point).
@@ -778,6 +854,7 @@ class Excellon(Geometry):
     def __init__(self):
         """
         The constructor takes no parameters.
+
         :return: Excellon object.
         :rtype: Excellon
         """
@@ -795,8 +872,74 @@ class Excellon(Geometry):
         # Always append to it because it carries contents
         # from Geometry.
         self.ser_attrs += ['tools', 'drills', 'zeros']
+
+        #### Patterns ####
+        # Regex basics:
+        # ^ - beginning
+        # $ - end
+        # *: 0 or more, +: 1 or more, ?: 0 or 1
+
+        # M48 - Beggining of Part Program Header
+        self.hbegin_re = re.compile(r'^M48$')
+
+        # M95 or % - End of Part Program Header
+        # NOTE: % has different meaning in the body
+        self.hend_re = re.compile(r'^(?:M95|%)$')
+
+        # FMAT Excellon format
+        self.fmat_re = re.compile(r'^FMAT,([12])$')
+
+        # Number format and units
+        # INCH uses 6 digits
+        # METRIC uses 5/6
+        self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
+
+        # Tool definition/parameters (?= is look-ahead
+        # NOTE: This might be an overkill!
+        self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
+                                     r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
+                                     r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
+                                     r'(?=.*Z(-?\d*\.?\d*))?[CFSBHT]')
+
+        # Tool select
+        # Can have additional data after tool number but
+        # is ignored if present in the header.
+        # Warning: This will match toolset_re too.
+        self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
+
+        # Comment
+        self.comm_re = re.compile(r'^;(.*)$')
+
+        # Absolute/Incremental G90/G91
+        self.absinc_re = re.compile(r'^G9([01])$')
+
+        # Modes of operation
+        # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
+        self.modes_re = re.compile(r'^G0([012345])')
+
+        # Measuring mode
+        # 1-metric, 2-inch
+        self.meas_re = re.compile(r'^M7([12])$')
+
+        # Coordinates
+        self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
+        self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
+
+        # R - Repeat hole (# times, X offset, Y offset)
+        self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X(\d*\.?\d*))?(?:Y(\d*\.?\d*))?$')
+
+        # Various stop/pause commands
+        self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
         
     def parse_file(self, filename):
+        """
+        Reads the specified file as array of lines as
+        passes it to ``parse_lines()``.
+
+        :param filename: The file to be read and parsed.
+        :type filename: str
+        :return: None
+        """
         efile = open(filename, 'r')
         estr = efile.readlines()
         efile.close()
@@ -805,71 +948,79 @@ class Excellon(Geometry):
     def parse_lines(self, elines):
         """
         Main Excellon parser.
+
+        :param elines: List of strings, each being a line of Excellon code.
+        :type elines: list
+        :return: None
         """
-        units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
 
         current_tool = ""
-        
+        in_header = False
+
         for eline in elines:
-            
-            ## Tool definitions ##
-            # TODO: Verify all this
-            indexT = eline.find("T")
-            indexC = eline.find("C")
-            indexF = eline.find("F")
-            # Type 1
-            if indexT != -1 and indexC > indexT and indexF > indexC:
-                tool = eline[1:indexC]
-                spec = eline[indexC+1:indexF]
-                self.tools[tool] = float(spec)
-                continue
-            # Type 2
-            # TODO: Is this inches?
-            #indexsp = eline.find(" ")
-            #indexin = eline.find("in")
-            #if indexT != -1 and indexsp > indexT and indexin > indexsp:
-            #    tool = eline[1:indexsp]
-            #    spec = eline[indexsp+1:indexin]
-            #    self.tools[tool] = spec
-            #    continue
-            # Type 3
-            if indexT != -1 and indexC > indexT:
-                tool = eline[1:indexC]
-                spec = eline[indexC+1:-1]
-                self.tools[tool] = float(spec)
-                continue
-            
-            ## Tool change
-            if indexT == 0:
-                current_tool = eline[1:-1]
+
+            ## Header Begin/End ##
+            if self.hbegin_re.search(eline):
+                in_header = True
                 continue
-            
-            ## Drill
-            indexx = eline.find("X")
-            indexy = eline.find("Y")
-            if indexx != -1 and indexy != -1:
-                x = float(int(eline[indexx+1:indexy])/10000.0)
-                y = float(int(eline[indexy+1:-1])/10000.0)
-                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+
+            if self.hend_re.search(eline):
+                in_header = False
                 continue
 
-            # Units and number format
-            match = units_re.match(eline)
-            if match:
-                self.zeros = match.group(2)  # "T" or "L"
-                self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
+            #### Body ####
+            if not in_header:
+
+                ## Tool change ##
+                match = self.toolsel_re.search(eline)
+                if match:
+                    current_tool = str(int(match.group(1)))
+                    continue
+
+                ## Drill ##
+                indexx = eline.find("X")
+                indexy = eline.find("Y")
+                if indexx != -1 and indexy != -1:
+                    x = float(int(eline[indexx+1:indexy])/10000.0)
+                    y = float(int(eline[indexy+1:-1])/10000.0)
+                    self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+                    continue
+
+            #### Header ####
+            if in_header:
+
+                ## Tool definitions ##
+                match = self.toolset_re.search(eline)
+                if match:
+                    name = str(int(match.group(1)))
+                    spec = {
+                        "C": float(match.group(2)),
+                        # "F": float(match.group(3)),
+                        # "S": float(match.group(4)),
+                        # "B": float(match.group(5)),
+                        # "H": float(match.group(6)),
+                        # "Z": float(match.group(7))
+                    }
+                    self.tools[name] = spec
+                    continue
+
+                ## Units and number format ##
+                match = self.units_re.match(eline)
+                if match:
+                    self.zeros = match.group(2)  # "T" or "L"
+                    self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
+                    continue
 
             print "WARNING: Line ignored:", eline
         
     def create_geometry(self):
         self.solid_geometry = []
-        sizes = {}
-        for tool in self.tools:
-            sizes[tool] = float(self.tools[tool])
+
         for drill in self.drills:
-            poly = Point(drill['point']).buffer(sizes[drill['tool']]/2.0)
+            poly = Point(drill['point']).buffer(self.tools[drill['tool']]["C"]/2.0)
             self.solid_geometry.append(poly)
-        self.solid_geometry = cascaded_union(self.solid_geometry)
+
+        #self.solid_geometry = cascaded_union(self.solid_geometry)
 
     def scale(self, factor):
         """
@@ -910,7 +1061,9 @@ class Excellon(Geometry):
 
         # Tools
         for tname in self.tools:
-            self.tools[tname] *= factor
+            self.tools[tname]["C"] *= factor
+
+        self.create_geometry()
 
         return factor
 
@@ -1236,8 +1389,7 @@ class CNCjob(Geometry):
                 arcdir = [None, None, "cw", "ccw"]
                 if current['G'] in [0, 1]:  # line
                     path.append((x, y))
-                    # geometry.append({'geom': LineString([(current['X'], current['Y']),
-                    #                                     (x, y)]), 'kind': kind})
+
                 if current['G'] in [2, 3]:  # arc
                     center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
                     radius = sqrt(gobj['I']**2 + gobj['J']**2)
@@ -1246,10 +1398,6 @@ class CNCjob(Geometry):
                     path += arc(center, radius, start, stop,
                                 arcdir[current['G']],
                                 self.steps_per_circ)
-                    # geometry.append({'geom': arc(center, radius, start, stop,
-                    #                              arcdir[current['G']],
-                    #                              self.steps_per_circ),
-                    #                  'kind': kind})
 
             # Update current instruction
             for code in gobj:

BIN
share/flatcam_icon16.png


BIN
share/flatcam_icon256.png


BIN
share/flatcam_icon32.png


BIN
share/flatcam_icon48.png