kopia lustrzana https://github.com/jaseg/gerbolyze
protoboards: WIP
rodzic
25ebdbe625
commit
68ce1505f1
|
@ -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()
|
|
@ -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()
|
|
@ -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__':
|
||||
|
|
|
@ -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 _map_expression(node):
|
||||
def symmetric_sides(self):
|
||||
return self.top == self.bottom
|
||||
|
||||
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))
|
||||
|
|
Ładowanie…
Reference in New Issue