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')