Beispiel #1
0
 def ipc_name(self) -> str:
     return '{}{}{}P{}X{}X{}-{}'.format(
         self.name_prefix,
         self.name,
         fd(self.pitch),
         fd(self.lead_span_x),
         fd(self.lead_span_y),
         fd(self.height_nom),
         self.lead_count,
     )
def generate_pkg(
    dirpath: str,
    author: str,
    name: str,
    description: str,
    pkgcat: str,
    keywords: str,
    config: DfnConfig,
    make_exposed: bool,
    create_date: Optional[str] = None,
) -> str:
    category = 'pkg'
    lines = []

    full_name = name.format(length=fd(config.length),
                            width=fd(config.width),
                            height=fd(config.height_nominal),
                            pin_count=config.pin_count,
                            pitch=fd(config.pitch))

    # Add pad length for otherwise identical names/packages
    if config.print_pad:
        full_name += "P{:s}".format(fd(config.lead_length))

    if make_exposed:
        # According to: http://www.ocipcdc.org/archive/What_is_New_in_IPC-7351C_03_11_2015.pdf
        exp_width = fd(config.exposed_width)
        exp_length = fd(config.exposed_length)
        if exp_width == exp_length:
            full_name += "T{}".format(exp_width)
        else:
            full_name += "T{}X{}".format(exp_width, exp_length)

    # Override name if specified
    if config.name:
        full_name = config.name

    full_description = description.format(height=config.height_nominal,
                                          pin_count=config.pin_count,
                                          pitch=config.pitch,
                                          width=config.width,
                                          length=config.length)
    if make_exposed:
        full_description += "\\nExposed Pad: {:.2f} x {:.2f} mm".format(
            config.exposed_width, config.exposed_length)

    if config.print_pad:
        full_description += "\\nPad length: {:.2f} mm".format(config.lead_length)

    def _uuid(identifier: str) -> str:
        return uuid(category, full_name, identifier)

    uuid_pkg = _uuid('pkg')
    uuid_pads = [_uuid('pad-{}'.format(p)) for p in range(1, config.pin_count + 1)]

    if make_exposed:
        uuid_exp = _uuid('exposed')

    print('Generating {}: {}'.format(full_name, uuid_pkg))

    # General info
    lines.append('(librepcb_package {}'.format(uuid_pkg))
    lines.append(' (name "{}")'.format(full_name))
    lines.append(' (description "{}\\n\\nGenerated with {}")'.format(full_description,
                                                                     GENERATOR_NAME))
    if config.keywords:
        lines.append(' (keywords "dfn{},{},{}")'.format(config.pin_count, keywords, config.keywords.lower()))
    else:
        lines.append(' (keywords "dfn{},{}")'.format(config.pin_count, keywords))
    lines.append(' (author "{}")'.format(author))
    lines.append(' (version "0.1.1")')
    lines.append(' (created {})'.format(create_date or now()))
    lines.append(' (deprecated false)')
    lines.append(' (category {})'.format(pkgcat))

    # Create Pad UUIDs
    for p in range(1, config.pin_count + 1):
        lines.append(' (pad {} (name "{}"))'.format(uuid_pads[p - 1], p))
    if make_exposed:
        lines.append(' (pad {} (name "{}"))'.format(uuid_exp, 'ExposedPad'))

    # Create Footprint function
    def _generate_footprint(key: str, name: str, pad_extension: float) -> None:
        # Create Meta-data
        uuid_footprint = _uuid('footprint-{}'.format(key))
        lines.append(' (footprint {}'.format(uuid_footprint))
        lines.append('  (name "{}")'.format(name))
        lines.append('  (description "")')

        pad_length = config.lead_length + config.toe_heel + pad_extension
        exposed_length = config.exposed_length
        abs_pad_pos_x = (config.width / 2) - (config.lead_length / 2) + (config.toe_heel / 2) + (pad_extension / 2)

        # Check clearance and make pads smaller if required
        if make_exposed:
            clearance = (config.width / 2) - config.lead_length - (exposed_length / 2)
            if clearance < MIN_CLEARANCE:
                print("Increasing clearance from {:.2f} to {:.2f}".format(clearance, MIN_CLEARANCE))
                d_clearance = (MIN_CLEARANCE - clearance) / 2
                pad_length = pad_length - d_clearance
                exposed_length = exposed_length - 2 * d_clearance
                abs_pad_pos_x = abs_pad_pos_x + (d_clearance / 2)

            if exposed_length < MIN_TRACE:
                print("Increasing exposed path width from {:.2f} to {:.2f}".format(exposed_length, MIN_TRACE))
                d_exp = MIN_TRACE - exposed_length
                exposed_length = exposed_length + d_exp
                pad_length = pad_length - (d_exp / 2)
                abs_pad_pos_x = abs_pad_pos_x + (d_exp / 4)

        # Place pads
        for pad_idx, pad_nr in enumerate(range(1, config.pin_count + 1)):
            half_n_pads = config.pin_count // 2
            pad_pos_y = get_y(pad_idx % half_n_pads + 1, half_n_pads, config.pitch, False)

            if pad_idx < (config.pin_count / 2):
                pad_pos_x = - abs_pad_pos_x
            else:
                pad_pos_x = abs_pad_pos_x
                pad_pos_y = - pad_pos_y

            lines.append('  (pad {} (side top) (shape rect)'.format(uuid_pads[pad_idx]))
            lines.append('   (position {} {}) (rotation 0.0) (size {} {}) (drill 0.0)'.format(
                         ff(pad_pos_x), ff(pad_pos_y),
                         ff(pad_length), ff(config.lead_width)))
            lines.append('  )')

        # Make exposed pad, if required
        # TODO: Handle pin1_corner_dx_dy in config once custom pad shapes are possible
        if make_exposed:
            lines.append('  (pad {} (side top) (shape rect)'.format(uuid_exp))
            lines.append('   (position 0.0 0.0) (rotation 0.0) (size {} {}) (drill 0.0)'.format(
                         ff(exposed_length), ff(config.exposed_width)))
            lines.append('  )')

            # Measure clearance pad-exposed pad
            clearance = abs(pad_pos_x) - (pad_length / 2) - (exposed_length / 2)
            if np.around(clearance, decimals=2) < MIN_CLEARANCE:
                print("Warning: minimal clearance violated in {}: {:.4f} < {:.2f}".format(full_name, clearance, MIN_CLEARANCE))

        # Create Silk Screen (lines and dot only)
        silk_down = (config.length / 2 - SILKSCREEN_OFFSET -
                     get_y(1, half_n_pads, config.pitch, False) -
                     config.lead_width / 2 -
                     SILKSCREEN_LINE_WIDTH / 2)    # required for round ending of line

        # Measure clearance silkscreen to exposed pad
        silk_top_line_height = config.length / 2
        if make_exposed:
            silk_clearance = silk_top_line_height - (SILKSCREEN_LINE_WIDTH / 2) - (config.exposed_width / 2)
            if np.around(silk_clearance, decimals=2) < SILKSCREEN_OFFSET:
                silk_top_line_height = silk_top_line_height + (SILKSCREEN_OFFSET - silk_clearance)
                silk_down = silk_down + (SILKSCREEN_OFFSET - silk_clearance)
                print("Increasing exp-silk clearance from {:.4f} to {:.2f}".format(silk_clearance, SILKSCREEN_OFFSET))

        for idx, silkscreen_pos in enumerate([-1, 1]):
            uuid_silkscreen_poly = _uuid('polygon-silkscreen-{}-{}'.format(key, idx))
            lines.append('  (polygon {} (layer top_placement)'.format(uuid_silkscreen_poly))
            lines.append('   (width {}) (fill false) (grab_area false)'.format(
                SILKSCREEN_LINE_WIDTH))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                         ff(-config.width / 2),
                         ff(silkscreen_pos * (silk_top_line_height - silk_down))))
            # If this is negative, the silkscreen line has to be moved away from
            # the real position, in order to keep the required distance to the
            # pad. We then only draw a single line, so we can omit the parts below.
            if silk_down > 0:
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                             ff(-config.width / 2),
                             ff(silkscreen_pos * silk_top_line_height)))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                             ff(config.width / 2),
                             ff(silkscreen_pos * silk_top_line_height)))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                         ff(config.width / 2),
                         ff(silkscreen_pos * (silk_top_line_height - silk_down))))

            lines.append('  )')

        # Create leads on docu
        uuid_leads = [_uuid('lead-{}'.format(p)) for p in range(1, config.pin_count + 1)]
        for pad_idx, pad_nr in enumerate(range(1, config.pin_count + 1)):
            lead_uuid = uuid_leads[pad_idx]

            # Make silkscreen lead exact pad width and length
            half_n_pads = config.pin_count // 2
            pad_pos_y = get_y(pad_idx % half_n_pads + 1, half_n_pads, config.pitch, False)
            if pad_idx >= (config.pin_count / 2):
                pad_pos_y = - pad_pos_y
            y_min = pad_pos_y - config.lead_width / 2
            y_max = pad_pos_y + config.lead_width / 2

            x_max = config.width / 2
            x_min = x_max - config.lead_length
            if pad_idx < (config.pin_count / 2):
                x_min, x_max = - x_min, - x_max

            # Convert numbers to librepcb format
            x_min_str, x_max_str = ff(x_min), ff(x_max)
            y_min_str, y_max_str = ff(y_min), ff(y_max)

            lines.append('  (polygon {} (layer top_documentation)'.format(lead_uuid))
            lines.append('   (width 0.0) (fill true) (grab_area false)')
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_min_str, y_max_str))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_max_str, y_max_str))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_max_str, y_min_str))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_min_str, y_min_str))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_min_str, y_max_str))
            lines.append('  )')

        # Create exposed pad on docu
        if make_exposed:
            uuid_docu_exposed = _uuid('lead-exposed')

            x_min, x_max = - config.exposed_length / 2, config.exposed_length / 2
            y_min, y_max = - config.exposed_width / 2, config.exposed_width / 2

            lines.append('  (polygon {} (layer top_documentation)'.format(uuid_docu_exposed))
            lines.append('   (width 0.0) (fill true) (grab_area false)')
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_min, y_max))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_max, y_max))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_max, y_min))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_min, y_min))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(x_min, y_max))
            lines.append('  )')

        # Create body outline on docu
        uuid_body_outline = _uuid('body-outline')
        outline_line_width = 0.2
        dx = config.width / 2 - outline_line_width / 2
        dy = config.length / 2 - outline_line_width / 2
        lines.append('  (polygon {} (layer top_documentation)'.format(uuid_body_outline))
        lines.append('   (width {}) (fill false) (grab_area false)'.format(outline_line_width))
        lines.append('   (vertex (position {} {}) (angle 0.0))'.format(-dx, dy))
        lines.append('   (vertex (position {} {}) (angle 0.0))'.format(dx, dy))
        lines.append('   (vertex (position {} {}) (angle 0.0))'.format(dx, -dy))
        lines.append('   (vertex (position {} {}) (angle 0.0))'.format(-dx, -dy))
        lines.append('   (vertex (position {} {}) (angle 0.0))'.format(-dx, dy))
        lines.append('  )')

        if config.extended_doc_fn:
            config.extended_doc_fn(config, _uuid, lines)

        # As discussed in https://github.com/LibrePCB-Libraries/LibrePCB_Base.lplib/pull/16
        # the silkscreen circle should have size SILKSCREEN_LINE_WIDTH for small packages,
        # and twice the size for larger packages. We define small to be either W or L <3.0mm
        # and large if both W and L >= 3.0mm
        if config.width >= 3.0 and config.length >= 3.0:
            silkscreen_circ_dia = 2.0 * SILKSCREEN_LINE_WIDTH
        else:
            silkscreen_circ_dia = SILKSCREEN_LINE_WIDTH

        if silkscreen_circ_dia == SILKSCREEN_LINE_WIDTH:
            silk_circ_y = config.length / 2 + silkscreen_circ_dia
            silk_circ_x = -config.width / 2 - SILKSCREEN_LINE_WIDTH
        else:
            silk_circ_y = config.length / 2 + SILKSCREEN_LINE_WIDTH / 2
            silk_circ_x = -config.width / 2 - silkscreen_circ_dia

        # Move silkscreen circle upwards if the line is moved too
        if silk_down < 0:
            silk_circ_y = silk_circ_y - silk_down

        uuid_silkscreen_circ = _uuid('circle-silkscreen-{}'.format(key))
        lines.append('  (circle {} (layer top_placement)'.format(uuid_silkscreen_circ))
        lines.append('   (width 0.0) (fill true) (grab_area false) '
                     '(diameter {}) (position {} {})'.format(
                         ff(silkscreen_circ_dia),
                         ff(silk_circ_x),
                         ff(silk_circ_y)
                     ))
        lines.append('  )')

        # Add name and value labels
        uuid_text_name = _uuid('text-name-{}'.format(key))
        uuid_text_value = _uuid('text-value-{}'.format(key))

        lines.append('  (stroke_text {} (layer top_names)'.format(uuid_text_name))
        lines.append('   {}'.format(TEXT_ATTRS))
        lines.append('   (align center bottom) (position 0.0 {}) (rotation 0.0)'.format(
            config.length / 2 + LABEL_OFFSET))
        lines.append('   (auto_rotate true) (mirror false) (value "{{NAME}}")')
        lines.append('  )')
        lines.append('  (stroke_text {} (layer top_values)'.format(uuid_text_value))
        lines.append('   {}'.format(TEXT_ATTRS))
        lines.append('   (align center top) (position 0.0 {}) (rotation 0.0)'.format(
            -config.length / 2 - LABEL_OFFSET))
        lines.append('   (auto_rotate true) (mirror false) (value "{{VALUE}}")')
        lines.append('  )')

        # Closing parenthese for footprint
        lines.append(' )')

    # Apply function to available footprints
    _generate_footprint('reflow', 'reflow', 0.0)
    _generate_footprint('hand-soldering', 'hand soldering', 0.3)

    # Final closing parenthese
    lines.append(')')

    # Save package
    pkg_dir_path = path.join(dirpath, uuid_pkg)
    if not (path.exists(pkg_dir_path) and path.isdir(pkg_dir_path)):
        makedirs(pkg_dir_path)
    with open(path.join(pkg_dir_path, '.librepcb-pkg'), 'w') as f:
        f.write('0.1\n')
    with open(path.join(pkg_dir_path, 'package.lp'), 'w') as f:
        f.write('\n'.join(lines))
        f.write('\n')

    return full_name
