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.
Preserve InstaPCB constraints in KiCad rules and derive legal via minima #2
The InstaPCB 2L KiCad file omits checks promised by its source profile, and its quoted minimum via pair violates the independently stated annular-ring minimum. Preserve the source values while deriving a legal via combination, carry the explicit 0.15 mm pad-pair limit, and export documented KiCad constraint syntax with copper/edge coverage, separate via/PTH rings and hole spacing.
Validation: eight source-contract regression tests pass. Current generator and record were re-read before editing. This draft has NOT been loaded into native KiCad with project rules. The shared DRC endpoint transports only the board, preventing that project-level check. Native negative-control fixtures and KiCad/Fusion/Altium parity are required before release.
Scope: focused candidate for the current 2L project. Altium pair-specific rules, 4L pad-pair source audit and the existing universal Fusion 0.15 mm clamp remain open. The board-setup JSON is explicitly described as an adapter contract rather than a native .kicad_pro file. Test with python3 tools/test_constraints.py. No installed bridge or shared production profile was replaced.
Diff Skip to comments
@@ -1,571 +1,562 @@⋯ 63 unchanged lines ⋯ drill = via.get("drillMm") if drill is None and rec.get("minDrill"): drill = rec["minDrill"]["mm"]+ if drill is not None and rec.get("minDrill"):+ drill = max(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)+ # Each field is an independent minimum, not a guaranteed legal pair.+ ring = (rec.get("annularRing") or {}).get("viaMm")+ if drill is not None and ring is not None:+ required = round(drill + 2 * ring, 4)+ pad = max(pad or 0, required) return drill, pad ⋯ 293 unchanged lines ⋯ # ---------------------------------------------------------------- KiCad def gen_kicad_dru(vendor, rec):+ """Export source constraints for KiCad 9/10; native DRC qualification is separate.""" 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'\"))",- "",- ]+ out = ["(version 1)", "", "# " + description_text(vendor, rec).replace("\n", "\n# "), ""]++ def rule(name, kind, minimum, condition=None, layer=None):+ if minimum is None:+ return+ rows = [f'(rule "{name}"', f' (constraint {kind} (min {fmm(minimum)}mm))']+ if layer:+ rows.append(f' (layer "{layer}")')+ if condition:+ rows.append(f' (condition "{condition}")')+ rows[-1] += ")"+ out.extend(rows + [""])++ # Unconditional electrical clearance covers tracks, arcs, pads, vias and zones.+ # KiCad evaluates clearance on different nets; do not add type filters here.+ rule("min-track-width", "track_width", tw)+ rule("min-clearance", "clearance", sp)+ # Later specific rules override earlier general rules in KiCad.+ pp = g(rec, "minPadSpacing")+ if pp is not None:+ rule("min-pad-clearance", "clearance", max(sp, pp), "A.Type == 'Pad' && B.Type == 'Pad'")+ rule("min-hole", "hole_size", dr)+ rule("min-via-hole", "hole_size", via_d, "A.Type == 'Via'")+ rule("min-via-diameter", "via_diameter", via_p, "A.Type == 'Via'")+ rule("min-via-annular-width", "annular_width", g(rec, "annularRing", "viaMm"), "A.Type == 'Via'")+ rule("min-pad-annular-width", "annular_width", g(rec, "annularRing", "padMm"), "A.Type == 'Pad' && A.Pad_Type == 'Through-hole'")+ rule("min-edge-clearance", "edge_clearance", g(rec, "copperToBoardEdge"))+ rule("min-hole-to-hole", "hole_to_hole", g(rec, "holeToHole"))+ rule("min-silk-thickness", "text_thickness", g(rec, "silkLineWidth"), "A.Type == 'Text' || A.Type == 'Text Box'", "*.SilkS") return "\n".join(out) ⋯ 6 unchanged lines ⋯ "boardSetupConstraints": { "minTrackWidthMm": rec["minTraceWidth"]["mm"], "minClearanceMm": rec["minSpacing"]["mm"],+ "minPadClearanceMm": g(rec, "minPadSpacing"), "minViaDrillMm": via_d, "minViaDiameterMm": via_p, "minHoleMm": rec["minDrill"]["mm"],⋯ 4 unchanged lines ⋯ "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.",+ "notes": "Apply as Board Setup, Constraints. Apply the matching .kicad_dru alongside the board and project with the same stem. This JSON is an adapter contract, not a native .kicad_pro file. Separate pad/via rules and pad-pair clearance need custom rules. Confirm loaded source hashes and native negative-control findings before claiming enforcement.", "sources": rec.get("sources", []), } return json.dumps(setup, indent=2) + "\n"⋯ 124 unchanged lines ⋯ if __name__ == "__main__": main()+@@ -0,0 +1,28 @@+"""Source-contract regression tests; not a substitute for native DRC execution."""+import unittest,importlib.util,json,copy+from pathlib import Path+spec=importlib.util.spec_from_file_location('rules',Path(__file__).with_name('generate.py'));g=importlib.util.module_from_spec(spec);spec.loader.exec_module(g)+BASE=Path(__file__).resolve().parents[1]+REC=json.loads((BASE/'rules/records/adom-instapcb.json').read_text())['records'][0]+class Constraints(unittest.TestCase):+ def test_declared_via_minima_are_not_a_legal_pair(self):+ d,p=g.effective_via(REC);self.assertAlmostEqual(d,.15);self.assertAlmostEqual(p,.35);self.assertEqual(REC['minVia']['padMm'],.25)+ def test_ring_grows_with_drill(self):+ r=copy.deepcopy(REC);r['minVia']['drillMm']=.4;d,p=g.effective_via(r);self.assertGreaterEqual((p-d)/2+.00000001,r['annularRing']['viaMm'])+ def test_global_drill_minimum_cannot_be_weakened(self):+ r=copy.deepcopy(REC);r['minDrill']['mm']=.5;self.assertEqual(g.effective_via(r),(.5,.7))+ def test_no_ring_claim_when_source_absent(self):+ r=copy.deepcopy(REC);r.pop('annularRing');self.assertEqual(g.effective_via(r),(.15,.25))+ def test_scope_covers_non_track_copper_and_edges(self):+ text=g.gen_kicad_dru('Adom InstaPCB',REC)+ self.assertIn('(rule "min-clearance"\n (constraint clearance (min 0.1mm)))',text)+ self.assertIn('(rule "min-edge-clearance"\n (constraint edge_clearance (min 0.2mm)))',text)+ self.assertGreater(text.index('min-pad-clearance'),text.index('min-clearance'))+ def test_native_constraint_names_and_separate_ring_limits(self):+ t=g.gen_kicad_dru('Adom InstaPCB',REC);self.assertNotIn('(constraint hole ',t);self.assertIn('hole_size',t);self.assertIn('(constraint annular_width (min 0.18mm))',t);self.assertIn('(constraint annular_width (min 0.1mm))',t);self.assertIn('(constraint hole_to_hole (min 0.2mm))',t)+ def test_absent_pad_pair_source_does_not_invent_fab_requirement(self):+ r=copy.deepcopy(REC);r.pop('minPadSpacing');self.assertNotIn('min-pad-clearance',g.gen_kicad_dru('Other process',r))+ def test_board_setup_exports_sourced_pair_minimum(self):+ v=json.loads(g.gen_kicad_board_setup('Adom InstaPCB',REC));self.assertEqual(v['boardSetupConstraints']['minPadClearanceMm'],.15);self.assertIn('not a native .kicad_pro',v['notes'])+if __name__=='__main__':unittest.main(verbosity=2)+@@ -1,57 +1,164 @@⋯ 5 unchanged lines ⋯ { "process": "standard", "layerCount": 2,- "minTraceWidth": {"mm": 0.10, "mil": 3.94, "condition": "1 oz copper"},- "minSpacing": {"mm": 0.10, "mil": 3.94, "condition": "1 oz copper, different nets"},- "minDrill": {"mm": 0.15, "mil": 5.91},- "minVia": {"drillMm": 0.15, "drillMil": 5.91, "padMm": 0.25, "padMil": 9.84},- "annularRing": {"viaMm": 0.10, "viaMil": 3.94, "padMm": 0.18, "padMil": 7.09, "note": "0.25 mm or above preferred"},- "copperToBoardEdge": {"mm": 0.20, "mil": 7.87},- "holeToHole": {"mm": 0.20, "mil": 7.87},- "maskExpansion": {"mm": 0.038, "mil": 1.50},+ "minTraceWidth": {+ "mm": 0.1,+ "mil": 3.94,+ "condition": "1 oz copper"+ },+ "minSpacing": {+ "mm": 0.1,+ "mil": 3.94,+ "condition": "1 oz copper, different nets"+ },+ "minDrill": {+ "mm": 0.15,+ "mil": 5.91+ },+ "minVia": {+ "drillMm": 0.15,+ "drillMil": 5.91,+ "padMm": 0.25,+ "padMil": 9.84+ },+ "annularRing": {+ "viaMm": 0.1,+ "viaMil": 3.94,+ "padMm": 0.18,+ "padMil": 7.09,+ "note": "0.25 mm or above preferred"+ },+ "copperToBoardEdge": {+ "mm": 0.2,+ "mil": 7.87+ },+ "holeToHole": {+ "mm": 0.2,+ "mil": 7.87+ },+ "maskExpansion": {+ "mm": 0.038,+ "mil": 1.5+ }, "maskSliver": null, "silkLineWidth": null, "silkToCopper": null,- "minCopperClearance": {"mm": 0.10, "mil": 3.94, "condition": "different nets"},- "boardThicknessOptionsMm": [1.6],- "copperWeightOptionsOz": [1],+ "minCopperClearance": {+ "mm": 0.1,+ "mil": 3.94,+ "condition": "different nets"+ },+ "boardThicknessOptionsMm": [+ 1.6+ ],+ "copperWeightOptionsOz": [+ 1+ ], "surfaceFinishes": [], "maxBoardSize": null,- "drillRange": {"minMm": 0.15, "maxMm": 6.3, "note": "holes with diameter >= 6.3 mm are CNC routed from a smaller drilled hole; min non-plated hole 0.50 mm"},+ "drillRange": {+ "minMm": 0.15,+ "maxMm": 6.3,+ "note": "holes with diameter >= 6.3 mm are CNC routed from a smaller drilled hole; min non-plated hole 0.50 mm"+ }, "notes": "House baseline of the Adom InstaPCB fab service. Preferred values in the seed file: default class preferred track 0.2 mm, power class preferred 0.3 mm; preferred via hole 0.2 mm (0.25 mm in the shipped file). Min via allowed on 2-layer.", "sources": [- {"url": "adom/fusion-bridge addin/AdomBridge/resources/dru/Adom-InstaPCB-2-Layer.edru", "dateRead": "2026-09-03", "kind": "bundled-house-rule-file"}- ]+ {+ "url": "adom/fusion-bridge addin/AdomBridge/resources/dru/Adom-InstaPCB-2-Layer.edru",+ "dateRead": "2026-09-03",+ "kind": "bundled-house-rule-file"+ }+ ],+ "minPadSpacing": {+ "mm": 0.15,+ "mil": 5.91,+ "condition": "different-net pad pairs; Copper Clearance builtin_ruleid=3 and mdPadPad in house 2L EDRU",+ "sources": [+ {+ "url": "adom/fusion-bridge addin/AdomBridge/resources/dru/Adom-InstaPCB-2-Layer.edru",+ "dateRead": "2026-09-09",+ "kind": "bundled-house-rule-file"+ }+ ]+ } }, { "process": "standard", "layerCount": 4,- "minTraceWidth": {"mm": 0.09, "mil": 3.54, "condition": "1 oz copper, multilayer"},- "minSpacing": {"mm": 0.09, "mil": 3.54, "condition": "1 oz copper, multilayer, different nets; 3 mil acceptable in BGA fan-outs"},- "minDrill": {"mm": 0.15, "mil": 5.91},- "minVia": {"drillMm": 0.15, "drillMil": 5.91, "padMm": 0.25, "padMil": 9.84},- "annularRing": {"viaMm": 0.10, "viaMil": 3.94, "padMm": 0.18, "padMil": 7.09, "note": "0.25 mm or above preferred"},- "copperToBoardEdge": {"mm": 0.20, "mil": 7.87},- "holeToHole": {"mm": 0.20, "mil": 7.87},- "maskExpansion": {"mm": 0.038, "mil": 1.50},+ "minTraceWidth": {+ "mm": 0.09,+ "mil": 3.54,+ "condition": "1 oz copper, multilayer"+ },+ "minSpacing": {+ "mm": 0.09,+ "mil": 3.54,+ "condition": "1 oz copper, multilayer, different nets; 3 mil acceptable in BGA fan-outs"+ },+ "minDrill": {+ "mm": 0.15,+ "mil": 5.91+ },+ "minVia": {+ "drillMm": 0.15,+ "drillMil": 5.91,+ "padMm": 0.25,+ "padMil": 9.84+ },+ "annularRing": {+ "viaMm": 0.1,+ "viaMil": 3.94,+ "padMm": 0.18,+ "padMil": 7.09,+ "note": "0.25 mm or above preferred"+ },+ "copperToBoardEdge": {+ "mm": 0.2,+ "mil": 7.87+ },+ "holeToHole": {+ "mm": 0.2,+ "mil": 7.87+ },+ "maskExpansion": {+ "mm": 0.038,+ "mil": 1.5+ }, "maskSliver": null, "silkLineWidth": null, "silkToCopper": null,- "minCopperClearance": {"mm": 0.09, "mil": 3.54, "condition": "different nets"},- "boardThicknessOptionsMm": [1.6],- "copperWeightOptionsOz": [1],+ "minCopperClearance": {+ "mm": 0.09,+ "mil": 3.54,+ "condition": "different nets"+ },+ "boardThicknessOptionsMm": [+ 1.6+ ],+ "copperWeightOptionsOz": [+ 1+ ], "surfaceFinishes": [], "maxBoardSize": null,- "drillRange": {"minMm": 0.15, "maxMm": 6.3, "note": "holes with diameter >= 6.3 mm are CNC routed from a smaller drilled hole; min non-plated hole 0.50 mm"},+ "drillRange": {+ "minMm": 0.15,+ "maxMm": 6.3,+ "note": "holes with diameter >= 6.3 mm are CNC routed from a smaller drilled hole; min non-plated hole 0.50 mm"+ }, "notes": "BGA pad diameter >= 0.25 mm. BGA pad to trace clearance min 0.09 mm. Vias can be placed within BGA pads using filled and plated-over vias.", "sources": [- {"url": "adom/fusion-bridge addin/AdomBridge/resources/dru/Adom-InstaPCB-4-Layer.edru", "dateRead": "2026-09-03", "kind": "bundled-house-rule-file"}+ {+ "url": "adom/fusion-bridge addin/AdomBridge/resources/dru/Adom-InstaPCB-4-Layer.edru",+ "dateRead": "2026-09-03",+ "kind": "bundled-house-rule-file"+ } ] } ] }+
Comments
Log in to comment.
No comments yet.