skill
PCB Design Rules
Public Made by Adomby adom
Canonical PCB fab design rules (DRC) for every popular vendor: one JSON record per vendor, process and layer count, with generated Fusion/EAGLE .dru and .edru, KiCad .kicad_dru plus board-setup JSON, Altium .RUL exports, and a human comparison table. Every number cites the vendor's own capabilities page and the date it was read.
main
John Lauer
Specify 3rdPartyFab reference/value sizes and face-aware mechanical silkscreen policy
a59b909
5d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
#!/usr/bin/env python3
"""Generate EDA rule files from the canonical records in rules/records/.
Canonical unit is mm; mil values are derived (1 mil = 0.0254 mm) and rounded
to 2 decimals. Every generated file carries its source URLs and read dates.
Outputs:
rules/index.json
fusion/<slug>[-<process>]-<n>L.dru (classic EAGLE text)
fusion/<slug>[-<process>]-<n>L.edru (Fusion/EAGLE 9 XML, seed-compatible)
kicad/<slug>[-<process>]-<n>L.kicad_dru
kicad/<slug>[-<process>]-<n>L.board-setup.json
altium/<slug>[-<process>]-<n>L.RUL
And rewrites the comparison table in README.md between the TABLE markers.
"""
import json
import pathlib
import re
import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
MIL = 0.0254
def mil(mm):
return round(mm / MIL, 2)
def fmm(mm):
if mm is None:
return "n/a"
s = f"{mm:.4f}".rstrip("0").rstrip(".")
return s
def slugify(s):
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
def file_slug(vendor_slug, process, layers):
p = "" if process == "standard" else f"-{process}"
return f"{vendor_slug}{p}-{layers}L"
def pad_pad_spacing(rec):
sp = rec["minSpacing"]["mm"]
return max(sp, 0.15)
def g(rec, key, sub="mm"):
v = rec.get(key)
if not v:
return None
return v.get(sub)
LAYER_SETUPS = {2: "(1*16)", 4: "(1*2*15*16)", 6: "(1*2*3*14*15*16)"}
def effective_via(rec):
"""Return (drillMm, padMm or None), deriving pad from drill + 2x annular ring."""
via = rec.get("minVia") or {}
drill = via.get("drillMm")
if drill is None and rec.get("minDrill"):
drill = rec["minDrill"]["mm"]
pad = via.get("padMm") if via else None
if pad is None and drill is not None and rec.get("annularRing"):
pad = round(drill + 2 * rec["annularRing"]["viaMm"], 4)
return drill, pad
def description_text(vendor, rec):
tw = rec["minTraceWidth"]
sp = rec["minSpacing"]
via_d, via_p = effective_via(rec)
lines = [
f"{vendor} {rec['layerCount']}-layer design rules ({rec['process']} process)",
"",
f"Min trace width: {fmm(tw['mm'])} mm ({tw['mil']} mil){' - ' + tw['condition'] if tw.get('condition') else ''}",
f"Min spacing: {fmm(sp['mm'])} mm ({sp['mil']} mil){' - ' + sp['condition'] if sp.get('condition') else ''}",
f"Min drill: {fmm(rec['minDrill']['mm'])} mm ({rec['minDrill']['mil']} mil)",
f"Min via: {fmm(via_d)} mm hole" + (f" / {fmm(via_p)} mm pad" if via_p else ""),
]
if rec.get("annularRing"):
ar = rec["annularRing"]
lines.append(f"Annular ring: via {fmm(ar['viaMm'])} mm" + (f", pad {fmm(ar['padMm'])} mm" if ar.get("padMm") else ""))
if rec.get("copperToBoardEdge"):
lines.append(f"Copper to board edge: {fmm(rec['copperToBoardEdge']['mm'])} mm")
if rec.get("holeToHole"):
lines.append(f"Hole to hole: {fmm(rec['holeToHole']['mm'])} mm")
if rec.get("drillRange"):
dr = rec["drillRange"]
lines.append(f"Drill range: {fmm(dr['minMm'])} mm to {fmm(dr['maxMm'])} mm")
if rec.get("boardThicknessOptionsMm"):
lines.append("Board thickness options: " + ", ".join(f"{t} mm" for t in rec["boardThicknessOptionsMm"]))
if rec.get("copperWeightOptionsOz"):
lines.append("Copper weight options: " + ", ".join(f"{w} oz" for w in rec["copperWeightOptionsOz"]))
if rec.get("surfaceFinishes"):
lines.append("Surface finishes: " + ", ".join(rec["surfaceFinishes"]))
if rec.get("maxBoardSize"):
mb = rec["maxBoardSize"]
lines.append(f"Max board size: {fmm(mb['xMm'])} x {fmm(mb['yMm'])} mm")
if rec.get("notes"):
lines += ["", rec["notes"]]
lines += ["", "Sources:"]
for s in rec.get("sources", []):
lines.append(f"- {s['url']} (read {s['dateRead']})")
return "\n".join(lines)
# ---------------------------------------------------------------- EAGLE .edru
def gen_edru(vendor, rec):
tw = rec["minTraceWidth"]["mm"]
sp = rec["minSpacing"]["mm"]
dr = rec["minDrill"]["mm"]
pp = pad_pad_spacing(rec)
c2e = g(rec, "copperToBoardEdge") or 0.2
h2h = g(rec, "holeToHole") or 0.2
arv = (g(rec, "annularRing", "viaMm")) or 0.1
arp = (g(rec, "annularRing", "padMm")) or 0.18
layers = rec["layerCount"]
name = file_slug(slugify(vendor), rec["process"], layers)
def clearance_rules():
rows = []
combos = [
("is_wire_polygon", "is_wire_polygon"),
("is_wire_polygon", "is_pad_smd"),
("is_wire_polygon", "is_via"),
("is_pad_smd", "is_pad_smd"),
("is_pad_smd", "is_via"),
("is_via", "is_via"),
]
same = [
("is_smd", "is_pad"),
("is_smd", "is_via"),
("is_smd", "is_smd"),
]
prio = 1
for i, (a, b) in enumerate(combos):
v = pp if (a, b) == ("is_pad_smd", "is_pad_smd") else sp
pv = v if (a, b) == ("is_pad_smd", "is_pad_smd") else max(sp * 2, v)
rows.append(f' <rule type="Copper Clearance" builtin_ruleid="{i}" enabled="yes" priority="{prio}" onescope="{a}" otherscope="{b}" value="{fmm(v)}mm" preferredvalue="{fmm(pv)}mm" samesignal="no" name="copper-clearance-{i+1}"/>')
prio += 1
for j, (a, b) in enumerate(same):
rows.append(f' <rule type="Copper Clearance" builtin_ruleid="{6+j}" enabled="yes" priority="{prio}" onescope="{a}" otherscope="{b}" value="{fmm(sp)}mm" preferredvalue="{fmm(max(sp*2, sp))}mm" samesignal="yes" name="copper-clearance-{7+j}"/>')
prio += 1
return "\n".join(rows)
rules_xml = f'''<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE designrules SYSTEM "eagle.dtd">
<eagle version="9.7.0">
<designrules name="{name}.edru">
<classes>
<class number="0" name="default">
</class>
<class number="1" name="pwr">
</class>
</classes>
<rules>
<rulecategory type="Copper Clearance" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Keepout Clearance" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Solder-Mask Clearance" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Dimension Clearance" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Drill Distance" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Restrict Clearance" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Minimum Drill Size" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Minimum Copper Width" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Acute Angle" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Air-Wire" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Small Gap" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Wire Stub" manualdrc="yes" livedrc="no"/>
<rulecategory type="Matched Lengths" manualdrc="yes" livedrc="yes"/>
<rulecategory type="Via Restrict Clearance" manualdrc="yes" livedrc="yes"/>
<rule type="Minimum Copper Width" enabled="yes" priority="0" onescope="classes=1" value="{fmm(tw)}mm" preferredvalue="{fmm(max(tw*3, 0.3))}mm" check_polywidth="no" name="Trace size for Power"/>
<rule type="Minimum Copper Width" builtin_ruleid="14" enabled="yes" priority="1" value="{fmm(tw)}mm" preferredvalue="{fmm(max(tw*2, 0.2))}mm" check_polywidth="no" name="minimum-copper-width-1"/>
<rule type="Minimum Drill Size" enabled="yes" priority="0" onescope="is_via" value="{fmm(dr)}mm" preferredvalue="{fmm(max(dr*2, 0.2))}mm" name="Min Via hole size on {layers}-layer"/>
<rule type="Minimum Drill Size" enabled="no" priority="1" onescope="all" value="0.5mm" preferredvalue="0.5mm" name="Min Non-plated holes"/>
<rule type="Minimum Drill Size" builtin_ruleid="15" enabled="yes" priority="2" value="{fmm(dr)}mm" preferredvalue="{fmm(max(dr*2, 0.25))}mm" name="minimum-drill-size-1"/>
<rule type="Acute Angle" builtin_ruleid="16" enabled="no" priority="0" name="acute-angle-1"/>
<rule type="Wire Stub" builtin_ruleid="17" enabled="yes" priority="0" name="wire-stub-1"/>
<rule type="Air-Wire" builtin_ruleid="19" enabled="yes" priority="0" name="air-wire-1"/>
<rule type="Matched Lengths" builtin_ruleid="20" enabled="yes" priority="0" tolerance="10mm" gapfactor="2.5" name="matched-lengths-1"/>
<rule type="Copper Clearance" enabled="no" priority="0" onescope="all" otherscope="all" value="{fmm(max(sp,0.25))}mm" preferredvalue="{fmm(max(sp,0.25))}mm" samesignal="yes" bothscope="all" name="Same-net track spacing"/>
{clearance_rules()}
<rule type="Dimension Clearance" builtin_ruleid="9" enabled="yes" priority="0" value="{fmm(c2e)}mm" preferredvalue="{fmm(max(c2e, 0.4))}mm" name="dimension-clearance-1"/>
<rule type="Via Restrict Clearance" builtin_ruleid="21" enabled="yes" priority="0" value="0mil" preferredvalue="0mil" name="via-restrict-clearance-1"/>
<rule type="Restrict Clearance" builtin_ruleid="10" enabled="yes" priority="0" value="0mil" preferredvalue="0mil" name="restrict-clearance-1"/>
<rule type="Small Gap" builtin_ruleid="18" enabled="yes" priority="0" name="small-gap-1"/>
<rule type="Solder-Mask Clearance" builtin_ruleid="11" enabled="no" priority="0" value="0mil" preferredvalue="0mil" check_name="yes" check_value="no" name="solder-mask-clearance-1"/>
<rule type="Drill Distance" builtin_ruleid="12" enabled="yes" priority="0" value="{fmm(h2h)}mm" preferredvalue="{fmm(h2h)}mm" name="drill-distance-1"/>
<rule type="Keepout Clearance" builtin_ruleid="13" enabled="yes" priority="0" value="0mil" preferredvalue="0mil" name="keepout-clearance-1"/>
</rules>
<param name="mdWireWire" value="{fmm(sp)}mm"/>
<param name="mdWirePad" value="{fmm(sp)}mm"/>
<param name="mdWireVia" value="{fmm(sp)}mm"/>
<param name="mdPadPad" value="{fmm(pp)}mm"/>
<param name="mdPadVia" value="{fmm(sp)}mm"/>
<param name="mdViaVia" value="{fmm(sp)}mm"/>
<param name="mdSmdPad" value="{fmm(sp)}mm"/>
<param name="mdSmdVia" value="{fmm(sp)}mm"/>
<param name="mdSmdSmd" value="{fmm(sp)}mm"/>
<param name="mdCopperDimension" value="{fmm(c2e)}mm"/>
<param name="mdDrill" value="{fmm(h2h)}mm"/>
<param name="msWidth" value="{fmm(tw)}mm"/>
<param name="msDrill" value="{fmm(dr)}mm"/>
<param name="dpMaxLengthDifference" value="10mm"/>
<param name="dpGapFactor" value="2.5"/>
<description language="en">{description_text(vendor, rec)}
</description>
<param name="layerSetup" value="{LAYER_SETUPS[layers]}"/>
<param name="mtCopper" value="{' '.join(['0.035mm'] * 16)}"/>
<param name="mtIsolate" value="1.6mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm"/>
<param name="mdViaViaSameLayer" value="6mil"/>
<param name="mnLayersViaInSmd" value="2"/>
<param name="mdSmdStop" value="0mil"/>
<param name="msMicroVia" value="0.3mm"/>
<param name="msBlindViaRatio" value="0.5"/>
<param name="rvPadTop" value="0"/>
<param name="rvPadInner" value="0"/>
<param name="rvPadBottom" value="0"/>
<param name="rvViaOuter" value="0"/>
<param name="rvViaInner" value="0"/>
<param name="rvMicroViaOuter" value="0"/>
<param name="rvMicroViaInner" value="0"/>
<param name="rlMinPadTop" value="{fmm(arp)}mm"/>
<param name="rlMaxPadTop" value="80mil"/>
<param name="rlMinPadInner" value="{fmm(arp)}mm"/>
<param name="rlMaxPadInner" value="20mil"/>
<param name="rlMinPadBottom" value="{fmm(arp)}mm"/>
<param name="rlMaxPadBottom" value="80mil"/>
<param name="rlMinViaOuter" value="{fmm(arv)}mm"/>
<param name="rlMaxViaOuter" value="20mil"/>
<param name="rlMinViaInner" value="{fmm(arv)}mm"/>
<param name="rlMaxViaInner" value="20mil"/>
<param name="rlMinMicroViaOuter" value="{fmm(arv)}mm"/>
<param name="rlMaxMicroViaOuter" value="20mil"/>
<param name="rlMinMicroViaInner" value="{fmm(arv)}mm"/>
<param name="rlMaxMicroViaInner" value="20mil"/>
<param name="psTop" value="-1"/>
<param name="psBottom" value="-1"/>
<param name="psFirst" value="-1"/>
<param name="psElongationLong" value="100"/>
<param name="psElongationOffset" value="100"/>
<param name="mvStopFrame" value="0"/>
<param name="mvCreamFrame" value="0"/>
<param name="mlMinStopFrame" value="0.038mm"/>
<param name="mlMaxStopFrame" value="4mil"/>
<param name="mlMinCreamFrame" value="0mil"/>
<param name="mlMaxCreamFrame" value="0mil"/>
<param name="mlViaStopLimit" value="0.5mm"/>
<param name="srRoundness" value="0"/>
<param name="srMinRoundness" value="0mil"/>
<param name="srMaxRoundness" value="0mil"/>
<param name="slThermalIsolate" value="10mil"/>
<param name="slThermalsForVias" value="0"/>
<param name="checkAngle" value="0"/>
<param name="checkRestrict" value="1"/>
<param name="checkStop" value="0"/>
<param name="checkValues" value="0"/>
<param name="checkNames" value="1"/>
<param name="checkWireStubs" value="1"/>
<param name="checkPolygonWidth" value="0"/>
<param name="useDiameter" value="13"/>
<param name="removeAnnularRing" value="0"/>
<param name="maxErrors" value="50"/>
</designrules>
</eagle>
'''
return rules_xml
# ---------------------------------------------------------------- EAGLE .dru
def gen_dru(vendor, rec):
tw = rec["minTraceWidth"]
sp = rec["minSpacing"]
dr = rec["minDrill"]
via = rec["minVia"]
c2e = g(rec, "copperToBoardEdge") or 0.2
h2h = g(rec, "holeToHole") or 0.2
arv = (g(rec, "annularRing", "viaMm")) or 0.1
arp = (g(rec, "annularRing", "padMm")) or 0.18
layers = rec["layerCount"]
pp = pad_pad_spacing(rec)
desc = description_text(vendor, rec).replace("\n", " | ")
lines = [
f"description[{desc}]",
"",
"layerSetup = " + LAYER_SETUPS[layers],
"mtCopper = " + " ".join(["0.035mm"] * 16),
"mtIsolate = 1.6mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm 0.15mm 0.2mm",
f"mdWireWire = {mil(sp['mm'])}mil",
f"mdWirePad = {mil(sp['mm'])}mil",
f"mdWireVia = {mil(sp['mm'])}mil",
f"mdPadPad = {mil(pp)}mil",
f"mdPadVia = {mil(sp['mm'])}mil",
f"mdViaVia = {mil(sp['mm'])}mil",
f"mdSmdPad = {mil(sp['mm'])}mil",
f"mdSmdVia = {mil(sp['mm'])}mil",
f"mdSmdSmd = {mil(sp['mm'])}mil",
f"mdCopperDimension = {mil(c2e)}mil",
f"mdDrill = {mil(h2h)}mil",
f"msWidth = {mil(tw['mm'])}mil",
f"msDrill = {mil(dr['mm'])}mil",
"mdViaViaSameLayer = 6mil",
"mdSmdStop = 0mil",
"msMicroVia = 12mil",
"msBlindViaRatio = 0.5",
"rvPadTop = 0",
"rvPadInner = 0",
"rvPadBottom = 0",
"rvViaOuter = 0",
"rvViaInner = 0",
"rvMicroViaOuter = 0",
"rvMicroViaInner = 0",
f"rlMinPadTop = {mil(arp)}mil",
"rlMaxPadTop = 80mil",
f"rlMinPadInner = {mil(arp)}mil",
"rlMaxPadInner = 20mil",
f"rlMinPadBottom = {mil(arp)}mil",
"rlMaxPadBottom = 80mil",
f"rlMinViaOuter = {mil(arv)}mil",
"rlMaxViaOuter = 20mil",
f"rlMinViaInner = {mil(arv)}mil",
"rlMaxViaInner = 20mil",
f"rlMinMicroViaOuter = {mil(arv)}mil",
"rlMaxMicroViaOuter = 20mil",
f"rlMinMicroViaInner = {mil(arv)}mil",
"rlMaxMicroViaInner = 20mil",
"psTop = -1",
"psBottom = -1",
"psFirst = -1",
"psElongationLong = 100",
"psElongationOffset = 100",
"mvStopFrame = 0",
"mvCreamFrame = 0",
"mlMinStopFrame = 1.5mil",
"mlMaxStopFrame = 4mil",
"mlMinCreamFrame = 0mil",
"mlMaxCreamFrame = 0mil",
"mlViaStopLimit = 20mil",
"srRoundness = 0",
"srMinRoundness = 0mil",
"srMaxRoundness = 0mil",
"slThermalIsolate = 10mil",
"slThermalsForVias = 0",
"dpMaxLengthDifference = 400mil",
"dpGapFactor = 2.5",
"checkAngle = 0",
"checkRestrict = 1",
"checkStop = 0",
"checkValues = 0",
"checkNames = 1",
"checkWireStubs = 1",
"checkPolygonWidth = 0",
"useDiameter = 13",
"maxErrors = 50",
"",
]
return "\n".join(lines)
# ---------------------------------------------------------------- KiCad
def gen_kicad_dru(vendor, rec):
tw = rec["minTraceWidth"]["mm"]
sp = rec["minSpacing"]["mm"]
dr = rec["minDrill"]["mm"]
via_d, via_p = effective_via(rec)
c2e = g(rec, "copperToBoardEdge")
silk = g(rec, "silkLineWidth")
mask = g(rec, "maskExpansion")
layers = rec["layerCount"]
desc = description_text(vendor, rec)
out = [
"(version 1)",
"",
"# " + desc.replace("\n", "\n# "),
"",
'(rule "min-track-width"',
f" (constraint track_width (min {fmm(tw)}mm))",
f" (layer \"{'*.Cu' if layers == 2 else '*.Cu'}\")",
" (condition \"A.type == 'track'\"))",
"",
'(rule "min-clearance"',
f" (constraint clearance (min {fmm(sp)}mm))",
" (condition \"A.type == 'track' && B.type == 'track'\"))",
"",
'(rule "min-hole"',
f" (constraint hole (min {fmm(dr)}mm))",
" (condition \"A.type == 'pad' || A.type == 'via'\"))",
"",
]
if via_p:
out += [
'(rule "min-via-diameter"',
f" (constraint via_diameter (min {fmm(via_p)}mm))",
" (condition \"A.type == 'via'\"))",
"",
]
if c2e:
out += [
'(rule "min-edge-clearance"',
f" (constraint edge_clearance (min {fmm(c2e)}mm))",
" (condition \"A.type == 'track'\"))",
"",
]
if silk:
out += [
'(rule "min-silk-thickness"',
f" (constraint text_thickness (min {fmm(silk)}mm))",
" (layer \"*.SilkS\")",
" (condition \"A.type == 'text' || A.type == 'graphic'\"))",
"",
]
return "\n".join(out)
def gen_kicad_board_setup(vendor, rec):
via_d, via_p = effective_via(rec)
setup = {
"vendor": vendor,
"process": rec["process"],
"layerCount": rec["layerCount"],
"boardSetupConstraints": {
"minTrackWidthMm": rec["minTraceWidth"]["mm"],
"minClearanceMm": rec["minSpacing"]["mm"],
"minViaDrillMm": via_d,
"minViaDiameterMm": via_p,
"minHoleMm": rec["minDrill"]["mm"],
"copperEdgeClearanceMm": g(rec, "copperToBoardEdge"),
"holeToHoleMm": g(rec, "holeToHole"),
"annularRingViaMm": g(rec, "annularRing", "viaMm"),
"annularRingPadMm": g(rec, "annularRing", "padMm"),
"silkMinLineWidthMm": g(rec, "silkLineWidth"),
"maskExpansionMm": g(rec, "maskExpansion"),
},
"notes": "Apply as Board Setup, Constraints. The matching .kicad_dru file carries the same limits as custom rules; the two are redundant on purpose so both the GUI constraints and kicad-cli DRC enforce them.",
"sources": rec.get("sources", []),
}
if rec.get("silkscreenPolicy"):
setup["silkscreenPolicy"] = rec["silkscreenPolicy"]
return json.dumps(setup, indent=2) + "\n"
# ---------------------------------------------------------------- Altium .RUL
def gen_rul(vendor, rec):
tw = rec["minTraceWidth"]
sp = rec["minSpacing"]
dr = rec["minDrill"]
via_d, via_p = effective_via(rec)
c2e = g(rec, "copperToBoardEdge")
arv = g(rec, "annularRing", "viaMm")
rules = [
("Width", "MinTraceWidth", "MinWidth=%gmm|PreferredWidth=%gmm|MaxWidth=25.4mm" % (tw["mm"], tw["mm"] * 2)),
("Clearance", "MinClearance", "Clearance=%gmm" % sp["mm"]),
]
if via_p:
rules.append(("RoutingViaStyle", "MinVia", "PreferredViaWidth=%gmm|PreferredViaHoleWidth=%gmm|MinViaWidth=%gmm|MaxViaWidth=6.35mm|MinViaHoleWidth=%gmm|MaxViaHoleWidth=6.35mm" % (via_p, via_d, via_p, via_d)))
rules.append(("HoleSize", "MinHole", "HoleWidth=%gmm" % dr["mm"]))
if arv:
rules.append(("AnnularRing", "MinAnnularRing", "MinAnnularRing=%gmm" % arv))
if c2e:
rules.append(("BoardOutlineClearance", "CopperToBoardEdge", "Clearance=%gmm" % c2e))
out = ["[Design Rules]", f"RuleCount={len(rules)}", ""]
for i, (kind, name, attrs) in enumerate(rules, 1):
out += [
f"[Rule{i}]",
f"RuleKind={kind}",
f"RuleName={name}",
"Scope1=All",
"Scope2=",
f"Attribute={attrs}",
"",
]
return "\n".join(out)
# ---------------------------------------------------------------- table
def mm_mil(v):
if not v:
return "n/p"
return f"{fmm(v['mm'])} / {v['mil']}"
def gen_table(records):
header = "| Vendor | Process | Layers | Min trace mm / mil | Min space mm / mil | Min drill mm / mil | Min via drill / pad mm | Annular ring via / pad mm | Cu to edge mm | Hole to hole mm | Source (read 2026-09-03) |"
sep = "|---|---|---|---|---|---|---|---|---|---|---|---|"
rows = [header, sep]
order = {v: i for i, v in enumerate([
"3rd party fab", "JLCPCB", "PCBWay", "OSH Park", "Seeed Fusion",
"Eurocircuits", "Aisler", "Elecrow", "Advanced Circuits", "Sierra Circuits",
"MacroFab", "NextPCB", "ALLPCB",
])}
for r in sorted(records, key=lambda r: (order.get(r["vendor"], 99), r["process"], r["layerCount"])):
via = r["minVia"]
ar = r.get("annularRing") or {}
src = r["sources"][0]["url"] if r.get("sources") else ""
via_d, via_p = effective_via(r)
rows.append(
"| {vendor} | {proc} | {layers} | {tw} | {sp} | {dr} | {via} | {ar} | {c2e} | {h2h} | {src} |".format(
vendor=r["vendor"], proc=r["process"], layers=r["layerCount"],
tw=mm_mil(r.get("minTraceWidth")), sp=mm_mil(r.get("minSpacing")),
dr=mm_mil(r.get("minDrill")),
via=(f"{fmm(via_d)} / {fmm(via_p)}" if via_d else "n/p"),
ar=(f"{fmm(ar.get('viaMm'))} / {fmm(ar.get('padMm')) if ar.get('padMm') else 'n/p'}" if ar and ar.get("viaMm") else "n/p"),
c2e=(fmm(g(r, "copperToBoardEdge")) if g(r, "copperToBoardEdge") else "n/p"),
h2h=(fmm(g(r, "holeToHole")) if g(r, "holeToHole") else "n/p"),
src=src,
))
return "\n".join(rows)
# ---------------------------------------------------------------- main
def main():
rec_files = sorted((ROOT / "rules" / "records").glob("*.json"))
if not rec_files:
sys.exit("no records found")
records = []
index = {"generatedBy": "tools/generate.py", "unitNote": "mm primary, mil secondary (1 mil = 0.0254 mm)", "vendors": []}
for rf in rec_files:
data = json.loads(rf.read_text())
for rec in data["records"]:
rec = dict(rec)
rec["vendor"] = data["vendor"]
rec["vendorSlug"] = data["vendorSlug"]
records.append(rec)
vs = file_slug(data["vendorSlug"], rec["process"], rec["layerCount"])
gen_ok = all(rec.get(k) for k in ("minTraceWidth", "minSpacing", "minDrill"))
if gen_ok:
(ROOT / "fusion" / f"{vs}.edru").write_text(gen_edru(data["vendor"], rec))
(ROOT / "fusion" / f"{vs}.dru").write_text(gen_dru(data["vendor"], rec))
(ROOT / "kicad" / f"{vs}.kicad_dru").write_text(gen_kicad_dru(data["vendor"], rec))
(ROOT / "kicad" / f"{vs}.board-setup.json").write_text(gen_kicad_board_setup(data["vendor"], rec))
(ROOT / "altium" / f"{vs}.RUL").write_text(gen_rul(data["vendor"], rec))
entry = {
"vendor": data["vendor"], "vendorSlug": data["vendorSlug"],
"process": rec["process"], "layerCount": rec["layerCount"],
"filesGenerated": gen_ok,
"files": {
"record": f"rules/records/{data['vendorSlug']}.json",
"fusionEdru": f"fusion/{vs}.edru",
"fusionDru": f"fusion/{vs}.dru",
"kicadDru": f"kicad/{vs}.kicad_dru",
"kicadBoardSetup": f"kicad/{vs}.board-setup.json",
"altiumRul": f"altium/{vs}.RUL",
},
"sources": rec.get("sources", []),
}
if not gen_ok:
entry["note"] = "No generated rule files: the vendor does not publish one or more of min trace width, min spacing, min drill for this process/layer count."
index["vendors"].append(entry)
(ROOT / "rules" / "index.json").write_text(json.dumps(index, indent=2) + "\n")
table = gen_table(records)
readme = (ROOT / "README.md").read_text()
new, n = re.subn(
r"<!-- TABLE:BEGIN -->.*?<!-- TABLE:END -->",
"<!-- TABLE:BEGIN -->\n" + table + "\n<!-- TABLE:END -->",
readme, flags=re.S)
if n:
(ROOT / "README.md").write_text(new)
print(f"generated {len(records)} record sets across {len(rec_files)} vendors")
if __name__ == "__main__":
main()