def generate_pkg(
    dirpath: str,
    author: str,
    name: str,
    description: str,
    configs: Iterable[SoConfig],
    lead_width_lookup: Dict[float, float],
    lead_contact_length: float,
    pkgcat: str,
    keywords: str,
    version: str,
    create_date: Optional[str],
) -> None:
    category = 'pkg'
    for config in configs:
        pitch = config.pitch
        pin_count = config.pin_count
        height = config.height
        body_width = config.body_width
        total_width = config.total_width
        body_length = config.body_length
        lead_width = lead_width_lookup[pitch]
        lead_length = (total_width - body_width) / 2

        lines = []

        full_name = name.format(
            height=fd(height),
            pitch=fd(pitch),
            pin_count=pin_count,
            body_length=fd(body_length),
            lead_span=fd(total_width),
            lead_width=fd(lead_width),
            lead_length=fd(lead_length),
        )
        full_description = description.format(
            height=height,
            pin_count=pin_count,
            pitch=pitch,
            body_length=body_length,
            body_width=body_width,
            lead_span=total_width,
            lead_width=lead_width,
            lead_length=lead_length,
            variation=config.variation,
        )

        def _uuid(identifier: str) -> str:
            return uuid(category, full_name, identifier)

        uuid_pkg = _uuid('pkg')
        uuid_pads = [
            _uuid('pad-{}'.format(p)) for p in range(1, pin_count + 1)
        ]
        uuid_leads1 = [
            _uuid('lead-contact-{}'.format(p))
            for p in range(1, pin_count + 1)
        ]
        uuid_leads2 = [
            _uuid('lead-proj-{}'.format(p)) for p in range(1, pin_count + 1)
        ]

        print('Generating {}: {}'.format(full_name, uuid_pkg))

        # General info
        lines.append('(librepcb_package {}'.format(uuid_pkg))
        lines.append(' (name "{}")'.format(full_name))
        lines.append(' (description "{}\\n\\nGenerated with {}")'.format(
            full_description, generator))
        lines.append(' (keywords "soic{},so{},{}")'.format(
            pin_count, pin_count, keywords))
        lines.append(' (author "{}")'.format(author))
        lines.append(' (version "{}")'.format(version))
        lines.append(' (created {})'.format(create_date or now()))
        lines.append(' (deprecated false)')
        lines.append(' (category {})'.format(pkgcat))
        for p in range(1, pin_count + 1):
            lines.append(' (pad {} (name "{}"))'.format(uuid_pads[p - 1], p))

        def add_footprint_variant(
            key: str,
            name: str,
            density_level: str,
        ) -> None:
            uuid_footprint = _uuid('footprint-{}'.format(key))
            uuid_silkscreen_top = _uuid('polygon-silkscreen-{}'.format(key))
            uuid_silkscreen_bot = _uuid('polygon-silkscreen2-{}'.format(key))
            uuid_outline = _uuid('polygon-outline-{}'.format(key))
            uuid_courtyard = _uuid('polygon-courtyard-{}'.format(key))
            uuid_text_name = _uuid('text-name-{}'.format(key))
            uuid_text_value = _uuid('text-value-{}'.format(key))

            # Max boundaries (pads or body)
            max_x = 0.0
            max_y = 0.0

            # Max boundaries (copper only)
            max_y_copper = 0.0

            lines.append(' (footprint {}'.format(uuid_footprint))
            lines.append('  (name "{}")'.format(name))
            lines.append('  (description "")')

            # Pad excess according to IPC density levels
            pad_heel = get_by_density(pitch, density_level, 'heel')
            pad_toe = get_by_density(pitch, density_level, 'toe')
            pad_side = get_by_density(pitch, density_level, 'side')

            # Pads
            pad_width = lead_width + pad_side
            pad_length = lead_contact_length + pad_heel + pad_toe
            pad_x_offset = total_width / 2 - lead_contact_length / 2 - pad_heel / 2 + pad_toe / 2
            for p in range(1, pin_count + 1):
                mid = pin_count // 2
                if p <= mid:
                    y = get_y(p, pin_count // 2, pitch, False)
                    pxo = ff(-pad_x_offset)
                else:
                    y = -get_y(p - mid, pin_count // 2, pitch, False)
                    pxo = ff(pad_x_offset)
                pad_uuid = uuid_pads[p - 1]
                lines.append(
                    '  (pad {} (side top) (shape rect)'.format(pad_uuid))
                lines.append(
                    '   (position {} {}) (rotation 0.0) (size {} {}) (drill 0.0)'
                    .format(
                        pxo,
                        ff(y),
                        ff(pad_length),
                        ff(pad_width),
                    ))
                lines.append('  )')
                max_y_copper = max(max_y_copper, y + pad_width / 2)
            max_x = max(max_x, total_width / 2 + pad_toe)

            # Documentation: Leads
            lead_contact_x_offset = total_width / 2 - lead_contact_length  # this is the inner side of the contact area
            for p in range(1, pin_count + 1):
                mid = pin_count // 2
                if p <= mid:  # left side
                    y = get_y(p, pin_count // 2, pitch, False)
                    lcxo_max = ff(-lead_contact_x_offset - lead_contact_length)
                    lcxo_min = ff(-lead_contact_x_offset)
                    body_side = ff(-body_width / 2)
                else:  # right side
                    y = -get_y(p - mid, pin_count // 2, pitch, False)
                    lcxo_min = ff(lead_contact_x_offset)
                    lcxo_max = ff(lead_contact_x_offset + lead_contact_length)
                    body_side = ff(body_width / 2)
                y_max = ff(y - lead_width / 2)
                y_min = ff(y + lead_width / 2)
                lead_uuid_ctct = uuid_leads1[p - 1]  # Contact area
                lead_uuid_proj = uuid_leads2[p - 1]  # Vertical projection
                # Contact area
                lines.append('  (polygon {} (layer top_documentation)'.format(
                    lead_uuid_ctct))
                lines.append('   (width 0.0) (fill true) (grab_area false)')
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    lcxo_min, y_max))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    lcxo_max, y_max))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    lcxo_max, y_min))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    lcxo_min, y_min))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    lcxo_min, y_max))
                lines.append('  )')
                # Vertical projection, between contact area and body
                lines.append('  (polygon {} (layer top_documentation)'.format(
                    lead_uuid_proj))
                lines.append('   (width 0.0) (fill true) (grab_area false)')
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    body_side, y_max))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    lcxo_min, y_max))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    lcxo_min, y_min))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    body_side, y_min))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    body_side, y_max))
                lines.append('  )')

            # Silkscreen (fully outside body)
            # Ensure minimum clearance between copper and silkscreen
            y_offset = max(
                silkscreen_offset - (body_length / 2 - max_y_copper), 0)
            y_max = ff(body_length / 2 + line_width / 2 + y_offset)
            y_min = ff(-body_length / 2 - line_width / 2 - y_offset)
            short_x_offset = body_width / 2 - line_width / 2
            long_x_offset = total_width / 2 - line_width / 2 + pad_toe  # Pin1 marking
            lines.append('  (polygon {} (layer top_placement)'.format(
                uuid_silkscreen_top))
            lines.append('   (width {}) (fill false) (grab_area false)'.format(
                line_width))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                ff(-long_x_offset), y_max))  # noqa
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                ff(short_x_offset), y_max))  # noqa
            lines.append('  )')
            lines.append('  (polygon {} (layer top_placement)'.format(
                uuid_silkscreen_bot))
            lines.append('   (width {}) (fill false) (grab_area false)'.format(
                line_width))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                ff(-short_x_offset), y_min))  # noqa
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                ff(short_x_offset), y_min))  # noqa
            lines.append('  )')

            # Documentation outline (fully inside body)
            outline_x_offset = body_width / 2 - line_width / 2
            lines.append(
                '  (polygon {} (layer top_documentation)'.format(uuid_outline))
            lines.append('   (width {}) (fill false) (grab_area true)'.format(
                line_width))
            y_max = ff(body_length / 2 - line_width / 2)
            y_min = ff(-body_length / 2 + line_width / 2)
            oxo = ff(outline_x_offset)  # Used for shorter code lines below :)
            lines.append('   (vertex (position -{} {}) (angle 0.0))'.format(
                oxo, y_max))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                oxo, y_max))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                oxo, y_min))
            lines.append('   (vertex (position -{} {}) (angle 0.0))'.format(
                oxo, y_min))
            lines.append('   (vertex (position -{} {}) (angle 0.0))'.format(
                oxo, y_max))
            lines.append('  )')
            max_y = max(max_y, body_length / 2)  # Body contour

            # Courtyard
            courtyard_excess = get_by_density(pitch, density_level,
                                              'courtyard')
            lines.extend(
                indent(
                    2,
                    generate_courtyard(
                        uuid=uuid_courtyard,
                        max_x=max_x,
                        max_y=max_y,
                        excess_x=courtyard_excess,
                        excess_y=courtyard_excess,
                    )))

            # Labels
            y_max = ff(body_length / 2 + 1.27)
            y_min = ff(-body_length / 2 - 1.27)
            text_attrs = '(height {}) (stroke_width 0.2) ' \
                         '(letter_spacing auto) (line_spacing auto)'.format(pkg_text_height)
            lines.append(
                '  (stroke_text {} (layer top_names)'.format(uuid_text_name))
            lines.append('   {}'.format(text_attrs))
            lines.append(
                '   (align center bottom) (position 0.0 {}) (rotation 0.0)'.
                format(y_max))
            lines.append(
                '   (auto_rotate true) (mirror false) (value "{{NAME}}")')
            lines.append('  )')
            lines.append(
                '  (stroke_text {} (layer top_values)'.format(uuid_text_value))
            lines.append('   {}'.format(text_attrs))
            lines.append(
                '   (align center top) (position 0.0 {}) (rotation 0.0)'.
                format(y_min))
            lines.append(
                '   (auto_rotate true) (mirror false) (value "{{VALUE}}")')
            lines.append('  )')

            lines.append(' )')

        add_footprint_variant('density~b',
                              'Density Level B (median protrusion)', 'B')
        add_footprint_variant('density~a', 'Density Level A (max protrusion)',
                              'A')
        add_footprint_variant('density~c', 'Density Level C (min protrusion)',
                              'C')

        lines.append(')')

        pkg_dir_path = path.join(dirpath, uuid_pkg)
        if not (path.exists(pkg_dir_path) and path.isdir(pkg_dir_path)):
            makedirs(pkg_dir_path)
        with open(path.join(pkg_dir_path, '.librepcb-pkg'), 'w') as f:
            f.write('0.1\n')
        with open(path.join(pkg_dir_path, 'package.lp'), 'w') as f:
            f.write('\n'.join(lines))
            f.write('\n')
