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.
KiCad: use valid hole_size constraint (55-file parser regression) #3
Scope: KiCad rule syntax only; no fabrication limits changed
The published generator emits (constraint hole (min ...)) in all 55 generated .kicad_dru files. Native KiCad 10 custom-rule parsing rejects every file with unknown constraint type 'hole'. The valid KiCad keyword is hole_size.
This draft makes exactly one source-code line change in tools/generate.py:
hole→hole_size
It changes no canonical vendor record, no minimum clearance, no drill dimension and no policy, and includes no regenerated production rule files. Its merge cannot silently publish or substitute a new PCBWay manufacturing profile. It is intentionally narrow and can be cherry-picked into the existing broader adom/pcb-design-rules draft PR #2 (global ID 98), if maintainers prefer one PR.
Reproduction
- Before: 55/55 generated KiCad DRUs invalid at parser.
- Scratch regenerated with only the one-line change: 55 valid, 0 invalid, 0 parser errors using the native ADOM KiCad bridge parser (KiCad 10.0.7).
- Source
rules/records/pcbway.jsonand every other canonical fab record unchanged. - Independent scratch validation log retained in PCB Design Studio alongside upstream issue #6.
NOT a complete manufacturing fix
This repair only makes the published generator emit syntactically loadable files. It does not prove KiCad enforces the source's annular-width, hole-to-hole, layer/copper-weight, mask, silk, or clearance-precedence requirements. ADOM's existing draft PR #2, issues #2, #4, #5 and #6 cover larger generator and manufacturer-source gaps. Those must be resolved with native DRC negative-control coupons and full source reconciliation before regenerated production DRUs are published.
The related UMCU21 project remains at Stage 2 Rules HOLD, with Stage 3 locked; no PCBWay production release is authorised.
Draft / review requested — do not merge this into a rules release without considering overlap with existing PR #2.
Diff Skip to comments
@@ -1,573 +1,574 @@-#!/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)", 8: "(1*2*3*4*13*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))",- " )" if vendor == "JLCPCB" else " (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")-+#!/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)", 8: "(1*2*3*4*13*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))", + " )" if vendor == "JLCPCB" else " (condition \"A.type == 'track' && B.type == 'track'\"))", + "", + '(rule "min-hole"', + f" (constraint hole_size (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() -if __name__ == "__main__":- main()
Comments
Log in to comment.
No comments yet.