293 lines
15 KiB
Python
293 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
# 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.
|
|
# See: https://github.com/matthewlai/JLCKicadTools/blob/master/jlc_kicad_tools/cpl_rotations_db.csv
|
|
# 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, DEFAULT_ROTATIONS, DEFAULT_ROT_FIELDS, DEFAULT_OFFSETS, DEFAULT_OFFSET_FIELDS
|
|
from . import log
|
|
|
|
logger = log.get_logger()
|
|
|
|
|
|
def get_field_value(comp, field):
|
|
""" Helper to process the footprint field in a special way """
|
|
field = field.lower()
|
|
if field == 'footprint':
|
|
# The databases are created just for the name of the footprint
|
|
return comp.footprint
|
|
if field == 'full footprint':
|
|
# The real 'footprint' field has it
|
|
field = 'footprint'
|
|
return comp.get_field_value(field)
|
|
|
|
|
|
class Regex(Optionable):
|
|
""" Implements the pair column/regex """
|
|
def __init__(self, regex=None, angle=None, offset_x=None, offset_y=None):
|
|
super().__init__()
|
|
self._unknown_is_error = True
|
|
with document:
|
|
self.field = 'footprint'
|
|
""" Name of field to apply the regular expression.
|
|
Use `_field_lcsc_part` to get the value defined in the global options.
|
|
Use `Footprint` for the name of the footprint without a library.
|
|
Use `Full Footprint` for the name of the footprint including the library """
|
|
self.regex = ''
|
|
""" Regular expression to match """
|
|
self.regexp = None
|
|
""" {regex} """
|
|
self.angle = 0.0
|
|
""" Rotation offset to apply to the matched component """
|
|
self.offset_x = 0.0
|
|
""" X position offset to apply to the matched component """
|
|
self.offset_y = 0.0
|
|
""" Y position offset to apply to the matched component """
|
|
self.apply_angle = True
|
|
""" Apply the angle offset """
|
|
self.apply_offset = True
|
|
""" Apply the position offset """
|
|
if regex is not None:
|
|
self.regex = regex
|
|
if angle is not None:
|
|
self.angle = angle
|
|
if offset_x is not None:
|
|
self.offset_x = offset_x
|
|
if offset_y is not None:
|
|
self.offset_y = offset_y
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
if not self.field:
|
|
raise KiPlotConfigurationError(f"Missing or empty `field` name ({str(self._tree)})")
|
|
if not self.regex:
|
|
raise KiPlotConfigurationError(f"Missing or empty `regex` for `{self.field}` field")
|
|
# We could be wanting to add a rule to avoid a default change
|
|
# if self.angle == 0.0 and self.offset_x == 0.0 and self.offset_y == 0.0:
|
|
# raise KiPlotConfigurationError(f"Rule for `{self.field}` field without any adjust")
|
|
self.field = self.solve_field_name(self.field).lower()
|
|
|
|
|
|
@filter_class
|
|
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__()
|
|
self._is_transform = True
|
|
with document:
|
|
self.extend = True
|
|
""" Extends the internal list of rotations with the one provided.
|
|
Otherwise just use the provided list.
|
|
Note that the provided list has more precedence than the internal list """
|
|
self.negative_bottom = True
|
|
""" Rotation for bottom components is computed via subtraction as `(component rot - angle)` """
|
|
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.
|
|
Footprints matching the regular expression will be rotated the indicated angle.
|
|
The angle matches the matthewlai/JLCKicadTools plugin specs """
|
|
self.offsets = Optionable
|
|
""" [list(list(string))] A list of pairs regular expression/offset.
|
|
Footprints matching the regular expression will be moved the specified offset.
|
|
The offset must be two numbers separated by a comma. The first is the X offset.
|
|
The signs matches the matthewlai/JLCKicadTools plugin specs """
|
|
self.rotations_and_offsets = Regex
|
|
""" [list(dict)] A list of rules to match components and specify the rotation and offsets.
|
|
This is a more flexible version of the `rotations` and `offsets` options.
|
|
Note that this list has more precedence """
|
|
self.skip_bottom = False
|
|
""" 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.
|
|
This option also controls the way offset signs are interpreted. When enabled the offsets matches this plugin,
|
|
when disabled matches the interpretation used by the matthewlai/JLCKicadTools plugin """
|
|
|
|
def config(self, parent):
|
|
super().config(parent)
|
|
self._rot = []
|
|
self._offset = []
|
|
# The main list first
|
|
if isinstance(self.rotations_and_offsets, list):
|
|
for v in self.rotations_and_offsets:
|
|
v.regex = compile(v.regex)
|
|
if v.apply_angle:
|
|
self._rot.append(v)
|
|
if v.apply_offset:
|
|
self._offset.append(v)
|
|
# List of rotations
|
|
if isinstance(self.rotations, list):
|
|
for r in self.rotations:
|
|
if len(r) != 2:
|
|
raise KiPlotConfigurationError("Each regex/angle pair must contain exactly two values, not {} ({})".
|
|
format(len(r), r))
|
|
try:
|
|
angle = float(r[1])
|
|
except ValueError:
|
|
raise KiPlotConfigurationError("The second value in the regex/angle pairs must be a number, not {}".
|
|
format(r[1]))
|
|
self._rot.append(Regex(regex=compile(r[0]), angle=angle))
|
|
# List of offsets
|
|
if isinstance(self.offsets, list):
|
|
for r in self.offsets:
|
|
if len(r) != 2:
|
|
raise KiPlotConfigurationError("Each regex/offset pair must contain exactly two values, not {} ({})".
|
|
format(len(r), r))
|
|
try:
|
|
offset_x = float(r[1].split(",")[0])
|
|
offset_y = float(r[1].split(",")[1])
|
|
except ValueError:
|
|
raise KiPlotConfigurationError("The second value in the regex/offset pairs must be two numbers "
|
|
f"separated by a comma, not {r[1]}")
|
|
self._offset.append(Regex(regex=compile(r[0]), offset_x=offset_x, offset_y=offset_y))
|
|
if self.extend:
|
|
for regex_str, angle in DEFAULT_ROTATIONS:
|
|
self._rot.append(Regex(regex=compile(regex_str), angle=angle))
|
|
for regex_str, offset in DEFAULT_OFFSETS:
|
|
self._offset.append(Regex(regex=compile(regex_str), offset_x=offset[0], offset_y=offset[1]))
|
|
if not self._rot and not self._offset:
|
|
raise KiPlotConfigurationError("No rotations and/or offsets 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 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/
|
|
# Currently 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:
|
|
comp.footprint_rot = -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(f'- rotating {comp.ref} from {old_footprint_rot} to {comp.footprint_rot}')
|
|
|
|
def apply_field_rotation(self, comp):
|
|
for f in self.rot_fields:
|
|
value = get_field_value(comp, 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
|
|
logger.debugl(2, f'- rotation from field `{f}`: {angle}')
|
|
self.apply_rotation_angle(comp, angle, bennymeg_mode=True)
|
|
return True
|
|
return False
|
|
|
|
def apply_rotation(self, comp):
|
|
if self.apply_field_rotation(comp):
|
|
return
|
|
# Try with the regex
|
|
for v in self._rot:
|
|
value = get_field_value(comp, v.field)
|
|
if value and v.regex.search(value):
|
|
logger.debugl(2, f'- matched {v.regex} on field {v.field} with {v.angle} degrees')
|
|
self.apply_rotation_angle(comp, v.angle)
|
|
return
|
|
# No rotation, apply 0 to apply bottom adjusts
|
|
self.apply_rotation_angle(comp, 0)
|
|
|
|
def apply_offset_value(self, comp, angle, pos_offset_x, pos_offset_y):
|
|
if angle:
|
|
rotation = radians(angle)
|
|
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
|
|
logger.debugl(2, f'- rotating offset {angle} degrees: {comp.pos_offset_x}, {comp.pos_offset_y} mm')
|
|
else:
|
|
comp.pos_offset_x = pos_offset_x
|
|
comp.pos_offset_y = pos_offset_y
|
|
# The signs here matches matthewlai/JLCKicadTools offsets because the database comes from this plugin
|
|
comp.pos_offset_x = -GS.from_mm(comp.pos_offset_x)
|
|
comp.pos_offset_y = GS.from_mm(comp.pos_offset_y)
|
|
logger.debugl(2, f'- final offset {comp.pos_offset_x}, {comp.pos_offset_y} KiCad IUs')
|
|
|
|
def apply_field_offset(self, comp):
|
|
for f in self.offset_fields:
|
|
value = get_field_value(comp, 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 False
|
|
logger.debugl(2, f'- offset from field `{f}`: {pos_offset_x}, {pos_offset_y} mm')
|
|
if self.bennymeg_mode:
|
|
# Signs here matches bennymeg/JLC-Plugin-for-KiCad because the fields usage comes from it
|
|
pos_offset_x = -pos_offset_x
|
|
pos_offset_y = -pos_offset_y
|
|
logger.debugl(2, f'- changing to {pos_offset_x}, {pos_offset_y} mm to match signs')
|
|
self.apply_offset_value(comp, comp.offset_footprint_rot, pos_offset_x, pos_offset_y)
|
|
return True
|
|
return False
|
|
|
|
def apply_offset(self, comp):
|
|
if self.apply_field_offset(comp):
|
|
return
|
|
# Try with the regex
|
|
for v in self._offset:
|
|
value = get_field_value(comp, v.field)
|
|
if value and v.regex.search(value):
|
|
logger.debugl(2, f'- matched {v.regex} on field {v.field} with offset {v.offset_x}, {v.offset_y} mm')
|
|
self.apply_offset_value(comp, comp.footprint_rot, v.offset_x, v.offset_y)
|
|
return
|
|
|
|
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
|
|
logger.debugl(2, f'{comp.ref} ({comp.footprint}):')
|
|
self.apply_rotation(comp)
|
|
self.apply_offset(comp)
|