def generate_pkg(
    dirpath: str,
    author: str,
    name: str,
    description: str,
    configs: Iterable[ChipConfig],
    pkgcat: str,
    keywords: str,
    create_date: Optional[str],
):
    category = 'pkg'
    for config in configs:
        lines = []

        fmt_params = {
            'size_metric': config.size_metric(),
            'size_imperial': config.size_imperial(),
        }  # type: Dict[str, Any]
        fmt_params_name = {
            **fmt_params,
            'height': fd(config.height),
        }
        fmt_params_desc = {
            **fmt_params,
            'length': config.length,
            'width': config.width,
            'height': config.height,
        }
        full_name = name.format(**fmt_params_name)
        full_desc = description.format(**fmt_params_desc)

        def _uuid(identifier):
            return uuid(category, full_name, identifier)

        # UUIDs
        uuid_pkg = _uuid('pkg')
        uuid_pads = [_uuid('pad-1'), _uuid('pad-2')]

        print('Generating {}: {}'.format(full_name, uuid_pkg))

        # General info
        lines.append('(librepcb_package {}'.format(uuid_pkg))
        lines.append(' (name "{}")'.format(full_name))
        lines.append(' (description "{}\\n\\nGenerated with {}")'.format(
            full_desc, generator))
        lines.append(' (keywords "{},{},{}")'.format(
            config.size_metric(),
            config.size_imperial(),
            keywords,
        ))
        lines.append(' (author "{}")'.format(author))
        lines.append(' (version "0.3")')
        lines.append(' (created {})'.format(create_date or now()))
        lines.append(' (deprecated false)')
        lines.append(' (category {})'.format(pkgcat))
        lines.append(' (pad {} (name "1"))'.format(uuid_pads[0]))
        lines.append(' (pad {} (name "2"))'.format(uuid_pads[1]))

        def add_footprint_variant(key: str, name: str, density_level: str,
                                  toe_extension: float):
            uuid_footprint = _uuid('footprint-{}'.format(key))
            uuid_text_name = _uuid('text-name-{}'.format(key))
            uuid_text_value = _uuid('text-value-{}'.format(key))
            uuid_silkscreen_top = _uuid('line-silkscreen-top-{}'.format(key))
            uuid_silkscreen_bot = _uuid('line-silkscreen-bot-{}'.format(key))
            uuid_courtyard = _uuid('polygon-courtyard-{}'.format(key))
            uuid_outline_top = _uuid('polygon-outline-top-{}'.format(key))
            uuid_outline_bot = _uuid('polygon-outline-bot-{}'.format(key))
            uuid_outline_left = _uuid('polygon-outline-left-{}'.format(key))
            uuid_outline_right = _uuid('polygon-outline-right-{}'.format(key))

            # Max boundary
            max_x = 0.0
            max_y = 0.0

            # Line width adjusted for size of element
            if config.length >= 2.0:
                silk_lw = line_width
                doc_lw = line_width
            elif config.length >= 1.0:
                silk_lw = line_width_thin
                doc_lw = line_width_thin
            else:
                silk_lw = line_width_thin
                doc_lw = line_width_thinner

            lines.append(' (footprint {}'.format(uuid_footprint))
            lines.append('  (name "{}")'.format(name))
            lines.append('  (description "")')

            # Pads
            for p in [0, 1]:
                pad_uuid = uuid_pads[p - 1]
                sign = -1 if p == 1 else 1
                # Note: We are using the gap from the actual resistors (Samsung), but calculate
                # the protrusion (toe and side) based on IPC7351.
                pad_width = config.width + get_by_density(
                    config.length, density_level, 'side')
                pad_toe = get_by_density(config.length, density_level,
                                         'toe') + toe_extension
                pad_length = (config.length - config.gap) / 2 + pad_toe
                dx = sign * (config.gap / 2 + pad_length / 2
                             )  # x offset (delta-x)
                lines.append(
                    '  (pad {} (side top) (shape rect)'.format(pad_uuid))
                lines.append(
                    '   (position {} 0.0) (rotation 0.0) (size {} {}) (drill 0.0)'
                    .format(
                        ff(dx),
                        ff(pad_length),
                        ff(pad_width),
                    ))
                max_x = max(max_x, pad_length / 2 + dx)
                lines.append('  )')

            # Documentation
            half_gap = ff(config.gap / 2)
            dx = ff(config.length / 2)
            dy = ff(config.width / 2)
            lines.append('  (polygon {} (layer {})'.format(
                uuid_outline_left, 'top_documentation'))
            lines.append('   (width 0.0) (fill true) (grab_area true)')
            lines.append('   (vertex (position -{} {}) (angle 0.0))'.format(
                dx, dy))  # NW
            lines.append('   (vertex (position -{} {}) (angle 0.0))'.format(
                half_gap, dy))  # NE
            lines.append('   (vertex (position -{} -{}) (angle 0.0))'.format(
                half_gap, dy))  # SE
            lines.append('   (vertex (position -{} -{}) (angle 0.0))'.format(
                dx, dy))  # SW
            lines.append('   (vertex (position -{} {}) (angle 0.0))'.format(
                dx, dy))  # NW
            lines.append('  )')
            lines.append('  (polygon {} (layer {})'.format(
                uuid_outline_right, 'top_documentation'))
            lines.append('   (width 0.0) (fill true) (grab_area true)')
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                dx, dy))  # NE
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                half_gap, dy))  # NW
            lines.append('   (vertex (position {} -{}) (angle 0.0))'.format(
                half_gap, dy))  # SW
            lines.append('   (vertex (position {} -{}) (angle 0.0))'.format(
                dx, dy))  # SE
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                dx, dy))  # NE
            lines.append('  )')
            dy = ff(config.width / 2 - doc_lw / 2)
            lines.append('  (polygon {} (layer {})'.format(
                uuid_outline_top, 'top_documentation'))
            lines.append(
                '   (width {}) (fill false) (grab_area true)'.format(doc_lw))
            lines.append('   (vertex (position -{} {}) (angle 0.0))'.format(
                half_gap, dy))
            lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                half_gap, dy))
            lines.append('  )')
            lines.append('  (polygon {} (layer {})'.format(
                uuid_outline_bot, 'top_documentation'))
            lines.append(
                '   (width {}) (fill false) (grab_area true)'.format(doc_lw))
            lines.append('   (vertex (position -{} -{}) (angle 0.0))'.format(
                half_gap, dy))
            lines.append('   (vertex (position {} -{}) (angle 0.0))'.format(
                half_gap, dy))
            lines.append('  )')
            max_y = max(max_y, config.width / 2)

            # Silkscreen
            if config.length > 1.0:
                dx = ff(config.gap / 2 - silk_lw / 2 - silkscreen_clearance)
                dy = ff(config.width / 2 + silk_lw / 2)
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_silkscreen_top, 'top_placement'))
                lines.append(
                    '   (width {}) (fill false) (grab_area false)'.format(
                        silk_lw))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(dx, dy))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    dx, dy))
                lines.append('  )')
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_silkscreen_bot, 'top_placement'))
                lines.append(
                    '   (width {}) (fill false) (grab_area false)'.format(
                        silk_lw))
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        dx, dy))
                lines.append(
                    '   (vertex (position {} -{}) (angle 0.0))'.format(dx, dy))
                lines.append('  )')
                max_y = max(max_y, config.width / 2 + silk_lw)

            # Courtyard
            courtyard_excess = get_by_density(config.length, density_level,
                                              'courtyard')
            lines.extend(
                indent(
                    2,
                    generate_courtyard(
                        uuid=uuid_courtyard,
                        max_x=max_x,
                        max_y=max_y,
                        excess_x=courtyard_excess,
                        excess_y=courtyard_excess,
                    )))

            # Labels
            if config.width < 2.0:
                offset = label_offset_thin
            else:
                offset = label_offset
            dy = ff(config.width / 2 + offset)  # y offset (delta-y)
            text_attrs = '(height {}) (stroke_width 0.2) ' \
                         '(letter_spacing auto) (line_spacing auto)'.format(pkg_text_height)
            lines.append(
                '  (stroke_text {} (layer top_names)'.format(uuid_text_name))
            lines.append('   {}'.format(text_attrs))
            lines.append(
                '   (align center bottom) (position 0.0 {}) (rotation 0.0)'.
                format(dy))
            lines.append(
                '   (auto_rotate true) (mirror false) (value "{{NAME}}")')
            lines.append('  )')
            lines.append(
                '  (stroke_text {} (layer top_values)'.format(uuid_text_value))
            lines.append('   {}'.format(text_attrs))
            lines.append(
                '   (align center top) (position 0.0 -{}) (rotation 0.0)'.
                format(dy))
            lines.append(
                '   (auto_rotate true) (mirror false) (value "{{VALUE}}")')
            lines.append('  )')

            lines.append(' )')

        add_footprint_variant('density~b',
                              'Density Level B (median protrusion)', 'B', 0.0)
        add_footprint_variant('density~a', 'Density Level A (max protrusion)',
                              'A', 0.0)
        # add_footprint_variant('density~hs', 'Hand Soldering', 'A', handsoldering_toe_extension)

        lines.append(')')

        pkg_dir_path = path.join(dirpath, uuid_pkg)
        if not (path.exists(pkg_dir_path) and path.isdir(pkg_dir_path)):
            makedirs(pkg_dir_path)
        with open(path.join(pkg_dir_path, '.librepcb-pkg'), 'w') as f:
            f.write('0.1\n')
        with open(path.join(pkg_dir_path, 'package.lp'), 'w') as f:
            f.write('\n'.join(lines))
            f.write('\n')
