[Rotation Filter][Added] rotations_and_offsets option

Implements a more flexible mechanism to select rotations and offsets.
So you can have two different rotations applied to the same footprint,
i.e. different components with the same footprint but different
orientation in the reel.
This commit is contained in:
Salvador E. Tropea 2023-11-17 10:46:59 -03:00
parent 623231be8d
commit a76b4771c4
3 changed files with 117 additions and 22 deletions

View File

@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `offsets`: a list of pairs containing regex and offset ("x, y") - `offsets`: a list of pairs containing regex and offset ("x, y")
- `bennymeg_mode`: used to provide compatibility with the - `bennymeg_mode`: used to provide compatibility with the
bennymeg/JLC-Plugin-for-KiCad tool. bennymeg/JLC-Plugin-for-KiCad tool.
- `rotations_and_offsets`: a more flexible mechanism to select
rotations and offsets. So you can have two different rotations
applied to the same footprint, i.e. different components with
the same footprint but different orientation in the reel.
- 3D outputs: - 3D outputs:
- `download_lcsc` option to disable LCSC 3D model download (See #415) - `download_lcsc` option to disable LCSC 3D model download (See #415)
- BoM: - BoM:

View File

@ -134,6 +134,7 @@ Supported filters
- ``comment`` :index:`: <pair: filter - rot_footprint; comment>` [string=''] A comment for documentation purposes. - ``comment`` :index:`: <pair: filter - rot_footprint; comment>` [string=''] A comment for documentation purposes.
- ``extend`` :index:`: <pair: filter - rot_footprint; extend>` [boolean=true] Extends the internal list of rotations with the one provided. - ``extend`` :index:`: <pair: filter - rot_footprint; extend>` [boolean=true] Extends the internal list of rotations with the one provided.
Otherwise just use the provided list. Otherwise just use the provided list.
Note that the provided list has more precendence than the internal list.
- ``invert_bottom`` :index:`: <pair: filter - rot_footprint; invert_bottom>` [boolean=false] Rotation for bottom components is negated, resulting in either: `(- component rot - angle)` - ``invert_bottom`` :index:`: <pair: filter - rot_footprint; invert_bottom>` [boolean=false] Rotation for bottom components is negated, resulting in either: `(- component rot - angle)`
or when combined with `negative_bottom`, `(angle - component rot)`. or when combined with `negative_bottom`, `(angle - component rot)`.
- ``mirror_bottom`` :index:`: <pair: filter - rot_footprint; mirror_bottom>` [boolean=false] The original component rotation for components in the bottom is mirrored before applying - ``mirror_bottom`` :index:`: <pair: filter - rot_footprint; mirror_bottom>` [boolean=false] The original component rotation for components in the bottom is mirrored before applying
@ -157,6 +158,24 @@ Supported filters
Footprints matching the regular expression will be rotated the indicated angle. Footprints matching the regular expression will be rotated the indicated angle.
The angle matches the matthewlai/JLCKicadTools plugin specs. The angle matches the matthewlai/JLCKicadTools plugin specs.
- ``rotations_and_offsets`` :index:`: <pair: filter - rot_footprint; rotations_and_offsets>` [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.
- Valid keys:
- ``angle`` :index:`: <pair: filter - rot_footprint - rotations_and_offsets; angle>` [number=0.0] Rotation offset to apply to the matched component.
- ``apply_angle`` :index:`: <pair: filter - rot_footprint - rotations_and_offsets; apply_angle>` [boolean=true] Apply the angle offset.
- ``apply_offset`` :index:`: <pair: filter - rot_footprint - rotations_and_offsets; apply_offset>` [boolean=true] Apply the position offset.
- ``field`` :index:`: <pair: filter - rot_footprint - rotations_and_offsets; field>` [string='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.
- ``offset_x`` :index:`: <pair: filter - rot_footprint - rotations_and_offsets; offset_x>` [number=0.0] X position offset to apply to the matched component.
- ``offset_y`` :index:`: <pair: filter - rot_footprint - rotations_and_offsets; offset_y>` [number=0.0] Y position offset to apply to the matched component.
- ``regex`` :index:`: <pair: filter - rot_footprint - rotations_and_offsets; regex>` [string=''] Regular expression to match.
- *regexp* :index:`: <pair: filter - rot_footprint - rotations_and_offsets; regexp>` Alias for regex.
- ``skip_bottom`` :index:`: <pair: filter - rot_footprint; skip_bottom>` [boolean=false] Do not rotate components on the bottom. - ``skip_bottom`` :index:`: <pair: filter - rot_footprint; skip_bottom>` [boolean=false] Do not rotate components on the bottom.
- ``skip_top`` :index:`: <pair: filter - rot_footprint; skip_top>` [boolean=false] Do not rotate components on the top. - ``skip_top`` :index:`: <pair: filter - rot_footprint; skip_top>` [boolean=false] Do not rotate components on the top.

View File

@ -99,6 +99,64 @@ DEFAULT_OFFSETS = [["^USB_C_Receptacle_XKB_U262-16XN-4BVC11", (0.0, -1.44)],
DEFAULT_OFFSET_FIELDS = ['JLCPCB Position Offset', 'JLCPosOffset'] DEFAULT_OFFSET_FIELDS = ['JLCPCB Position Offset', 'JLCPosOffset']
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 @filter_class
class Rot_Footprint(BaseFilter): # noqa: F821 class Rot_Footprint(BaseFilter): # noqa: F821
""" Footprint Rotator """ Footprint Rotator
@ -112,7 +170,8 @@ class Rot_Footprint(BaseFilter): # noqa: F821
with document: with document:
self.extend = True self.extend = True
""" Extends the internal list of rotations with the one provided. """ Extends the internal list of rotations with the one provided.
Otherwise just use the provided list """ Otherwise just use the provided list.
Note that the provided list has more precedence than the internal list """
self.negative_bottom = True self.negative_bottom = True
""" Rotation for bottom components is computed via subtraction as `(component rot - angle)` """ """ Rotation for bottom components is computed via subtraction as `(component rot - angle)` """
self.invert_bottom = False self.invert_bottom = False
@ -130,6 +189,10 @@ class Rot_Footprint(BaseFilter): # noqa: F821
Footprints matching the regular expression will be moved the specified 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 offset must be two numbers separated by a comma. The first is the X offset.
The signs matches the matthewlai/JLCKicadTools plugin specs """ 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 self.skip_bottom = False
""" Do not rotate components on the bottom """ """ Do not rotate components on the bottom """
self.skip_top = False self.skip_top = False
@ -152,41 +215,48 @@ class Rot_Footprint(BaseFilter): # noqa: F821
def config(self, parent): def config(self, parent):
super().config(parent) super().config(parent)
# List of rotations
self._rot = [] 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): if isinstance(self.rotations, list):
for r in self.rotations: for r in self.rotations:
if len(r) != 2: if len(r) != 2:
raise KiPlotConfigurationError("Each regex/angle pair must contain exactly two values, not {} ({})". raise KiPlotConfigurationError("Each regex/angle pair must contain exactly two values, not {} ({})".
format(len(r), r)) format(len(r), r))
regex = compile(r[0])
try: try:
angle = float(r[1]) angle = float(r[1])
except ValueError: except ValueError:
raise KiPlotConfigurationError("The second value in the regex/angle pairs must be a number, not {}". raise KiPlotConfigurationError("The second value in the regex/angle pairs must be a number, not {}".
format(r[1])) format(r[1]))
self._rot.append([regex, angle]) self._rot.append(Regex(regex=compile(r[0]), angle=angle))
# List of offsets # List of offsets
self._offset = []
if isinstance(self.offsets, list): if isinstance(self.offsets, list):
for r in self.offsets: for r in self.offsets:
if len(r) != 2: if len(r) != 2:
raise KiPlotConfigurationError("Each regex/offset pair must contain exactly two values, not {} ({})". raise KiPlotConfigurationError("Each regex/offset pair must contain exactly two values, not {} ({})".
format(len(r), r)) format(len(r), r))
regex = compile(r[0])
try: try:
offset = (float(r[1].split(",")[0]), float(r[1].split(",")[1])) offset_x = float(r[1].split(",")[0])
offset_y = float(r[1].split(",")[1])
except ValueError: except ValueError:
raise KiPlotConfigurationError("The second value in the regex/offset pairs must be two numbers " raise KiPlotConfigurationError("The second value in the regex/offset pairs must be two numbers "
f"separated by a comma, not {r[1]}") f"separated by a comma, not {r[1]}")
self._offset.append([regex, offset]) self._offset.append(Regex(regex=compile(r[0]), offset_x=offset_x, offset_y=offset_y))
if self.extend: if self.extend:
for regex_str, angle in DEFAULT_ROTATIONS: for regex_str, angle in DEFAULT_ROTATIONS:
self._rot.append([compile(regex_str), angle]) self._rot.append(Regex(regex=compile(regex_str), angle=angle))
for regex_str, offset in DEFAULT_OFFSETS: for regex_str, offset in DEFAULT_OFFSETS:
self._offset.append([compile(regex_str), offset]) self._offset.append(Regex(regex=compile(regex_str), offset_x=offset[0], offset_y=offset[1]))
if not self._rot: if not self._rot and not self._offset:
raise KiPlotConfigurationError("No rotations provided") raise KiPlotConfigurationError("No rotations and/or offsets provided")
self.rot_fields = self.force_list(self.rot_fields, default=DEFAULT_ROT_FIELDS) 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) self.offset_fields = self.force_list(self.offset_fields, default=DEFAULT_OFFSET_FIELDS)
@ -220,7 +290,7 @@ class Rot_Footprint(BaseFilter): # noqa: F821
def apply_field_rotation(self, comp): def apply_field_rotation(self, comp):
for f in self.rot_fields: for f in self.rot_fields:
value = comp.get_field_value(f) value = get_field_value(comp, f)
if value: if value:
try: try:
angle = float(value) angle = float(value)
@ -236,10 +306,11 @@ class Rot_Footprint(BaseFilter): # noqa: F821
if self.apply_field_rotation(comp): if self.apply_field_rotation(comp):
return return
# Try with the regex # Try with the regex
for regex, angle in self._rot: for v in self._rot:
if regex.search(comp.footprint): value = get_field_value(comp, v.field)
logger.debugl(2, f'- matched {regex} with {angle} degrees') if value and v.regex.search(value):
self.apply_rotation_angle(comp, angle) logger.debugl(2, f'- matched {v.regex} on field {v.field} with {v.angle} degrees')
self.apply_rotation_angle(comp, v.angle)
return return
# No rotation, apply 0 to apply bottom adjusts # No rotation, apply 0 to apply bottom adjusts
self.apply_rotation_angle(comp, 0) self.apply_rotation_angle(comp, 0)
@ -262,7 +333,7 @@ class Rot_Footprint(BaseFilter): # noqa: F821
def apply_field_offset(self, comp): def apply_field_offset(self, comp):
for f in self.offset_fields: for f in self.offset_fields:
value = comp.get_field_value(f) value = get_field_value(comp, f)
if value: if value:
try: try:
pos_offset_x = float(value.split(",")[0]) pos_offset_x = float(value.split(",")[0])
@ -284,10 +355,11 @@ class Rot_Footprint(BaseFilter): # noqa: F821
if self.apply_field_offset(comp): if self.apply_field_offset(comp):
return return
# Try with the regex # Try with the regex
for regex, offset in self._offset: for v in self._offset:
if regex.search(comp.footprint): value = get_field_value(comp, v.field)
logger.debugl(2, f'- matched {regex} with offset {offset[0]}, {offset[1]} mm') if value and v.regex.search(value):
self.apply_offset_value(comp, comp.footprint_rot, offset[0], offset[1]) 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 return
def filter(self, comp): def filter(self, comp):