From be8371c7bc21abe12db49f9dd38dac4513a64886 Mon Sep 17 00:00:00 2001
From: jaseg <git@jaseg.de>
Date: Wed, 6 Nov 2024 14:49:50 +0100
Subject: Improve allegro/orcad import

---
 gerbonara/excellon.py | 46 +++++++++++++++++++++++++++++++++++-----------
 1 file changed, 35 insertions(+), 11 deletions(-)

(limited to 'gerbonara/excellon.py')

diff --git a/gerbonara/excellon.py b/gerbonara/excellon.py
index 9b75a69..73f9592 100755
--- a/gerbonara/excellon.py
+++ b/gerbonara/excellon.py
@@ -566,6 +566,8 @@ class ExcellonParser(object):
         self.filename = None
         self.external_tools = external_tools or {}
         self.found_kicad_format_comment = False
+        self.allegro_eof_toolchange_hack = False
+        self.allegro_eof_toolchange_hack_index = 1
 
     def warn(self, msg):
         warnings.warn(f'{self.filename}:{self.lineno} "{self.line}": {msg}', SyntaxWarning)
@@ -606,18 +608,25 @@ class ExcellonParser(object):
     exprs = RegexMatcher()
 
     # NOTE: These must be kept before the generic comment handler at the end of this class so they match first.
-    @exprs.match(r';T(?P<index1>[0-9]+) Holesize (?P<index2>[0-9]+)\. = (?P<diameter>[0-9/.]+) Tolerance = \+[0-9/.]+/-[0-9/.]+ (?P<plated>PLATED|NON_PLATED|OPTIONAL) (?P<unit>MILS|MM) Quantity = [0-9]+')
+    @exprs.match(r';(?P<index1_prefix>T(?P<index1>[0-9]+))?\s+Holesize (?P<index2>[0-9]+)\. = (?P<diameter>[0-9/.]+) Tolerance = \+[0-9/.]+/-[0-9/.]+ (?P<plated>PLATED|NON_PLATED|OPTIONAL) (?P<unit>MILS|MM) Quantity = [0-9]+')
     def parse_allegro_tooldef(self, match):
         # NOTE: We ignore the given tolerances here since they are non-standard.
         self.program_state = ProgramState.HEADER # TODO is this needed? we need a test file.
         self.generator_hints.append('allegro')
 
-        if (index := int(match['index1'])) != int(match['index2']): # index1 has leading zeros, index2 not.
+        index = int(match['index2'])
+
+        if match['index1'] and index != int(match['index1']): # index1 has leading zeros, index2 not.
             raise SyntaxError('BUG: Allegro excellon tool def has mismatching tool indices. Please file a bug report on our issue tracker and provide this file!')
 
         if index in self.tools:
             self.warn('Re-definition of tool index {index}, overwriting old definition.') 
 
+        if not match['index1_prefix']:
+            # This is a really nasty orcad file without tool change commands, that instead just puts all holes in order
+            # of the hole size definitions with M00's in between.
+            self.allegro_eof_toolchange_hack = True
+
         # NOTE: We map "optionally" plated holes to plated holes for API simplicity. If you hit a case where that's a
         # problem, please raise an issue on our issue tracker, explain why you need this and provide an example file.
         is_plated = None if match['plated'] is None else (match['plated'] in ('PLATED', 'OPTIONAL'))
@@ -630,13 +639,19 @@ class ExcellonParser(object):
         else:
             unit = MM
 
-        if unit != self.settings.unit:
+        if self.settings.unit is None:
+            self.settings.unit = unit
+
+        elif unit != self.settings.unit:
             self.warn('Allegro Excellon drill file tool definitions in {unit.name}, but file parameters say the '
                     'file should be in {settings.unit.name}. Please double-check that this is correct, and if it is, '
                     'please raise an issue on our issue tracker.')
 
         self.tools[index] = ExcellonTool(diameter=diameter, plated=is_plated, unit=unit)
 
+        if self.allegro_eof_toolchange_hack and self.active_tool is None:
+            self.active_tool = self.tools[index]
+
     # Searching Github I found that EasyEDA has two different variants of the unit specification here.
     @exprs.match(';Holesize (?P<index>[0-9]+) = (?P<diameter>[.0-9]+) (?P<unit>INCH|inch|METRIC|mm)')
     def parse_easyeda_tooldef(self, match):
@@ -753,6 +768,12 @@ class ExcellonParser(object):
     def handle_end_of_program(self, match):
         if self.program_state in (None, ProgramState.HEADER):
             self.warn('M30 statement found before end of header.')
+
+        if self.allegro_eof_toolchange_hack:
+            self.allegro_eof_toolchange_hack_index = min(max(self.tools), self.allegro_eof_toolchange_hack_index + 1)
+            self.active_tool = self.tools[self.allegro_eof_toolchange_hack_index]
+            return
+
         self.program_state = ProgramState.FINISHED
         # TODO: maybe add warning if this is followed by other commands.
 
@@ -762,14 +783,17 @@ class ExcellonParser(object):
     def do_move(self, coord_groups):
         x_s, x, y_s, y = coord_groups
 
-        if self.settings.number_format == (None, None) and '.' not in x:
-            # TARGET3001! exports zeros as "00" even when it uses an explicit decimal point everywhere else.
-            if x != '00':
-                raise SyntaxError('No number format set and value does not contain a decimal point. If this is an Allegro '
-                    'Excellon drill file make sure either nc_param.txt or ncdrill.log ends up in the same folder as '
-                    'it, because Allegro does not include this critical information in their Excellon output. If you '
-                    'call this through ExcellonFile.from_string, you must manually supply from_string with a '
-                    'FileSettings object from excellon.parse_allegro_ncparam.')
+        if '.' not in x:
+            self.settings._file_has_fixed_width_coordinates = True
+
+            if self.settings.number_format == (None, None):
+                # TARGET3001! exports zeros as "00" even when it uses an explicit decimal point everywhere else.
+                if x != '00':
+                    raise SyntaxError('No number format set and value does not contain a decimal point. If this is an Allegro '
+                        'Excellon drill file make sure either nc_param.txt or ncdrill.log ends up in the same folder as '
+                        'it, because Allegro does not include this critical information in their Excellon output. If you '
+                        'call this through ExcellonFile.from_string, you must manually supply from_string with a '
+                        'FileSettings object from excellon.parse_allegro_ncparam.')
 
         x = self.settings.parse_gerber_value(x)
         if x_s:
-- 
cgit