def generate_pkg(dirpath: str, author: str, name: str, description: str,
                 polarization: Optional[PolarizationConfig],
                 configs: Iterable[ChipConfig], pkgcat: str, keywords: str,
                 version: str, create_date: Optional[str]) -> None:
    category = 'pkg'
    for config in configs:
        lines = []

        fmt_params = {
            'size_metric': config.size_metric(),
            'size_imperial': config.size_imperial(),
        }  # type: Dict[str, Any]
        fmt_params_name = {
            **fmt_params,
            'length':
            fd(config.body.length),
            'width':
            fd(config.body.width),
            'height':
            fd(config.body.height),
            'lead_length':
            fd(config.body.lead_length) if config.body.lead_length else None,
            'lead_width':
            fd(config.body.lead_width) if config.body.lead_width else None,
        }
        fmt_params_desc = {
            **fmt_params,
            'length': config.body.length,
            'width': config.body.width,
            'height': config.body.height,
            'meta': config.meta,
        }
        full_name = name.format(**fmt_params_name)
        full_desc = description.format(**fmt_params_desc)
        full_keywords = keywords.format(**fmt_params_desc).lower()

        def _uuid(identifier: str) -> str:
            return uuid(category, full_name, identifier)

        # UUIDs
        uuid_pkg = _uuid('pkg')
        if polarization:
            uuid_pads = [
                _uuid('pad-{}'.format(polarization.id_marked)),
                _uuid('pad-{}'.format(polarization.id_unmarked)),
            ]
        else:
            uuid_pads = [_uuid('pad-1'), _uuid('pad-2')]

        print('Generating pkg "{}": {}'.format(full_name, uuid_pkg))

        # General info
        lines.append('(librepcb_package {}'.format(uuid_pkg))
        lines.append(' (name "{}")'.format(full_name))
        lines.append(' (description "{}\\n\\nGenerated with {}")'.format(
            full_desc, generator))
        lines.append(' (keywords "{}")'.format(','.join(
            filter(None, [
                config.size_metric(),
                config.size_imperial(),
                full_keywords,
            ]))))
        lines.append(' (author "{}")'.format(author))
        lines.append(' (version "{}")'.format(version))
        lines.append(' (created {})'.format(create_date or now()))
        lines.append(' (deprecated false)')
        lines.append(' (category {})'.format(pkgcat))
        if polarization:
            lines.append(' (pad {} (name "{}"))'.format(
                uuid_pads[0], polarization.name_marked))
            lines.append(' (pad {} (name "{}"))'.format(
                uuid_pads[1], polarization.name_unmarked))
        else:
            lines.append(' (pad {} (name "1"))'.format(uuid_pads[0]))
            lines.append(' (pad {} (name "2"))'.format(uuid_pads[1]))

        def add_footprint_variant(
                key: str,
                name: str,
                density_level: str,
                *,
                gap: Optional[float] = None,
                footprint: Optional[FootprintDimensions] = None) -> None:
            """
            Generate a footprint variant.

            Note: Either the toe extension or footprint dimensions must be set.
            """
            if gap is not None and footprint is not None:
                raise ValueError('Only toe extension or footprint may be set')
            if gap is None and footprint is None:
                raise ValueError(
                    'Either toe extension or footprint must be set')
            uuid_footprint = _uuid('footprint-{}'.format(key))
            uuid_text_name = _uuid('text-name-{}'.format(key))
            uuid_text_value = _uuid('text-value-{}'.format(key))
            uuid_silkscreen_top = _uuid('line-silkscreen-top-{}'.format(key))
            uuid_silkscreen_bot = _uuid('line-silkscreen-bot-{}'.format(key))
            uuid_courtyard = _uuid('polygon-courtyard-{}'.format(key))
            uuid_outline_top = _uuid('polygon-outline-top-{}'.format(key))
            uuid_outline_bot = _uuid('polygon-outline-bot-{}'.format(key))
            uuid_outline_left = _uuid('polygon-outline-left-{}'.format(key))
            uuid_outline_right = _uuid('polygon-outline-right-{}'.format(key))
            uuid_outline_around = _uuid(
                'polygon-outline-around-{}'.format(key))
            uuid_polarization_mark = _uuid(
                'polygon-polarization-mark-{}'.format(key))

            # Max boundary
            max_x = 0.0
            max_y = 0.0

            # Line width adjusted for size of element
            if config.body.length >= 2.0:
                silk_lw = line_width
                doc_lw = line_width
            elif config.body.length >= 1.0:
                silk_lw = line_width_thin
                doc_lw = line_width_thin
            else:
                silk_lw = line_width_thin
                doc_lw = line_width_thinner

            lines.append(' (footprint {}'.format(uuid_footprint))
            lines.append('  (name "{}")'.format(name))
            lines.append('  (description "")')

            # Pads
            if footprint is not None:
                pad_width = footprint.pad_width
                pad_length = footprint.pad_length
                pad_gap = footprint.pad_gap
                pad_dx = (pad_gap / 2 + pad_length / 2)  # x offset (delta-x)
            elif gap is not None:
                pad_gap = gap
                pad_width = config.body.width + get_by_density(
                    config.body.length, density_level, 'side')
                pad_toe = get_by_density(config.body.length, density_level,
                                         'toe')
                pad_length = (config.body.length - gap) / 2 + pad_toe
                pad_dx = (gap / 2 + pad_length / 2)  # x offset (delta-x)
            else:
                raise ValueError('Either footprint or gap must be set')
            for p in [0, 1]:
                pad_uuid = uuid_pads[p - 1]
                sign = -1 if p == 1 else 1
                lines.append(
                    '  (pad {} (side top) (shape rect)'.format(pad_uuid))
                lines.append(
                    '   (position {} 0.0) (rotation 0.0) (size {} {}) (drill 0.0)'
                    .format(
                        ff(sign * pad_dx),
                        ff(pad_length),
                        ff(pad_width),
                    ))
                max_x = max(max_x, pad_length / 2 + sign * pad_dx)
                lines.append('  )')
            max_y = max(max_y, config.body.width / 2)
            max_y = max(max_y, pad_width / 2)

            # Documentation
            half_gap_raw = (config.body.gap or pad_gap) / 2
            half_gap = ff(half_gap_raw)
            if footprint is None:
                # We assume that leads are across the entire width of the part (e.g. MLCC)
                dx = ff(config.body.length / 2)
                dy = ff(config.body.width / 2)
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_outline_left, 'top_documentation'))
                lines.append('   (width 0.0) (fill true) (grab_area false)')
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        dx, dy))  # NW
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        half_gap, dy))  # NE
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        half_gap, dy))  # SE
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        dx, dy))  # SW
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        dx, dy))  # NW
                lines.append('  )')
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_outline_right, 'top_documentation'))
                lines.append('   (width 0.0) (fill true) (grab_area false)')
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    dx, dy))  # NE
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    half_gap, dy))  # NW
                lines.append(
                    '   (vertex (position {} -{}) (angle 0.0))'.format(
                        half_gap, dy))  # SW
                lines.append(
                    '   (vertex (position {} -{}) (angle 0.0))'.format(
                        dx, dy))  # SE
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    dx, dy))  # NE
                lines.append('  )')
                dy = ff(config.body.width / 2 - doc_lw / 2)
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_outline_top, 'top_documentation'))
                lines.append(
                    '   (width {}) (fill false) (grab_area false)'.format(
                        doc_lw))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        half_gap, dy))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    half_gap, dy))
                lines.append('  )')
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_outline_bot, 'top_documentation'))
                lines.append(
                    '   (width {}) (fill false) (grab_area false)'.format(
                        doc_lw))
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        half_gap, dy))
                lines.append(
                    '   (vertex (position {} -{}) (angle 0.0))'.format(
                        half_gap, dy))
                lines.append('  )')
            else:
                # We have more precise information about the lead (e.g. molded
                # packages where leads are not the full width of the package).
                dx = ff(config.body.length / 2 - doc_lw / 2)
                dy = ff(config.body.width / 2 - doc_lw / 2)
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_outline_around, 'top_documentation'))
                lines.append(
                    '   (width {}) (fill false) (grab_area false)'.format(
                        doc_lw))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(dx, dy))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    dx, dy))
                lines.append(
                    '   (vertex (position {} -{}) (angle 0.0))'.format(dx, dy))
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        dx, dy))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(dx, dy))
                lines.append('  )')
                dx = ff(config.body.length / 2)
                dy = ff((config.body.lead_width or footprint.pad_width) / 2)
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_outline_left, 'top_documentation'))
                lines.append('   (width 0.0) (fill true) (grab_area false)')
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(dx, dy))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        half_gap, dy))
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        half_gap, dy))
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        dx, dy))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(dx, dy))
                lines.append('  )')
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_outline_right, 'top_documentation'))
                lines.append('   (width 0.0) (fill true) (grab_area false)')
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    dx, dy))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    half_gap, dy))
                lines.append(
                    '   (vertex (position {} -{}) (angle 0.0))'.format(
                        half_gap, dy))
                lines.append(
                    '   (vertex (position {} -{}) (angle 0.0))'.format(dx, dy))
                lines.append('   (vertex (position {} {}) (angle 0.0))'.format(
                    dx, dy))
                lines.append('  )')
            if polarization:
                polarization_mark_width = config.body.width / 8
                dx_outer = ff(half_gap_raw - polarization_mark_width / 2)
                dx_inner = ff(half_gap_raw - polarization_mark_width * 1.5)
                dy = ff(config.body.width / 2 - doc_lw)
                lines.append('  (polygon {} (layer {})'.format(
                    uuid_polarization_mark, 'top_documentation'))
                lines.append('   (width 0.0) (fill true) (grab_area true)')
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        dx_outer, dy))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        dx_inner, dy))
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        dx_inner, dy))
                lines.append(
                    '   (vertex (position -{} -{}) (angle 0.0))'.format(
                        dx_outer, dy))
                lines.append(
                    '   (vertex (position -{} {}) (angle 0.0))'.format(
                        dx_outer, dy))
                lines.append('  )')

            # Silkscreen
            if config.body.length > 1.0:
                if polarization:
                    dx_unmarked = pad_dx + pad_length / 2
                    dx_marked = dx_unmarked + silk_lw / 2 + silkscreen_clearance
                    dy = ff(
                        max(
                            config.body.width / 2 +
                            silk_lw / 2,  # Based on body width
                            pad_width / 2 + silk_lw / 2 +
                            silkscreen_clearance,  # Based on pad width
                        ))
                    lines.append('  (polygon {} (layer {})'.format(
                        uuid_silkscreen_top, 'top_placement'))
                    lines.append(
                        '   (width {}) (fill false) (grab_area false)'.format(
                            silk_lw))
                    lines.append(
                        '   (vertex (position {} {}) (angle 0.0))'.format(
                            ff(dx_unmarked), dy))
                    lines.append(
                        '   (vertex (position -{} {}) (angle 0.0))'.format(
                            ff(dx_marked), dy))
                    lines.append(
                        '   (vertex (position -{} -{}) (angle 0.0))'.format(
                            ff(dx_marked), dy))
                    lines.append(
                        '   (vertex (position {} -{}) (angle 0.0))'.format(
                            ff(dx_unmarked), dy))
                    lines.append('  )')
                else:
                    assert gap is not None, \
                        "Support for non-polarized packages with irregular pads not yet fully implemented"
                    dx = ff(gap / 2 - silk_lw / 2 - silkscreen_clearance)
                    dy = ff(config.body.width / 2 + silk_lw / 2)
                    lines.append('  (polygon {} (layer {})'.format(
                        uuid_silkscreen_top, 'top_placement'))
                    lines.append(
                        '   (width {}) (fill false) (grab_area false)'.format(
                            silk_lw))
                    lines.append(
                        '   (vertex (position -{} {}) (angle 0.0))'.format(
                            dx, dy))
                    lines.append(
                        '   (vertex (position {} {}) (angle 0.0))'.format(
                            dx, dy))
                    lines.append('  )')
                    lines.append('  (polygon {} (layer {})'.format(
                        uuid_silkscreen_bot, 'top_placement'))
                    lines.append(
                        '   (width {}) (fill false) (grab_area false)'.format(
                            silk_lw))
                    lines.append(
                        '   (vertex (position -{} -{}) (angle 0.0))'.format(
                            dx, dy))
                    lines.append(
                        '   (vertex (position {} -{}) (angle 0.0))'.format(
                            dx, dy))
                    lines.append('  )')

            # Courtyard
            courtyard_excess = get_by_density(config.body.length,
                                              density_level, 'courtyard')
            lines.extend(
                indent(
                    2,
                    generate_courtyard(
                        uuid=uuid_courtyard,
                        max_x=max_x,
                        max_y=max_y,
                        excess_x=courtyard_excess,
                        excess_y=courtyard_excess,
                    )))

            # Labels
            if config.body.width < 2.0:
                offset = label_offset_thin
            else:
                offset = label_offset
            dy = ff(config.body.width / 2 + offset)  # y offset (delta-y)
            text_attrs = '(height {}) (stroke_width 0.2) ' \
                         '(letter_spacing auto) (line_spacing auto)'.format(pkg_text_height)
            lines.append(
                '  (stroke_text {} (layer top_names)'.format(uuid_text_name))
            lines.append('   {}'.format(text_attrs))
            lines.append(
                '   (align center bottom) (position 0.0 {}) (rotation 0.0)'.
                format(dy))
            lines.append(
                '   (auto_rotate true) (mirror false) (value "{{NAME}}")')
            lines.append('  )')
            lines.append(
                '  (stroke_text {} (layer top_values)'.format(uuid_text_value))
            lines.append('   {}'.format(text_attrs))
            lines.append(
                '   (align center top) (position 0.0 -{}) (rotation 0.0)'.
                format(dy))
            lines.append(
                '   (auto_rotate true) (mirror false) (value "{{VALUE}}")')
            lines.append('  )')

            lines.append(' )')

        if config.gap:
            add_footprint_variant('density~b',
                                  'Density Level B (median protrusion)',
                                  'B',
                                  gap=config.gap)
            add_footprint_variant('density~a',
                                  'Density Level A (max protrusion)',
                                  'A',
                                  gap=config.gap)
        elif config.footprints:
            a = config.footprints.get('A')
            b = config.footprints.get('B')
            c = config.footprints.get('C')
            if b:
                add_footprint_variant('density~b',
                                      'Density Level B (median protrusion)',
                                      'B',
                                      footprint=b)
            if a:
                add_footprint_variant('density~a',
                                      'Density Level A (max protrusion)',
                                      'A',
                                      footprint=a)
            if c:
                add_footprint_variant('density~c',
                                      'Density Level C (min protrusion)',
                                      'C',
                                      footprint=c)
        else:
            raise ValueError('Either gap or footprints must be set')

        lines.append(')')

        pkg_dir_path = path.join(dirpath, uuid_pkg)
        if not (path.exists(pkg_dir_path) and path.isdir(pkg_dir_path)):
            makedirs(pkg_dir_path)
        with open(path.join(pkg_dir_path, '.librepcb-pkg'), 'w') as f:
            f.write('0.1\n')
        with open(path.join(pkg_dir_path, 'package.lp'), 'w') as f:
            f.write('\n'.join(lines))
            f.write('\n')