aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--export_previews.py26
-rw-r--r--export_protoboards.py26
-rw-r--r--generate_protoboards.py310
-rw-r--r--gerbolyze/protoboard.py140
4 files changed, 427 insertions, 75 deletions
diff --git a/export_previews.py b/export_previews.py
new file mode 100644
index 0000000..68c895e
--- /dev/null
+++ b/export_previews.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+
+import multiprocessing as mp
+import subprocess
+import pathlib
+
+import click
+from tqdm import tqdm
+
+def process_file(indir, outdir, inpath):
+ outpath = outdir / inpath.relative_to(indir).with_suffix('.png')
+ outpath.parent.mkdir(parents=True, exist_ok=True)
+ subprocess.run(['resvg', '--export-id', 'g-top-copper', '--width', '1000', inpath, outpath],
+ check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+@click.command()
+@click.argument('indir', type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path))
+def export(indir):
+ jobs = list(indir.glob('svg/**/*.svg'))
+ with tqdm(total = len(jobs)) as tq:
+ with mp.Pool() as pool:
+ results = [ pool.apply_async(process_file, (indir / 'svg', indir / 'png', path), callback=lambda _res: tq.update(1)) for path in jobs ]
+ results = [ res.get() for res in results ]
+
+if __name__ == '__main__':
+ export()
diff --git a/export_protoboards.py b/export_protoboards.py
new file mode 100644
index 0000000..fe3aadf
--- /dev/null
+++ b/export_protoboards.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+
+import multiprocessing as mp
+import subprocess
+import pathlib
+
+import click
+from tqdm import tqdm
+
+def process_file(indir, outdir, inpath):
+ outpath = outdir / inpath.relative_to(indir).with_suffix('.zip')
+ outpath.parent.mkdir(parents=True, exist_ok=True)
+ subprocess.run('python3 -m gerbolyze convert --zip --pattern-complete-tiles-only --use-apertures-for-patterns'.split() + [inpath, outpath],
+ check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+@click.command()
+@click.argument('indir', type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=pathlib.Path))
+def export(indir):
+ jobs = list(indir.glob('svg/**/*.svg'))
+ with tqdm(total = len(jobs)) as tq:
+ with mp.Pool() as pool:
+ results = [ pool.apply_async(process_file, (indir / 'svg', indir / 'gerber', path), callback=lambda _res: tq.update(1)) for path in jobs ]
+ results = [ res.get() for res in results ]
+
+if __name__ == '__main__':
+ export()
diff --git a/generate_protoboards.py b/generate_protoboards.py
index 31c208d..2183bca 100644
--- a/generate_protoboards.py
+++ b/generate_protoboards.py
@@ -2,10 +2,11 @@
import itertools
import pathlib
+import textwrap
import click
-from gerbolyze.protoboard import ProtoBoard
+from gerbolyze.protoboard import ProtoBoard, EmptyProtoArea, THTProtoAreaCircles, SMDProtoAreaRectangles
common_defs = '''
empty = Empty(copper=False);
@@ -28,15 +29,15 @@ smd500r = SMDPads(2.0, 0.5);
def tht_normal_pitch100mil(size, mounting_holes=None):
- return ProtoBoard(common_defs, 'tht', mounting_holes, border=2).generate(*size)
+ 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).generate(*size)
+ 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).generate(*size)
+ return ProtoBoard(common_defs, f'tht50@{f}mm / tht', mounting_holes, border=2, tight_layout=True)
smd_basic = {
'smd100': 'smd_soic_100mil',
@@ -55,7 +56,7 @@ 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):
+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'
@@ -63,7 +64,10 @@ def generate(outdir, fun, sizes=sizes_large, name=None):
for w, h in sizes:
outfile = plain_dir / f'{name}_{w}x{h}.svg'
- outfile.write_text(fun((w, h)))
+ 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}'
@@ -74,42 +78,280 @@ def generate(outdir, fun, sizes=sizes_large, name=None):
continue
outfile = hole_dir / f'{name}_{w}x{h}_holes_M{dia:.1f}.svg'
try:
- outfile.write_text(fun((w, h), (dia, dia+2)))
+ 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 = [
+ ('<tr>'
+ f'<td><a href="gerber/{path.relative_to(outdir / "svg").with_suffix(".zip")}" download>Gerbers</a></td>'
+ f'<td><a href="png/{path.relative_to(outdir / "svg").with_suffix(".png")}">Preview</a></td>'
+ f'<td><a href="{path.relative_to(outdir)}" download>SVG</a></td>'
+ f'<td>{w:.2f} mm</td>'
+ f'<td>{h:.2f} mm</td>'
+ f'<td>{"Yes" if hole_dia is not None else "No"}</td>'
+ f'<td>{format_length(hole_dia)}</td>'
+ f'<td>{len(patterns)}</td>'
+ f'<td>{"Yes" if symmetric else "No"}</td>'
+ f'<td>{"Yes" if has_ground_plane(patterns) else "No"}</td>'
+ f'<td>{format_pitches(tht_pitches(patterns))}</td>'
+ f'<td>{format_pitches(smd_pitches(patterns))}</td>'
+ '</tr>')
+ 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'<th>{key}</th>' 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'<div class="filter-check"><input type="checkbox" id="check-{key_id(key)}-{val_id(value)}"><label for="check-{key_id(key)}-{val_id(value)}">{format_value(value)}</label></div>' for value in values)
+ for key, values in filter_cols.items() }
+ filter_cols = [f'<td id="filter-{key_id(key)}">{values}</td>' 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'''
+ <!DOCTYPE html>
+ <html>
+ <head><title>Protoboard Index</title></head>
+ <script src="tablesort.min.js"></script>
+ <script src="tablesort.number.min.js"></script>
+ <style>
+ {style}
+ </style>
+ <body>
+ <div id="filters-container">
+ <table id="filter">
+ <tr>
+ {filter_headers}
+ </tr>
+ <tr>
+ {filter_content}
+ </tr>
+ </table>
+ <button type="button" id="apply">Apply</button>
+ <button type="button" id="reset-filters">Reset filters</button>
+ </div>
+ <div id="listing-container">
+ <table id="listing">
+ <thead>
+ <tr>
+ <th data-sort-method="none">Download</th>
+ <th data-sort-method="none">Preview</th>
+ <th data-sort-method="none">Source SVG</th>
+ <th data-filter-key="width">Width</th>
+ <th data-filter-key="height">Height</th>
+ <th>Has Mounting Holes?</th>
+ <th data-filter-key="mounting_hole_diameter">Mounting Hole Diameter</th>
+ <th data-filter-key="number_of_areas">Number of Areas</th>
+ <th data-filter-key="symmetric_top_and_bottom">Symmetric Top and Bottom?</th>
+ <th data-filter-key="ground_plane">Ground Plane?</th>
+ <th data-filter-key="tht_pitches">THT Pitches</th>
+ <th data-filter-key="smd_pitches">SMD Pitches</th>
+ </tr>
+ </thead>
+ <tbody>
+ {table_content}
+ </tbody>
+ </table>
+ </div>
+ <script>
+ new Tablesort(document.getElementById('listing'));
+
+ {filter_js}
+ </script>
+ </body>
+ </html>
+ '''.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))
-def generate_all(outdir):
- generate(outdir / 'simple', tht_normal_pitch100mil)
- generate(outdir / 'simple', tht_pitch_50mil)
- generate(outdir / 'mixed', tht_mixed_pitch)
+@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).generate(*size)
- generate(outdir / 'simple', gen, sizes_small, name=f'{name}_ground_plane')
+ 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).generate(*size)
- generate(outdir / 'simple', gen, sizes_small, name=f'{name}_single_side')
+ 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).generate(*size)
- generate(outdir / 'simple', gen, sizes_small, name=f'{name}_double_side')
+ 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).generate(*size)
- generate(outdir / 'mixed', gen, sizes_small, name=f'tht_and_{name}')
+ 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).generate(*size)
- generate(outdir / 'mixed', gen, sizes_small, name=f'{name}_and_tht')
+ 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'):
@@ -117,8 +359,8 @@ def generate_all(outdir):
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).generate(*size)
- generate(outdir / 'mixed', gen, sizes_medium, name=f'tht_and_three_smd_100mil_950um_{suffix}')
+ 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('_')
@@ -127,34 +369,36 @@ def generate_all(outdir):
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).generate(*size)
- generate(outdir / 'mixed', gen, sizes_small, name=f'tht_and_two_smd_{name1}_{name2}')
+ 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).generate(*size)
- generate(outdir / 'mixed', gen, sizes_small, name=f'tht_and_two_sided_smd_{name1}_{name2}')
+ 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).generate(*size)
- generate(outdir / 'mixed', gen, sizes_small, name=f'two_sided_smd_{name1}_{name2}')
+ 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).generate(*size)
- generate(outdir / 'mixed', gen, sizes_medium, name=f'tht_and_50mil_and_two_smd_100mil_950um_{suffix}')
+ 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).generate(*size)
- generate(outdir / 'mixed', gen, [ (w, h) for w, h in sizes_medium if w > 60 and h > 60 ], name=f'all_tht_and_smd')
+ 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__':
diff --git a/gerbolyze/protoboard.py b/gerbolyze/protoboard.py
index 4de98aa..324f365 100644
--- a/gerbolyze/protoboard.py
+++ b/gerbolyze/protoboard.py
@@ -90,7 +90,7 @@ class PatternProtoArea:
raise ValueError('Pattern has different X and Y pitches')
return self.pitch_x
- def fit_size(self, defs, w, h):
+ def fit_size(self, w, h):
x, y, w, h = self.fit_rect(0, 0, w, h, False)
t, r, b, l = self.border
return (w+l+r), (h+t+b)
@@ -110,9 +110,16 @@ class PatternProtoArea:
else:
return x, y, w_fit, h_fit
- def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False):
+ def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
yield {}
+ def symmetric_sides(self):
+ return False
+
+ def used_patterns(self):
+ yield self
+
+
class EmptyProtoArea:
def __init__(self, copper=False, border=None):
self.copper = copper
@@ -127,10 +134,10 @@ class EmptyProtoArea:
else:
self.border = (border, border, border, border)
- def fit_size(self, defs, w, h):
+ def fit_size(self, w, h):
return w, h
- def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False):
+ def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
if self.copper:
t, r, b, l = self.border
x, y, w, h = x+l, y+t, w-l-r, h-t-b
@@ -138,6 +145,10 @@ class EmptyProtoArea:
else:
yield {}
+ def used_patterns(self):
+ yield self
+
+
class THTProtoAreaCircles(PatternProtoArea):
def __init__(self, pad_dia=2.0, drill=1.0, pitch=2.54, sides='both', plated=True, border=None):
super().__init__(pitch, border=border)
@@ -149,7 +160,7 @@ class THTProtoAreaCircles(PatternProtoArea):
self.plated = plated
self.sides = sides
- def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False):
+ def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
x, y, w, h = self.fit_rect(x, y, w, h, center)
drill = 'plated drill' if self.plated else 'nonplated drill'
@@ -173,6 +184,10 @@ class THTProtoAreaCircles(PatternProtoArea):
def __repr__(self):
return f'THTCircles(d={self.pad_dia}, h={self.drill}, p={self.pitch}, sides={self.sides}, plated={self.plated})'
+ def symmetric_sides(self):
+ return True
+
+
class SMDProtoAreaRectangles(PatternProtoArea):
def __init__(self, pitch_x, pitch_y, w=None, h=None, border=None):
super().__init__(pitch_x, pitch_y, border=border)
@@ -182,13 +197,16 @@ class SMDProtoAreaRectangles(PatternProtoArea):
self.pad_pattern = RectPattern(w, h, pitch_x, pitch_y)
self.patterns = [self.pad_pattern]
- def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False):
+ def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
x, y, w, h = self.fit_rect(x, y, w, h, center)
pad_id = str(uuid.uuid4())
yield {'defs': [self.pad_pattern.svg_def(pad_id, x, y)],
'top copper': make_rect(pad_id, x, y, w, h, clip),
'top mask': make_rect(pad_id, x, y, w, h, clip)}
+ def symmetric_sides(self):
+ return False
+
LAYERS = [
'top paste',
'top silk',
@@ -206,7 +224,7 @@ LAYERS = [
class ProtoBoard:
def __init__(self, defs, expr, mounting_holes=None, border=None, center=True, tight_layout=False):
self.defs = eval_defs(defs)
- self.layout = parse_layout(expr)
+ self.layout = parse_layout(expr, self.defs)
self.mounting_holes = mounting_holes
self.center = center
self.tight_layout = tight_layout
@@ -221,6 +239,14 @@ class ProtoBoard:
else:
self.border = (border, border, border, border)
+ @property
+ def symmetric_sides(self):
+ return self.layout.symmetric_sides()
+
+ @property
+ def used_patterns(self):
+ return set(self.layout.used_patterns())
+
def generate(self, w, h):
out = {l: [] for l in LAYERS}
svg_defs = []
@@ -251,7 +277,7 @@ class ProtoBoard:
f'<circle cx="{o}" cy="{h-o}" r="{d/2}"/>' ])
t, r, b, l = self.border
- for layer_dict in self.layout.generate(l, t, w-l-r, h-t-b, self.defs, self.center, clip, self.tight_layout):
+ for layer_dict in self.layout.generate(l, t, w-l-r, h-t-b, self.center, clip, self.tight_layout):
for l in LAYERS:
if l in layer_dict:
out[l].append(layer_dict[l])
@@ -297,14 +323,14 @@ class PropLayout:
if len(content) != len(proportions):
raise ValueError('proportions and content must have same length')
- def generate(self, x, y, w, h, defs, center=True, clip='', tight_layout=False):
- for (c_x, c_y, c_w, c_h), child in self.layout_2d(defs, x, y, w, h, tight_layout):
- yield from child.generate(c_x, c_y, c_w, c_h, defs, center, clip, tight_layout)
+ def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
+ for (c_x, c_y, c_w, c_h), child in self.layout_2d(x, y, w, h, tight_layout):
+ yield from child.generate(c_x, c_y, c_w, c_h, center, clip, tight_layout)
- def fit_size(self, defs, w, h):
+ def fit_size(self, w, h):
widths = []
heights = []
- for (_x, _y, w, h), child in self.layout_2d(defs, 0, 0, w, h, True):
+ for (_x, _y, w, h), child in self.layout_2d(0, 0, w, h, True):
if not isinstance(child, EmptyProtoArea):
widths.append(w)
heights.append(h)
@@ -313,7 +339,7 @@ class PropLayout:
else:
return max(widths), sum(heights)
- def layout_2d(self, defs, x, y, w, h, tight_layout=False):
+ def layout_2d(self, x, y, w, h, tight_layout=False):
actual_l = 0
target_l = 0
for l, child in zip(self.layout(w if self.direction == 'h' else h), self.content):
@@ -321,23 +347,22 @@ class PropLayout:
this_w, this_h = w, h
target_l += l
- if isinstance(child, str):
- child = defs[child]
-
if self.direction == 'h':
this_w = target_l - actual_l
else:
this_h = target_l - actual_l
if tight_layout:
- this_w, this_h = child.fit_size(defs, this_w, this_h)
+ this_w, this_h = child.fit_size(this_w, this_h)
if self.direction == 'h':
x += this_w
actual_l += this_w
+ this_h = h
else:
y += this_h
actual_l += this_h
+ this_w = w
yield (this_x, this_y, this_w, this_h), child
@@ -356,9 +381,17 @@ class PropLayout:
children = ', '.join( f'{elem}:{width}' for elem, width in zip(self.content, self.proportions))
return f'PropLayout[{self.direction.upper()}]({children})'
+ def symmetric_sides(self):
+ return all(child.symmetric_sides() for child in self.content)
+
+ def used_patterns(self):
+ for child in self.content:
+ yield from child.used_patterns()
+
+
class TwoSideLayout:
def __init__(self, top, bottom):
- self._top, self._bottom = top, bottom
+ self.top, self.bottom = top, bottom
def flip(self, defs):
out = dict(defs)
@@ -378,16 +411,10 @@ class TwoSideLayout:
return defs
- def top(self, defs):
- return defs[self._top] if isinstance(self._top, str) else self._top
-
- def bottom(self, defs):
- return defs[self._bottom] if isinstance(self._bottom, str) else self._bottom
-
- def fit_size(self, defs, w, h):
- top, bottom = self.top(defs), self.bottom(defs)
- w1, h1 = top.fit_size(defs, w, h)
- w2, h2 = bottom.fit_size(defs, w, h)
+ def fit_size(self, w, h):
+ top, bottom = self.top, self.bottom
+ w1, h1 = top.fit_size(w, h)
+ w2, h2 = bottom.fit_size(w, h)
if isinstance(top, EmptyProtoArea):
if isinstance(bottom, EmptyProtoArea):
return w1, h1
@@ -396,13 +423,21 @@ class TwoSideLayout:
return w1, h1
return max(w1, w2), max(h1, h2)
- def generate(self, x, y, w, h, defs, center=True, clip='', tight_layout=False):
- yield from self.top(defs).generate(x, y, w, h, defs, center, clip, tight_layout)
- yield from map(self.flip, self.bottom(defs).generate(x, y, w, h, defs, center, clip, tight_layout))
+ def generate(self, x, y, w, h, center=True, clip='', tight_layout=False):
+ yield from self.top.generate(x, y, w, h, center, clip, tight_layout)
+ yield from map(self.flip, self.bottom.generate(x, y, w, h, center, clip, tight_layout))
+
+ def symmetric_sides(self):
+ return self.top == self.bottom
-def _map_expression(node):
+ def used_patterns(self):
+ yield from self.top.used_patterns()
+ yield from self.bottom.used_patterns()
+
+
+def _map_expression(node, defs):
if isinstance(node, ast.Name):
- return node.id
+ return defs[node.id]
elif isinstance(node, ast.Constant):
return node.value
@@ -414,14 +449,14 @@ def _map_expression(node):
left, right = node.left, node.right
if isinstance(left, ast.BinOp) and isinstance(left.op, ast.MatMult):
- left_prop = _map_expression(left.right)
+ left_prop = _map_expression(left.right, defs)
left = left.left
if isinstance(right, ast.BinOp) and isinstance(right.op, ast.MatMult):
- right_prop = _map_expression(right.right)
+ right_prop = _map_expression(right.right, defs)
right = right.left
- left, right = _map_expression(left), _map_expression(right)
+ left, right = _map_expression(left, defs), _map_expression(right, defs)
direction = 'h' if isinstance(node.op, ast.BitOr) else 'v'
if isinstance(left, PropLayout) and left.direction == direction and left_prop is None:
@@ -449,7 +484,7 @@ def _map_expression(node):
else:
raise SyntaxError(f'Invalid layout expression "{ast.unparse(node)}"')
-def parse_layout(expr):
+def parse_layout(expr, defs):
''' Example layout:
( tht @ 2in | smd ) @ 50% / tht
@@ -462,14 +497,14 @@ def parse_layout(expr):
expr = ast.parse(expr, mode='eval').body
match expr:
case ast.Name():
- return PropLayout([expr.id], 'h', [None])
+ return PropLayout([defs[expr.id]], 'h', [None])
case ast.BinOp(op=ast.MatMult()):
assert isinstance(expr.right, ast.Constant)
- return PropLayout([_map_expression(expr.left)], 'h', [expr.right.value])
+ return PropLayout([_map_expression(expr.left, defs)], 'h', [expr.right.value])
case _:
- return _map_expression(expr)
+ return _map_expression(expr, defs)
except SyntaxError as e:
raise SyntaxError('Invalid layout expression') from e
@@ -506,6 +541,26 @@ def eval_defs(defs):
out[key] = PROTO_AREA_TYPES[pattern](*args, **kws)
return out
+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);
+'''
+
+
if __name__ == '__main__':
# import sys
# print('===== Layout expressions =====')
@@ -538,5 +593,6 @@ if __name__ == '__main__':
# print('===== Proto board =====')
#b = ProtoBoard('tht = THTCircles(); tht_small = THTCircles(pad_dia=1.0, drill=0.6, pitch=1.27)',
# 'tht@1in|(tht_small@2/tht@1)', mounting_holes=(3.2, 5.0, 5.0), border=2, center=False)
- b = ProtoBoard('tht = THTCircles(); smd1 = SMDPads(2.0, 2.0); smd2 = SMDPads(0.95, 1.895); plane=Empty(copper=True)', 'tht@25mm | (smd1 + plane)', mounting_holes=(3.2, 5.0, 5.0), border=2, tight_layout=True)
+ #b = ProtoBoard('tht = THTCircles(); smd1 = SMDPads(2.0, 2.0); smd2 = SMDPads(0.95, 1.895); plane=Empty(copper=True)', 'tht@25mm | (smd1 + plane)', mounting_holes=(3.2, 5.0, 5.0), border=2, tight_layout=True)
+ b = ProtoBoard(COMMON_DEFS, f'((smd100 + smd100) | (smd950 + smd950) | tht50@20mm)@20mm / tht', mounting_holes=(3.2,5,5), border=1, tight_layout=True, center=True)
print(b.generate(80, 60))