from datetime import datetime from pathlib import Path from typing import List, Literal, Optional, Tuple, Union INCH = 25.4 class Footprint: text_size = { # layer: (size_x, size_y, stroke) "F.SilkS": (0.030 * INCH, 0.030 * INCH, 0.007 * INCH), "B.SilkS": (0.030 * INCH, 0.030 * INCH, 0.007 * INCH), None: (0.025 * INCH, 0.025 * INCH, 0.005 * INCH), } line_size = { "F_SilkS": 0.007, "B_SilkS": 0.007, None: 0.005, } def __init__( self, name: str, lib: Union[Path, str], description: Optional[str] = None, keywords: Optional[List[str]] = None, ): self.name = name self.lib = Path(lib) self.description = "" if description is None else description self._pads = [] self._models = [] self._contents = ( "(" f'footprint "{self.name}"\n' f' (version {datetime.now().strftime("%Y%m%d")})\n' " (generator pcbnew)\n" ' (layer "F.Cu")\n' " (attr smd)\n" ) if description is not None: self._contents += f' (descr "{description}")\n' if keywords is not None: self._contents += f' (tags "{" ".join(keywords)}")\n' def add_pad( self, id: Union[int, str], x: float, y: float, size: Union[float, Tuple[float]], hole: Optional[float] = None, shape: Optional[Literal["circle", "rect", "round_rect"]] = None, ): try: size = (float(size), float(size)) except TypeError: size = tuple(size) if shape not in [None, "circle", "rect", "round_rect"]: raise ValueError if hole is not None: self._contents.replace("(attr smd)", "(attr through_hole)") self._contents += ( f' (pad "{id}" {"thru_hole" if hole is not None else "smd"} {shape} ' + f"(at {x} {y}) " + f"(size {size[0]} {size[1]}) " + (f"(drill {hole}) " if hole is not None else "") + ("(layers *.Cu *.Mask)" if hole is not None else '(layers "F.Cu" "F.Paste" "F.Mask")') + ")\n" ) def add_model( self, path: Union[Path, str], offset: Tuple[float] = (0, 0, 0), rotate: Tuple[float] = (0, 0, 0), scale: Tuple[float] = (1, 1, 1), ): self._contents += ( f' (model "{str(Path(path).relative_to(self.lib))}" ' f"(offset (xyz {offset[0]} {offset[1]} {offset[2]})) " f"(scale (xyz {scale[0]} {scale[1]} {scale[2]})) " f"(rotate (xyz {rotate[0]} {rotate[1]} {rotate[2]})) " ")\n" ) def add_text( self, name: str, text: str, x: float, y: float, layer: str, rotation: float = 0, size: Optional[Tuple[float]] = None, hidden: bool = False, ): if size is None: if layer in self.text_size: size = self.text_size[layer] else: size = self.text_size[None] self._contents += ( f' (fp_text {name} "{text}" ' f"(at {x} {y} {rotation}) " f'(layer "{layer}") ' f"{'hide' if hidden else ''}" f"(effects (font (size {size[0]} {size[1]}) (thickness {size[2]}))) " ")\n" ) def add_line( self, start: Tuple[float], end: Tuple[float], layer: str, stroke: Optional[float] = None, ): if stroke is None: if layer in self.text_size: stroke = self.text_size[layer][2] else: stroke = self.text_size[None][2] stroke = float(stroke) self._contents += ( f' (fp_line (start {start[0]} {start[1]}) (end {end[0]} {end[1]}) (layer "{layer}") (width {stroke}))\n' ) def add_rect( self, start: Tuple[float], end: Tuple[float], layer: str, stroke: Optional[float] = None, fill: bool = False, ): if stroke is None: if layer in self.text_size: stroke = self.text_size[layer][2] else: stroke = self.text_size[None][2] stroke = float(stroke) self._contents += ( " (fp_rect " f"(start {start[0]} {start[1]}) " f"(end {end[0]} {end[1]}) " f"(stroke (width {stroke}) (type default)) " f'(fill {"none" if not fill else "solidButIDontKnowWhatStringToUse"}) ' f'(layer "{layer}") ' ")\n" ) def write(self): with open(self.lib / f"{self.name}.kicad_mod", "w") as f: f.write(self._contents + ")")