#!/usr/bin/env python3 import itertools import pathlib import textwrap import click from gerbolyze.protoboard import ProtoBoard, EmptyProtoArea, THTProtoAreaCircles, SMDProtoAreaRectangles common_defs = ''' empty = Empty(copper=False); ground = Empty(copper=True); tht = THTCircles(); tht50 = THTCircles(pad_dia=1.0, drill=0.6, pitch=1.27); smd100 = SMDPads(1.27, 2.54); smd100r = SMDPads(2.54, 1.27); smd950 = SMDPads(0.95, 2.5); smd950r = SMDPads(2.5, 0.95); smd800 = SMDPads(0.80, 2.0); smd800r = SMDPads(2.0, 0.80); smd650 = SMDPads(0.65, 2.0); smd650r = SMDPads(2.0, 0.65); smd500 = SMDPads(0.5, 2.0); smd500r = SMDPads(2.0, 0.5); ''' def tht_normal_pitch100mil(size, mounting_holes=None): return ProtoBoard(common_defs, 'tht', mounting_holes, border=2) def tht_pitch_50mil(size, mounting_holes=None): return ProtoBoard(common_defs, 'tht50', mounting_holes, border=2) def tht_mixed_pitch(size, mounting_holes=None): w, h = size f = max(1.27*5, min(30, h*0.3)) return ProtoBoard(common_defs, f'tht50@{f}mm / tht', mounting_holes, border=2, tight_layout=True) smd_basic = { 'smd100': 'smd_soic_100mil', 'smd950': 'smd_sot_950um', 'smd800': 'smd_sop_800um', 'smd650': 'smd_sot_650um', 'smd500': 'smd_sop_500um' } #lengths_large = [15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100, 120, 150, 160, 180, 200, 250, 300] lengths_large = [30, 40, 50, 60, 80, 100, 120, 150, 160] sizes_large = list(itertools.combinations(lengths_large, 2)) lengths_small = [15, 20, 25, 30, 40, 50, 60, 80, 100] sizes_small = list(itertools.combinations(lengths_small, 2)) lengths_medium = lengths_large sizes_medium = list(itertools.combinations(lengths_medium, 2)) def generate(outdir, fun, sizes=sizes_large, name=None, generate_svg=True): name = name or fun.__name__ outdir = outdir / f'{name}' plain_dir = outdir / 'no_mounting_holes' plain_dir.mkdir(parents=True, exist_ok=True) for w, h in sizes: outfile = plain_dir / f'{name}_{w}x{h}.svg' board = fun((w, h)) yield outfile, (float(w), float(h), None, board.symmetric_sides, board.used_patterns) if generate_svg: outfile.write_text(board.generate(w, h)) for dia in (2, 2.5, 3, 4): hole_dir = outdir / f'mounting_holes_M{dia:.1f}' hole_dir.mkdir(exist_ok=True) for w, h in sizes: if w < 25 or h < 25: continue outfile = hole_dir / f'{name}_{w}x{h}_holes_M{dia:.1f}.svg' try: board = fun((w, h), (dia, dia+2)) yield outfile, (float(w), float(h), float(dia), board.symmetric_sides, board.used_patterns) if generate_svg: outfile.write_text(board.generate(w, h)) except ValueError: # mounting hole keepout too large for small board, ignore. pass def write_index(index, outdir): tht_pitches = lambda patterns: [ p.pitch for p in patterns if isinstance(p, THTProtoAreaCircles) ] smd_pitches = lambda patterns: [ min(p.pitch_x, p.pitch_y) for p in patterns if isinstance(p, SMDProtoAreaRectangles) ] has_ground_plane = lambda patterns: any(isinstance(p, EmptyProtoArea) and p.copper for p in patterns) format_pitches = lambda pitches: ', '.join(f'{p:.2f} mm' for p in sorted(pitches)) format_length = lambda length_or_none, default='': default if length_or_none is None else f'{length_or_none:.2f} mm' table_rows = [ ('' f'Gerbers' f'Preview' f'SVG' f'{w:.2f} mm' f'{h:.2f} mm' f'{"Yes" if hole_dia is not None else "No"}' f'{format_length(hole_dia)}' f'{len(patterns)}' f'{"Yes" if symmetric else "No"}' f'{"Yes" if has_ground_plane(patterns) else "No"}' f'{format_pitches(tht_pitches(patterns))}' f'{format_pitches(smd_pitches(patterns))}' '') for path, (w, h, hole_dia, symmetric, patterns) in index.items() ] table_content = '\n'.join(table_rows) length_sort = lambda length: float(length.partition(' ')[0]) filter_cols = { 'Width': sorted(set(w for w, h, *rest in index.values())), 'Height': sorted(set(h for w, h, *rest in index.values())), 'Mounting Hole Diameter': sorted(set(dia for w, h, dia, *rest in index.values() if dia)) + ['None'], 'Number of Areas': sorted(set(len(patterns) for *_rest, patterns in index.values())), 'Symmetric Top and Bottom?': ['Yes', 'No'], 'Ground Plane?': ['Yes', 'No'], 'THT Pitches': sorted(set(p for *_rest, patterns in index.values() for p in tht_pitches(patterns))) + ['None'], 'SMD Pitches': sorted(set(p for *_rest, patterns in index.values() for p in smd_pitches(patterns))) + ['None'], } filter_headers = '\n'.join(f'{key}' for key in filter_cols) key_id = lambda key: key.lower().replace("?", "").replace(" ", "_") val_id = lambda value: str(value).replace(".", "_") def format_value(value): if isinstance(value, str): return value elif isinstance(value, int): return str(value) elif isinstance(value, bool): return value and 'Yes' or 'No' else: return format_length(value) filter_cols = { key: '\n'.join(f'
' for value in values) for key, values in filter_cols.items() } filter_cols = [f'{values}' for key, values in filter_cols.items()] filter_content = '\n'.join(filter_cols) filter_js = textwrap.dedent(''' function get_filters(){ let filters = {}; table = document.querySelector('#filter'); for (let filter of table.querySelectorAll('td')) { selected = []; for (let checkbox of filter.querySelectorAll('input')) { if (checkbox.checked) { selected.push(checkbox.nextElementSibling.textContent); } } filters[filter.id.replace(/^filter-/, '')] = selected; } return filters; } filter_indices = { }; for (const [i, header] of document.querySelectorAll("#listing th").entries()) { if (header.hasAttribute('data-filter-key')) { filter_indices[header.attributes['data-filter-key'].value] = i; } } function filter_row(filters, row) { cols = row.querySelectorAll('td'); for (const [filter_id, values] of Object.entries(filters)) { if (values.length == 0) { continue; } const row_value = cols[filter_indices[filter_id]].textContent; if (values.includes("None") && !row_value) { continue; } if (values.includes(row_value)) { continue; } return false; } return true; } let timeout = undefined; function apply_filters() { if (timeout) { clearTimeout(timeout); timeout = undefined; } const filters = get_filters(); for (let row of document.querySelectorAll("#listing tbody tr")) { if (filter_row(filters, row)) { row.style.display = ''; } else { row.style.display = 'none'; } } } function refresh_filters() { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(apply_filters, 2000); } function reset_filters() { for (let checkbox of document.querySelectorAll("#filter input")) { checkbox.checked = false; } refresh_filters(); } document.querySelector("#apply").onclick = apply_filters; document.querySelector("#reset-filters").onclick = reset_filters; for (let checkbox of document.querySelectorAll("#filter input")) { checkbox.onchange = refresh_filters; } apply_filters(); '''.strip()) style = textwrap.dedent(''' :root { --gray1: #d0d0d0; --gray2: #eeeeee; font-family: sans-serif; } table { table-layout: fixed; border-collapse: collapse; box-shadow: 0 0 3px gray; } td { border: 1px solid var(--gray1); padding: .1em .5em; } th { border: 1px solid var(--gray1); padding: .5em; background: linear-gradient(0deg, #e0e0e0, #eeeeee); } #listing tr:hover { background-color: #ffff80; } button { margin: 2em 0.2em; padding: .5em 1em; } '''.strip()) html = textwrap.dedent(f''' Protoboard Index
{filter_headers} {filter_content}
{table_content}
Download Preview Source SVG Width Height Has Mounting Holes? Mounting Hole Diameter Number of Areas Symmetric Top and Bottom? Ground Plane? THT Pitches SMD Pitches
'''.strip()) (outdir / 'index.html').write_text(html) @click.command() @click.argument('outdir', type=click.Path(file_okay=False, dir_okay=True, path_type=pathlib.Path)) @click.option('--generate-svg/--no-generate-svg') def generate_all(outdir, generate_svg): index = {} index.update(generate(outdir / 'svg' / 'simple', tht_normal_pitch100mil, generate_svg=generate_svg)) index.update(generate(outdir / 'svg' / 'simple', tht_pitch_50mil, generate_svg=generate_svg)) index.update(generate(outdir / 'svg' / 'mixed', tht_mixed_pitch, generate_svg=generate_svg)) for pattern, name in smd_basic.items(): def gen(size, mounting_holes=None): return ProtoBoard(common_defs, f'{pattern} + ground', mounting_holes, border=1) index.update(generate(outdir / 'svg' / 'simple', gen, sizes_small, name=f'{name}_ground_plane', generate_svg=generate_svg)) def gen(size, mounting_holes=None): return ProtoBoard(common_defs, f'{pattern} + empty', mounting_holes, border=1) index.update(generate(outdir / 'svg' / 'simple', gen, sizes_small, name=f'{name}_single_side', generate_svg=generate_svg)) def gen(size, mounting_holes=None): return ProtoBoard(common_defs, f'{pattern} + {pattern}', mounting_holes, border=1) index.update(generate(outdir / 'svg' / 'simple', gen, sizes_small, name=f'{name}_double_side', generate_svg=generate_svg)) def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(30, h*0.3)) return ProtoBoard(common_defs, f'({pattern} + {pattern})@{f}mm / tht', mounting_holes, border=1, tight_layout=True) index.update(generate(outdir / 'svg' / 'mixed', gen, sizes_small, name=f'tht_and_{name}', generate_svg=generate_svg)) def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(30, h*0.3)) return ProtoBoard(common_defs, f'({pattern} + {pattern}) / tht@{f}mm', mounting_holes, border=1, tight_layout=True) index.update(generate(outdir / 'svg' / 'mixed', gen, sizes_small, name=f'{name}_and_tht', generate_svg=generate_svg)) *_, suffix = name.split('_') if suffix not in ('100mil', '950um'): def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(50, h*0.3)) f2 = max(1.27*5, min(30, w*0.2)) return ProtoBoard(common_defs, f'((smd100 + smd100) | (smd950 + smd950) | ({pattern}r + {pattern}r)@{f2}mm)@{f}mm / tht', mounting_holes, border=1, tight_layout=True) index.update(generate(outdir / 'svg' / 'mixed', gen, sizes_medium, name=f'tht_and_three_smd_100mil_950um_{suffix}', generate_svg=generate_svg)) for (pattern1, name1), (pattern2, name2) in itertools.combinations(smd_basic.items(), 2): *_, name1 = name1.split('_') *_, name2 = name2.split('_') def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(30, h*0.3)) return ProtoBoard(common_defs, f'(({pattern1} + {pattern1}) | ({pattern2} + {pattern2}))@{f}mm / tht', mounting_holes, border=1, tight_layout=True) index.update(generate(outdir / 'svg' / 'mixed', gen, sizes_small, name=f'tht_and_two_smd_{name1}_{name2}', generate_svg=generate_svg)) def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(30, h*0.3)) return ProtoBoard(common_defs, f'({pattern1} + {pattern2})@{f}mm / tht', mounting_holes, border=1, tight_layout=True) index.update(generate(outdir / 'svg' / 'mixed', gen, sizes_small, name=f'tht_and_two_sided_smd_{name1}_{name2}', generate_svg=generate_svg)) def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(30, h*0.3)) return ProtoBoard(common_defs, f'{pattern1} + {pattern2}', mounting_holes, border=1) index.update(generate(outdir / 'svg' / 'mixed', gen, sizes_small, name=f'two_sided_smd_{name1}_{name2}', generate_svg=generate_svg)) def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(50, h*0.3)) f2 = max(1.27*5, min(30, w*0.2)) return ProtoBoard(common_defs, f'((smd100 + smd100) | (smd950 + smd950) | tht50@{f2}mm)@{f}mm / tht', mounting_holes, border=1, tight_layout=True) index.update(generate(outdir / 'svg' / 'mixed', gen, sizes_medium, name=f'tht_and_50mil_and_two_smd_100mil_950um', generate_svg=generate_svg)) def gen(size, mounting_holes=None): w, h = size f = max(1.27*5, min(30, h*0.3)) f2 = max(1.27*5, min(25, w*0.1)) return ProtoBoard(common_defs, f'tht50@10mm | tht | ((smd100r + smd100r) / (smd950r + smd950r) / (smd800 + smd800)@{f2}mm / (smd650 + smd650)@{f2}mm / (smd500 + smd500)@{f2}mm)@{f}mm', mounting_holes, border=1, tight_layout=True) index.update(generate(outdir / 'svg' / 'mixed', gen, [ (w, h) for w, h in sizes_medium if w > 61 and h > 60 ], name=f'all_tht_and_smd', generate_svg=generate_svg)) write_index(index, outdir) if __name__ == '__main__': generate_all()