From 08b1e5f7b8179a26de18a78c33058452aee1e340 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Tue, 14 Nov 2023 11:07:12 -0300 Subject: [PATCH] [Rotation Filter] Fixes and extensions for JLCPCB - Fixed the JLCPCB template to apply correct rotations to the bottom components - Added options to specify rotations and offsets using fields - Added an option to mirror the bottom rotation angles - Needs verifications for the offset stuff Related to #510 --- CHANGELOG.md | 10 ++ docs/source/configuration/filters.rst | 1 + docs/source/configuration/sup_filters.rst | 15 ++ kibot/fil_base.py | 4 + kibot/fil_rot_footprint.py | 133 ++++++++++++++---- kibot/kicad/v5_sch.py | 2 + kibot/misc.py | 1 + kibot/out_position.py | 4 + .../config_templates/JLCPCB.kibot.yaml | 2 +- 9 files changed, 145 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe3354f3..790b766f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Internal templates: - 3DRender_top, 3DRender_top_straight, 3DRender_bottom and 3DRender_bottom_straight: to generate simple and quick 3D renders. +- Filters: + - New `_rot_footprint_jlcpcb` internal filter to fix the JLCPCB bottom + rotations. + - New options for the `rot_footprint` filters: (See #510) + - `mirror_bottom`: used to undo the KiCad mirroring of the bottom. + - `rot_fields`: list of fields to indicate arbitrary rotations. + - `offset_fields`: list of fields to indicate arbitrary offsets. + - `bennymeg_mode`: used to provide compatibility with the + bennymeg/JLC-Plugin-for-KiCad tool. - 3D outputs: - `download_lcsc` option to disable LCSC 3D model download (See #415) - BoM: @@ -74,6 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sub PCB separation using annotation method for some edeges and KiCad versions (#496) - Problems when using NET_NAME(n) for a value (#511) +- JLCPCB rotations for bottom components ## [1.6.3] - 2023-06-26 diff --git a/docs/source/configuration/filters.rst b/docs/source/configuration/filters.rst index b694d161..799d880e 100644 --- a/docs/source/configuration/filters.rst +++ b/docs/source/configuration/filters.rst @@ -132,6 +132,7 @@ Built-in filters - **_only_tht** is used to get only THT parts - **_only_virtual** is used to get only virtual parts - **_rot_footprint** is a default ``rot_footprint`` filter +- **_rot_footprint_jlcpcb** is a ``rot_footprint`` filter with option specific for JLCPCB - **_value_split** splits the Value field but the field remains and the extra data is not visible - **_value_split_replace** splits the Value field and replaces it diff --git a/docs/source/configuration/sup_filters.rst b/docs/source/configuration/sup_filters.rst index f5704cd5..31fa5e63 100644 --- a/docs/source/configuration/sup_filters.rst +++ b/docs/source/configuration/sup_filters.rst @@ -120,17 +120,32 @@ Supported filters - **rot_footprint**: (**Footprint Rotator**) This filter can rotate footprints, used for the positions file generation. |br| Some manufacturers use a different rotation than KiCad. |br| + The `JLCPCB Rotation Offset` and `JLCPCB Position Offset` fields can be used to adjust special cases. |br| The internal `_rot_footprint` filter implements the simplest case. - Valid keys: + - ``bennymeg_mode`` :index:`: ` [boolean=true] Implements the `rot_fields` and `offset_fields` in the same way that the bennymeg/JLC-Plugin-for-KiCad tool. + Note that the computation for bottom rotations is wrong, forcing the user to uses arbitrary rotations. + The correct computation is `(180 - component rot) + angle` but the plugin does `180 - (component rot + angle)`. + This option forces the wrong computation for compatibility. - ``comment`` :index:`: ` [string=''] A comment for documentation purposes. - ``extend`` :index:`: ` [boolean=true] Extends the internal list of rotations with the one provided. Otherwise just use the provided list. - ``invert_bottom`` :index:`: ` [boolean=false] Rotation for bottom components is negated, resulting in either: `(- component rot - angle)` or when combined with `negative_bottom`, `(angle - component rot)`. + - ``mirror_bottom`` :index:`: ` [boolean=false] The original component rotation for components in the bottom is mirrored before applying + the adjust so you get `(180 - component rot + angle)`. This is used by JLCPCB. - ``name`` :index:`: ` [string=''] Used to identify this particular filter definition. - ``negative_bottom`` :index:`: ` [boolean=true] Rotation for bottom components is computed via subtraction as `(component rot - angle)`. + - ``offset_fields`` :index:`: ` [string|list(string)='JLCPCB Position Offset,JLCPosOffset'] List of fields that can contain a position offset. + The optional fields can contain a comma seperated x,y position offset. + This concept is from the bennymeg/JLC-Plugin-for-KiCad tool. + + - ``rot_fields`` :index:`: ` [string|list(string)='JLCPCB Rotation Offset,JLCRotOffset'] List of fields that can contain a rotation offset. + The optional fields can contain a counter-clockwise orientation offset in degrees. + This concept is from the bennymeg/JLC-Plugin-for-KiCad tool. + - ``rotations`` :index:`: ` [list(list(string))] A list of pairs regular expression/rotation. Components matching the regular expression will be rotated the indicated angle. diff --git a/kibot/fil_base.py b/kibot/fil_base.py index f4fdc6b0..4612623a 100644 --- a/kibot/fil_base.py +++ b/kibot/fil_base.py @@ -73,6 +73,10 @@ SIMP_FIL = {'_only_smd': {'comment': 'Internal filter for only SMD parts', 'exclude_smd': True}, '_rot_footprint': {'type': 'rot_footprint', 'comment': 'Internal default footprint rotator'}, + '_rot_footprint_jlcpcb': {'type': 'rot_footprint', + 'comment': 'Internal footprint rotator for JLCPCB', + 'negative_bottom': False, + 'mirror_bottom': True}, '_expand_text_vars': {'type': 'expand_text_vars', 'comment': 'Internal default text variables expander'}, '_datasheet_link': {'type': 'urlify', diff --git a/kibot/fil_rot_footprint.py b/kibot/fil_rot_footprint.py index ea6b8ec1..a9b1a2da 100644 --- a/kibot/fil_rot_footprint.py +++ b/kibot/fil_rot_footprint.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2021 Salvador E. Tropea -# Copyright (c) 2020-2021 Instituto Nacional de TecnologĂ­a Industrial -# License: GPL-3.0 +# Copyright (c) 2020-2023 Salvador E. Tropea +# Copyright (c) 2020-2023 Instituto Nacional de TecnologĂ­a Industrial +# License: AGPL-3.0 # Project: KiBot (formerly KiPlot) # Description: Implements a filter to rotate footprints. # This is inspired in JLCKicadTools by Matthew Lai. +# I latter added more information from bennymeg/JLC-Plugin-for-KiCad +from math import sin, cos, radians from re import compile from .gs import GS from .optionable import Optionable from .error import KiPlotConfigurationError from .macros import macros, document, filter_class # noqa: F401 +from .misc import W_BADANGLE, W_BADOFFSET from . import log logger = log.get_logger() @@ -20,29 +23,30 @@ DEFAULT_ROTATIONS = [["^R_Array_Convex_", 90.0], ["^R_Array_Concave_", 90.0], ["^SOT-223", 180.0], ["^SOT-23", 180.0], + ["^D_SOT-23", 180.0], ["^TSOT-23", 180.0], ["^SOT-353", 180.0], ["^QFN-", 270.0], ["^LQFP-", 270.0], ["^TQFP-", 270.0], ["^SOP-(?!18_)", 270.0], + ["^MSOP-", 270.0], ["^TSSOP-", 270.0], ["^DFN-", 270.0], ["^SOIC-", 270.0], # ["^SOP-18_", 0], ["^VSSOP-10_", 270.0], - ["^CP_EIA-3216-18_", 180.0], - ["^CP_EIA-3528-15_AVX-H", 180.0], - ["^CP_EIA-3528-21_Kemet-B", 180.0], - ["^CP_Elec_8x10.5", 180.0], - ["^CP_Elec_6.3x7.7", 180.0], - ["^CP_Elec_8x6.7", 180.0], - ["^CP_Elec_8x10", 180.0], + ["^CP_EIA-", 180.0], + ["^CP_Elec_", 180.0], + ["^C_Elec_", 180.0], + ["^LED_WS2812B_PLCC4", 180.0], ["^(.*?_|V)?QFN-(16|20|24|28|40)(-|_|$)", 270.0], - ["^Bosch_LGA-8_2x2.5mm_P0.65mm_ClockwisePinNumbering", 90.0], + ["^Bosch_LGA-", 90.0], ["^PowerPAK_SO-8_Single", 270.0], - ["^HTSSOP-28-1EP_4.4x9.7mm*", 270.0], + ["^HTSSOP-", 270.0], ] +DEFAULT_ROT_FIELDS = ['JLCPCB Rotation Offset', 'JLCRotOffset'] +DEFAULT_OFFSET_FIELDS = ['JLCPCB Position Offset', 'JLCPosOffset'] @filter_class @@ -50,6 +54,7 @@ class Rot_Footprint(BaseFilter): # noqa: F821 """ Footprint Rotator This filter can rotate footprints, used for the positions file generation. Some manufacturers use a different rotation than KiCad. + The `JLCPCB Rotation Offset` and `JLCPCB Position Offset` fields can be used to adjust special cases. The internal `_rot_footprint` filter implements the simplest case """ def __init__(self): super().__init__() @@ -63,6 +68,9 @@ class Rot_Footprint(BaseFilter): # noqa: F821 self.invert_bottom = False """ Rotation for bottom components is negated, resulting in either: `(- component rot - angle)` or when combined with `negative_bottom`, `(angle - component rot)` """ + self.mirror_bottom = False + """ The original component rotation for components in the bottom is mirrored before applying + the adjust so you get `(180 - component rot + angle)`. This is used by JLCPCB """ self.rotations = Optionable """ [list(list(string))] A list of pairs regular expression/rotation. Components matching the regular expression will be rotated the indicated angle """ @@ -70,6 +78,19 @@ class Rot_Footprint(BaseFilter): # noqa: F821 """ Do not rotate components on the bottom """ self.skip_top = False """ Do not rotate components on the top """ + self.rot_fields = Optionable + """ [string|list(string)='JLCPCB Rotation Offset,JLCRotOffset'] List of fields that can contain a rotation offset. + The optional fields can contain a counter-clockwise orientation offset in degrees. + This concept is from the bennymeg/JLC-Plugin-for-KiCad tool """ + self.offset_fields = Optionable + """ [string|list(string)='JLCPCB Position Offset,JLCPosOffset'] List of fields that can contain a position offset. + The optional fields can contain a comma separated x,y position offset. + This concept is from the bennymeg/JLC-Plugin-for-KiCad tool """ + self.bennymeg_mode = True + """ Implements the `rot_fields` and `offset_fields` in the same way that the bennymeg/JLC-Plugin-for-KiCad tool. + Note that the computation for bottom rotations is wrong, forcing the user to uses arbitrary rotations. + The correct computation is `(180 - component rot) + angle` but the plugin does `180 - (component rot + angle)`. + This option forces the wrong computation for compatibility """ def config(self, parent): super().config(parent) @@ -91,23 +112,83 @@ class Rot_Footprint(BaseFilter): # noqa: F821 self._rot.append([compile(regex_str), angle]) if not self._rot: raise KiPlotConfigurationError("No rotations provided") + self.rot_fields = self.force_list(self.rot_fields, default=DEFAULT_ROT_FIELDS) + self.offset_fields = self.force_list(self.offset_fields, default=DEFAULT_OFFSET_FIELDS) - def filter(self, comp): - """ Apply the rotation """ - if (self.skip_top and not comp.bottom) or (self.skip_bottom and comp.bottom): - # Component should be excluded - return - for regex, angle in self._rot: - if regex.search(comp.footprint): - old_angle = comp.footprint_rot - if self.negative_bottom and comp.bottom: + def apply_rotation_angle(self, comp, angle, bennymeg_mode=False): + old_footprint_rot = comp.footprint_rot + if comp.bottom: + # Apply adjusts for bottom components + if bennymeg_mode and self.bennymeg_mode: + # Compatible with https://github.com/bennymeg/JLC-Plugin-for-KiCad/ + # Wrong! The real value is (180-comp.footprint_rot)+angle and not + # 180-(comp.footprint_rot+angle) + comp.footprint_rot = (comp.footprint_rot + angle) % 360.0 + comp.offset_footprint_rot = old_footprint_rot + comp.footprint_rot = (540.0 - comp.footprint_rot) % 360.0 + else: + if self.mirror_bottom: + comp.footprint_rot = 180 - comp.footprint_rot + if self.negative_bottom: comp.footprint_rot -= angle else: comp.footprint_rot += angle - if self.invert_bottom and comp.bottom: + if self.invert_bottom: comp.footprint_rot = -comp.footprint_rot - comp.footprint_rot = comp.footprint_rot % 360 - if GS.debug_level > 2: - logger.debug('Rotating ref: {} {}: {} -> {}'. - format(comp.ref, comp.footprint, old_angle, comp.footprint_rot)) + comp.offset_footprint_rot = old_footprint_rot + else: + comp.footprint_rot += angle + comp.offset_footprint_rot = old_footprint_rot + comp.footprint_rot = comp.footprint_rot % 360 + if GS.debug_level > 2: + logger.debug('Rotating ref: {} {}: {} -> {}'. + format(comp.ref, comp.footprint, old_footprint_rot, comp.footprint_rot)) + + def apply_field_rotation(self, comp): + for f in self.rot_fields: + value = comp.get_field_value(f) + if value: + try: + angle = float(value) + except ValueError: + logger.warning(f'{W_BADANGLE}Wrong angle `{value}` in {f} field of {comp.ref}') + angle = 0 + self.apply_rotation_angle(comp, angle, bennymeg_mode=True) return + + def apply_rotation(self, comp): + if self.apply_field_rotation(comp): + return + # Try with the regex + for regex, angle in self._rot: + if regex.search(comp.footprint): + self.apply_rotation_angle(comp, angle) + return + # No rotation, apply 0 to apply bottom adjusts + self.apply_rotation_angle(comp, 0) + + def apply_field_offset(self, comp): + for f in self.offset_fields: + value = comp.get_field_value(f) + if value: + try: + pos_offset_x = float(value.split(",")[0]) + pos_offset_y = float(value.split(",")[1]) + except ValueError: + logger.warning(f'{W_BADOFFSET}Wrong offset `{value}` in {f} field of {comp.ref}') + return + rotation = radians(comp.offset_footprint_rot) + rsin = sin(rotation) + rcos = cos(rotation) + comp.pos_offset_x = pos_offset_x * rcos - pos_offset_y * rsin + comp.pos_offset_y = pos_offset_x * rsin + pos_offset_y * rcos + return + + def filter(self, comp): + """ Apply the rotation """ + logger.error(f"{self.invert_bottom} {self.mirror_bottom}") + if (self.skip_top and not comp.bottom) or (self.skip_bottom and comp.bottom): + # Component should be excluded + return + self.apply_rotation(comp) + self.apply_field_offset(comp) diff --git a/kibot/kicad/v5_sch.py b/kibot/kicad/v5_sch.py index ac8cf709..8e7734b7 100644 --- a/kibot/kicad/v5_sch.py +++ b/kibot/kicad/v5_sch.py @@ -906,6 +906,8 @@ class SchematicComponent(object): self.has_pcb_info = False self.qty = 1 self.annotation_error = False + # Position offset i.e. from the rotation filter + self.pos_offset_x = self.pos_offset_y = None # KiCad 5 PCB flags (mutually exclusive) self.smd = False self.virtual = False diff --git a/kibot/misc.py b/kibot/misc.py index 861127fb..297811d6 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -288,6 +288,7 @@ W_MISSDIR = '(W132) ' W_EXTRAINVAL = '(W133) ' W_BADANGLE = '(W134) ' W_VALMISMATCH = '(W135) ' +W_BADOFFSET = '(W136) ' # Somehow arbitrary, the colors are real, but can be different PCB_MAT_COLORS = {'fr1': "937042", 'fr2': "949d70", 'fr3': "adacb4", 'fr4': "332B16", 'fr5': "6cc290"} PCB_FINISH_COLORS = {'hal': "8b898c", 'hasl': "8b898c", 'imag': "8b898c", 'enig': "cfb96e", 'enepig': "cfb96e", diff --git a/kibot/out_position.py b/kibot/out_position.py index bf034d21..619a508c 100644 --- a/kibot/out_position.py +++ b/kibot/out_position.py @@ -288,6 +288,10 @@ class PositionOptions(VariantOptions): center = GS.get_center(m) center_x = center.x center_y = center.y + if c.pos_offset_x is not None: + # Offset from the rotation filter + center_x += c.pos_offset_x + center_y += c.pos_offset_y if value is None: value = m.GetValue() footprint = str(m.GetFPID().GetLibItemName()) # pcbnew.UTF8 type diff --git a/kibot/resources/config_templates/JLCPCB.kibot.yaml b/kibot/resources/config_templates/JLCPCB.kibot.yaml index 5b755218..38a5cbff 100644 --- a/kibot/resources/config_templates/JLCPCB.kibot.yaml +++ b/kibot/resources/config_templates/JLCPCB.kibot.yaml @@ -140,7 +140,7 @@ definitions: _KIBOT_IMPORT_DIR: 'JLCPCB' _KIBOT_MANF_DIR: '@_KIBOT_IMPORT_DIR@' _KIBOT_MANF_DIR_COMP: '@_KIBOT_IMPORT_DIR@' - _KIBOT_POS_PRE_TRANSFORM: _rot_footprint + _KIBOT_POS_PRE_TRANSFORM: _rot_footprint_jlcpcb _KIBOT_POS_ENABLED: true _KIBOT_BOM_ENABLED: true _KIBOT_GERBER_LAYERS: |