From 738ff16a0d95a86a6f97ad20959946049184558d Mon Sep 17 00:00:00 2001
From: evanshultz <evan.shultz@gmail.com>
Date: Thu, 10 Sep 2020 08:26:02 -0700
Subject: [PATCH] Support hidden and deleted pins (#604)

It turns out this was actually already supported, but with a different name and we have agreed on new terminology (https://github.com/pointhi/kicad-footprint-generator/pull/586#issuecomment-680057204)
---
 KicadModTree/nodes/specialized/PadArray.py    |  32 +++-
 .../ipc_gullwing_generator.py                 |  46 ++++--
 .../size_definitions/soic.yaml                |   2 +-
 .../test_hidden_deleted_pins.yaml             | 150 ++++++++++++++++++
 scripts/Packages/package_config_KLCv3.yaml    |   6 +
 scripts/tools/pad_number_generators.py        |  22 ++-
 scripts/tools/quad_dual_pad_border.py         |   6 +-
 7 files changed, 238 insertions(+), 26 deletions(-)
 create mode 100644 scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/test_hidden_deleted_pins.yaml

diff --git a/KicadModTree/nodes/specialized/PadArray.py b/KicadModTree/nodes/specialized/PadArray.py
index 3a0998956..378b3aa1d 100644
--- a/KicadModTree/nodes/specialized/PadArray.py
+++ b/KicadModTree/nodes/specialized/PadArray.py
@@ -78,8 +78,11 @@ class PadArray(Node):
           shape for marking pad 1 for through hole components. (deafult: ``Pad.SHAPE_ROUNDRECT``)
         * *tht_pad1_id* (``int, string``) --
           pad number used for "pin 1" (default: 1)
-        * *exclude_pin_list* (``int, Vector1D``) --
-          which pin number should be skipped"
+        * *hidden_pins* (``int, Vector1D``) --
+          pin number(s) to be skipped; a footprint with hidden pins has missing pads and matching pin numbers
+        * *deleted_pins* (``int, Vector1D``) --
+          pin locations(s) to be skipped; a footprint with deleted pins has pads missing but no missing pin numbers"
+
 
     :Example:
 
@@ -105,9 +108,15 @@ def _initPincount(self, **kwargs):
         if type(self.pincount) is not int or self.pincount <= 0:
             raise ValueError('{pc} is an invalid value for pincount'.format(pc=self.pincount))
 
+        if kwargs.get('hidden_pins') and kwargs.get('deleted_pins'):
+            raise KeyError('hidden pins and deleted pins cannot be used together')
+
         self.exclude_pin_list = []
-        if kwargs.get('exclude_pin_list'):
-            self.exclude_pin_list = kwargs.get('exclude_pin_list')
+        if kwargs.get('hidden_pins'):
+            # exclude_pin_list is for pads being removed based on pad number
+            # deleted pins are filtered out later by pad location (not number)
+            self.exclude_pin_list = kwargs.get('hidden_pins')
+
             if type(self.exclude_pin_list) not in [list, tuple]:
                 raise TypeError('exclude pin list must be specified like "exclude_pin_list=[0,1]"')
             elif any([type(i) not in [int] for i in self.exclude_pin_list]):
@@ -235,10 +244,17 @@ def _createPads(self, **kwargs):
 
         for i, number in enumerate(pad_numbers):
             includePad = True
-            if type(self.initialPin) == 'int':
-                includePad = (self.initialPin + i) not in self.exclude_pin_list
-            else:
-                includePad = number not in self.exclude_pin_list
+
+            # deleted pins are filtered by pad/pin position (they are 'None' in pad_numbers list)
+            if type(number) not in [int, str]:
+                includePad = False
+
+            # hidden pins are filtered out by pad number (index of pad_numbers list)
+            if not kwargs.get('deleted_pins'):
+                if type(self.initialPin) == 'int':
+                    includePad = (self.initialPin + i) not in self.exclude_pin_list
+                else:
+                    includePad = number not in self.exclude_pin_list
 
             if includePad:
                 current_pad_pos = Vector2D(
diff --git a/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/ipc_gullwing_generator.py b/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/ipc_gullwing_generator.py
index 52a3ba51b..af63085ba 100755
--- a/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/ipc_gullwing_generator.py
+++ b/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/ipc_gullwing_generator.py
@@ -140,10 +140,21 @@ def deviceDimensions(device_size_data):
     def generateFootprint(self, device_params, header):
         dimensions = Gullwing.deviceDimensions(device_params)
 
-        if dimensions['has_EP'] and 'thermal_vias' in device_params:
-            self.__createFootprintVariant(device_params, header, dimensions, True)
+        if 'deleted_pins' in device_params:
+            if type(device_params['deleted_pins']) is int:
+                device_params['deleted_pins'] = [device_params['deleted_pins']]
 
-        self.__createFootprintVariant(device_params, header, dimensions, False)
+        if 'hidden_pins' in device_params:
+            if type(device_params['hidden_pins']) is int:
+                device_params['hidden_pins'] = [device_params['hidden_pins']]
+
+        if 'deleted_pins' in device_params and 'hidden_pins' in device_params:
+            print("A footprint may not have deleted pins and hidden pins.")
+        else:
+            if dimensions['has_EP'] and 'thermal_vias' in device_params:
+                self.__createFootprintVariant(device_params, header, dimensions, True)
+
+            self.__createFootprintVariant(device_params, header, dimensions, False)
 
     def __createFootprintVariant(self, device_params, header, dimensions, with_thermal_vias):
         fab_line_width = self.configuration.get('fab_line_width', 0.1)
@@ -154,7 +165,17 @@ def __createFootprintVariant(self, device_params, header, dimensions, with_therm
         size_x = dimensions['body_size_x'].nominal
         size_y = dimensions['body_size_y'].nominal
 
-        pincount = device_params['num_pins_x']*2 + device_params['num_pins_y']*2
+        pincount_full = device_params['num_pins_x']*2 + device_params['num_pins_y']*2
+        
+        if 'hidden_pins' in device_params:
+            pincount_text = '{}-{}'.format(pincount_full - len(device_params['hidden_pins']), pincount_full)
+            pincount = pincount_full - len(device_params['hidden_pins'])
+        elif 'deleted_pins' in device_params:
+            pincount_text = '{}-{}'.format(pincount_full, pincount_full - len(device_params['deleted_pins']))
+            pincount = pincount_full - len(device_params['deleted_pins'])
+        else:
+            pincount_text = '{}'.format(pincount_full)
+            pincount = pincount_full
 
         ipc_reference = 'ipc_spec_gw_large_pitch' if device_params['pitch'] >= 0.625 else 'ipc_spec_gw_small_pitch'
         if device_params.get('force_small_pitch_ipc_definition', False):
@@ -166,12 +187,12 @@ def __createFootprintVariant(self, device_params, header, dimensions, with_therm
 
         pitch = device_params['pitch']
 
-        name_format = self.configuration['fp_name_format_string_no_trailing_zero']
+        name_format = self.configuration['fp_name_format_string_no_trailing_zero_pincount_text']
         EP_size = {'x':0, 'y':0}
         EP_mask_size = {'x':0, 'y':0}
 
         if dimensions['has_EP']:
-            name_format = self.configuration['fp_name_EP_format_string_no_trailing_zero']
+            name_format = self.configuration['fp_name_EP_format_string_no_trailing_zero_pincount_text']
             if 'EP_size_x_overwrite' in device_params:
                 EP_size = {
                     'x':device_params['EP_size_x_overwrite'],
@@ -183,7 +204,7 @@ def __createFootprintVariant(self, device_params, header, dimensions, with_therm
                     'y':dimensions['EP_size_y'].nominal
                     }
             if 'EP_mask_x' in dimensions:
-                name_format = self.configuration['fp_name_EP_custom_mask_format_string_no_trailing_zero']
+                name_format = self.configuration['fp_name_EP_custom_mask_format_string_no_trailing_zero_pincount_text']
                 EP_mask_size = {'x':dimensions['EP_mask_x'].nominal, 'y':dimensions['EP_mask_y'].nominal}
         EP_size = Vector2D(EP_size)
 
@@ -201,7 +222,7 @@ def __createFootprintVariant(self, device_params, header, dimensions, with_therm
             man=device_params.get('manufacturer',''),
             mpn=device_params.get('part_number',''),
             pkg=header['device_type'],
-            pincount=pincount,
+            pincount=pincount_text,
             size_y=size_y,
             size_x=size_x,
             pitch=device_params['pitch'],
@@ -218,7 +239,7 @@ def __createFootprintVariant(self, device_params, header, dimensions, with_therm
             man=device_params.get('manufacturer',''),
             mpn=device_params.get('part_number',''),
             pkg=header['device_type'],
-            pincount=pincount,
+            pincount=pincount_text,
             size_y=size_y,
             size_x=size_x,
             pitch=device_params['pitch'],
@@ -240,7 +261,7 @@ def __createFootprintVariant(self, device_params, header, dimensions, with_therm
 
         kicad_mod = Footprint(fp_name)
 
-                # init kicad footprint
+        # init kicad footprint
         kicad_mod.setDescription(
             "{manufacturer} {mpn} {package}, {pincount} Pin ({datasheet}), generated with kicad-footprint-generator {scriptname}"\
             .format(
@@ -260,7 +281,10 @@ def __createFootprintVariant(self, device_params, header, dimensions, with_therm
             ).lstrip())
         kicad_mod.setAttribute('smd')
 
-        pad_radius = add_dual_or_quad_pad_border(kicad_mod, configuration, pad_details, device_params)
+        if 'custom_pad_layout' in device_params:
+            pad_radius = add_custom_pad_layout(kicad_mod, configuration, pad_details, device_params)
+        else:
+            pad_radius = add_dual_or_quad_pad_border(kicad_mod, configuration, pad_details, device_params)
 
         EP_round_radius = 0
         if dimensions['has_EP']:
diff --git a/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/soic.yaml b/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/soic.yaml
index a71fa723c..e1737a864 100644
--- a/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/soic.yaml
+++ b/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/soic.yaml
@@ -24,7 +24,7 @@ SOIC-4_4.55x3.7mm_P2.54mm:
   pitch: 1.27
   num_pins_x: 0
   num_pins_y: 3
-  exclude_pin_list: [2, 5]
+  hidden_pins: [2, 5]
 
 SOIC-4_4.55x2.6mm_P1.27mm:
   size_source: 'https://toshiba.semicon-storage.com/info/docget.jsp?did=12884&prodName=TLP291'
diff --git a/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/test_hidden_deleted_pins.yaml b/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/test_hidden_deleted_pins.yaml
new file mode 100644
index 000000000..baff90ef3
--- /dev/null
+++ b/scripts/Packages/Package_Gullwing__QFP_SOIC_SO/size_definitions/test_hidden_deleted_pins.yaml
@@ -0,0 +1,150 @@
+# hidden and deleted pins are defined and described by IPC 7351B naming convention
+# http://ohm.bu.edu/~pbohn/__Engineering_Reference/pcb_layout/pcbmatrix/IPC-7x51%20&%20PCBM%20Land%20Pattern%20Naming%20Convention.pdf
+# hidden pins remove a pin location and pin number (for example, going from pad 1 to pad 3 with pin 2 missing from the package)
+# deleted pins remove a pin location but not pin number (for example, pin 2 is missing from the package but the footprint has pad 2)
+# deleted pins do not support custom numbering schemes https://github.com/pointhi/kicad-footprint-generator/pull/371
+
+FileHeader:
+  library_Suffix: 'TO_SOT_SMD'
+  device_type: 'SOT'
+
+# test add_quad_pad_border() in quad_dual_pad_border.py
+LQFP-48_7x7mm_P0.5mm:
+  size_source: '~'
+  body_size_x: 7
+  body_size_y: 7
+  overall_size_x: 9
+  overall_size_y: 9
+  lead_width: 0.17 .. 0.27
+  lead_len: 0.45 .. 0.75
+  pitch: 0.5
+  num_pins_x: 12
+  num_pins_y: 12
+
+# test add_quad_pad_border() in quad_dual_pad_border.py with hidden pins
+LQFP-48_7x7mm_P0.5mm_hidden:
+  size_source: '~'
+  body_size_x: 7
+  body_size_y: 7
+  overall_size_x: 9
+  overall_size_y: 9
+  lead_width: 0.17 .. 0.27
+  lead_len: 0.45 .. 0.75
+  pitch: 0.5
+  num_pins_x: 12
+  num_pins_y: 12
+  hidden_pins: [1,15,24,30,31,44]
+
+# test add_quad_pad_border() in quad_dual_pad_border.py with deleted pins
+LQFP-48_7x7mm_P0.5mm_deleted:
+  size_source: '~'
+  body_size_x: 7
+  body_size_y: 7
+  overall_size_x: 9
+  overall_size_y: 9
+  lead_width: 0.17 .. 0.27
+  lead_len: 0.45 .. 0.75
+  pitch: 0.5
+  num_pins_x: 12
+  num_pins_y: 12
+  deleted_pins: [1,15,24,30,31,44]
+
+# test add_dual_pad_border_y() in quad_dual_pad_border.py
+SOT-23-6:
+  size_source: '~'
+  custom_name_format: 'SOT-23-6'
+  body_size_x: 1.6
+  body_size_y: 2.9
+  overall_size_x: 2.8
+  lead_width: 0.3 .. 0.5
+  lead_len: 0.3 .. 0.45 .. 0.6
+  pitch: 0.95
+  num_pins_x: 0
+  num_pins_y: 3
+
+# test add_dual_pad_border_y() in quad_dual_pad_border.py with hidden pins
+# manually-made footprint merged at https://github.com/KiCad/kicad-footprints/pull/2298
+SOIC-14-16_3.9x9.9mm_P1.27mm:
+  size_source: '~'
+  body_size_x: 3.8 .. 4.0
+  body_size_y: 9.8 .. 10
+  overall_height:
+    maximum: 1.75
+
+  overall_size_x: 6 +/-0.2
+  lead_len: 0.4 .. 1.27
+  lead_width: 0.31 .. 0.51
+
+  pitch: 1.27
+  num_pins_x: 0
+  num_pins_y: 8
+  hidden_pins: [2, 13]
+
+# test add_dual_pad_border_y() in quad_dual_pad_border.py with deleted pins
+SOT-23-5:
+  size_source: '~'
+  custom_name_format: 'SOT-23-5'
+  body_size_x: 1.6
+  body_size_y: 2.9
+  overall_size_x: 2.8
+  lead_width: 0.3 .. 0.5
+  lead_len: 0.3 .. 0.45 .. 0.6
+  pitch: 0.95
+  num_pins_x: 0
+  num_pins_y: 3
+  deleted_pins: 5
+
+# test add_dual_pad_border_x() in quad_dual_pad_border.py
+SOT-23-6R:
+  size_source: '~'
+  custom_name_format: 'SOT-23-6R'
+  body_size_x: 1.6
+  body_size_y: 2.9
+  overall_size_x: 2.8
+  lead_width: 0.3 .. 0.5
+  lead_len: 0.3 .. 0.45 .. 0.6
+  pitch: 0.95
+  num_pins_x: 3
+  num_pins_y: 0
+
+# test add_dual_pad_border_x() in quad_dual_pad_border.py with hidden pins
+SOT-23-6R_hidden:
+  size_source: '~'
+  custom_name_format: 'SOT-23-6R_Hidden'
+  body_size_x: 1.6
+  body_size_y: 2.9
+  overall_size_x: 2.8
+  lead_width: 0.3 .. 0.5
+  lead_len: 0.3 .. 0.45 .. 0.6
+  pitch: 0.95
+  num_pins_x: 3
+  num_pins_y: 0
+  hidden_pins: [5]
+
+# test add_dual_pad_border_x() in quad_dual_pad_border.py with deleted pins
+SOT-23-6R_deleted:
+  size_source: '~'
+  custom_name_format: 'SOT-23-6R_Deleted'
+  body_size_x: 1.6
+  body_size_y: 2.9
+  overall_size_x: 2.8
+  lead_width: 0.3 .. 0.5
+  lead_len: 0.3 .. 0.45 .. 0.6
+  pitch: 0.95
+  num_pins_x: 3
+  num_pins_y: 0
+  deleted_pins: [5]
+
+# test footprint with removed pins on both sides
+SOT-23:
+  size_source: '~'
+  custom_name_format: 'SOT-23'
+  body_size_x: 1.6
+  body_size_y: 2.9
+  overall_size_x: 2.8
+  lead_width: 0.3 .. 0.5
+  lead_len: 0.3 .. 0.45 .. 0.6
+  pitch: 0.95
+  num_pins_x: 0
+  num_pins_y: 3
+  deleted_pins: [2, 4, 6]
diff --git a/scripts/Packages/package_config_KLCv3.yaml b/scripts/Packages/package_config_KLCv3.yaml
index 8c2abc1b0..aea82f2a3 100644
--- a/scripts/Packages/package_config_KLCv3.yaml
+++ b/scripts/Packages/package_config_KLCv3.yaml
@@ -1,9 +1,12 @@
 fp_name_format_string: '{man:s}_{mpn:s}_{pkg:s}-{pincount:d}_{size_x:.1f}x{size_y:.1f}mm_P{pitch:.2f}mm{suffix:s}'
+fp_name_format_string_pincount_text: '{man:s}_{mpn:s}_{pkg:s}-{pincount:s}_{size_x:.1f}x{size_y:.1f}mm_P{pitch:.2f}mm{suffix:s}'
+
 keyword_fp_string: '{man:s} {package:s} {category:s}'
 
 lib_name_format_string: 'Package_{category:s}'
 
 fp_name_format_string_no_trailing_zero: '{man:s}_{mpn:s}_{pkg:s}-{pincount:d}_{size_x:g}x{size_y:g}mm_P{pitch:g}mm{suffix:s}{suffix2:s}'
+fp_name_format_string_no_trailing_zero_pincount_text: '{man:s}_{mpn:s}_{pkg:s}-{pincount:s}_{size_x:g}x{size_y:g}mm_P{pitch:g}mm{suffix:s}{suffix2:s}'
 
 fp_name_lga_format_string_no_trailing_zero: '{man:s}_{mpn:s}_{pkg:s}-{pincount:d}_{size_x:g}x{size_y:g}mm_P{pitch:g}mm{layout:s}{suffix:s}{suffix2:s}'
 
@@ -11,6 +14,9 @@ lga_layout_grid: '_LayoutGrid{nx:d}x{ny:d}'
 lga_layout_border: '_LayoutBorder{nx:d}x{ny:d}y'
 
 fp_name_EP_format_string_no_trailing_zero: '{man:s}_{mpn:s}_{pkg:s}-{pincount:d}-1EP_{size_x:g}x{size_y:g}mm_P{pitch:g}mm{suffix:s}_EP{ep_size_x:g}x{ep_size_y:g}mm{suffix2:s}{vias:s}'
+fp_name_EP_format_string_no_trailing_zero_pincount_text: '{man:s}_{mpn:s}_{pkg:s}-{pincount:s}-1EP_{size_x:g}x{size_y:g}mm_P{pitch:g}mm{suffix:s}_EP{ep_size_x:g}x{ep_size_y:g}mm{suffix2:s}{vias:s}'
+
 fp_name_EP_custom_mask_format_string_no_trailing_zero: '{man:s}_{mpn:s}_{pkg:s}-{pincount:d}-1EP_{size_x:g}x{size_y:g}mm_P{pitch:g}mm{suffix:s}_EP{ep_size_x:g}x{ep_size_y:g}mm_Mask{mask_size_x:g}x{mask_size_y:g}mm{suffix2:s}{vias:s}'
+fp_name_EP_custom_mask_format_string_no_trailing_zero_pincount_text: '{man:s}_{mpn:s}_{pkg:s}-{pincount:s}-1EP_{size_x:g}x{size_y:g}mm_P{pitch:g}mm{suffix:s}_EP{ep_size_x:g}x{ep_size_y:g}mm_Mask{mask_size_x:g}x{mask_size_y:g}mm{suffix2:s}{vias:s}'
 
 thermal_via_suffix: "_ThermalVias"
diff --git a/scripts/tools/pad_number_generators.py b/scripts/tools/pad_number_generators.py
index 942a2d7b7..f8a8906bc 100644
--- a/scripts/tools/pad_number_generators.py
+++ b/scripts/tools/pad_number_generators.py
@@ -6,7 +6,8 @@
 
 Available generators:
 ----------------------
-- increment: Is a simple generator that increments the pin number by one.
+- increment: Generates pin numbers increasing by one and supports skipping pin numbers
+            using the deleted_pins kwarg (skipped pins return a bogus value of -1)
 - cw_dual: Generates pin numbers counting clockwise from the starting position
 - ccw_dual: Generates pin numbers counting counter-clockwise from the starting position
 
@@ -23,9 +24,22 @@
 
 def increment(pincount, init=1, **kwargs):
     i = init
+    j = init # pad number for deleted pins
+
+    if kwargs.get("deleted_pins"):
+        skip_pins = kwargs["deleted_pins"]
+
     while i <= pincount:
-        yield i
-        i += 1
+        if kwargs.get("deleted_pins"):
+            if i in skip_pins:
+                yield None
+            else:
+                yield j
+                j += 1
+            i += 1 # i acts like pin location for deleted pins
+        else:
+            yield i
+            i += 1
 
 
 def _get_pin_cw(pincount, loc):
@@ -146,7 +160,7 @@ def get_generator(device_params):
     pad_nums = device_params.get("pad_numbers")
 
     if not pad_nums:
-        return generators["increment"](pincount)
+        return generators["increment"](pincount, **device_params)
 
     init = pad_nums.get("init", 1)
 
diff --git a/scripts/tools/quad_dual_pad_border.py b/scripts/tools/quad_dual_pad_border.py
index c93b53c3d..65e267e14 100644
--- a/scripts/tools/quad_dual_pad_border.py
+++ b/scripts/tools/quad_dual_pad_border.py
@@ -12,8 +12,10 @@ def add_dual_or_quad_pad_border(kicad_mod, configuration, pad_details, device_pa
     if 'round_rect_max_radius' in configuration:
         pad_shape_details['maximum_radius'] = configuration['round_rect_max_radius']
 
-    if 'exclude_pin_list' in device_params:
-        pad_shape_details['exclude_pin_list'] = device_params['exclude_pin_list']
+    if 'hidden_pins' in device_params:
+        pad_shape_details['hidden_pins'] = device_params['hidden_pins']
+    if 'deleted_pins' in device_params:
+        pad_shape_details['deleted_pins'] = device_params['deleted_pins']
 
     if device_params['num_pins_x'] == 0:
         radius = add_dual_pad_border_y(kicad_mod, pad_details, device_params, pad_shape_details)