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.
PCBWay 4L 1oz sourced rules and KiCad export correctness (DRAFT; supersedes #4/#5 content; reconciles #2 generator; merge blocked) #6
DRAFT, NOT FOR MERGE. Merge is blocked by the items under "Merge blockers". This proposes source files only. No regenerated production .kicad_dru files are included.
What this changes (3 files)
tools/generate.py: KiCad export for every vendor. Useshole_size(nothole, which KiCad 10 rejects in all 55 published files). Every emitted rule is an explicit(severity error), because hole-to-hole and silk were reported as non-gating warnings. Via and PTH annular rings are separate rules: viaA.Type == 'Via', PTH padA.Type == 'Pad' && A.Pad_Type == 'Through-hole'. Hole-to-hole uses the record value. Inner hole-to-copper (hole_clearance, no layer qualifier) and silk height (text_height) are emitted only when the record carries them. The rest of the generator is main, unchanged.rules/records/pcbway.json: PCBWay standard 4-layer 1 oz record only (see values below).tools/test_pcbway_4l.py: 10 source-conformance tests on the generated 4-layer rules.
Relation to open drafts: this supersedes the content of #4 (syntax-only, one line; included here) and #5 (record values; included here). It reconciles the generator functions of #2 (global 98). #2's InstaPCB record and its test_constraints.py are not included and remain in #2. Note: #2's exact-text assertions for min-clearance and min-edge-clearance were updated here to include the explicit (severity error) line. That is an intentional change to #2's test contract.
Sources (archived 2026-11-02, SHA-256 of archived bytes)
- https://www.pcbway.com/capabilities.html: 694ce30266e8c0bde8173d590c3ac71e6d928eb730fad3555e6a1dd298e0a980 (footer "© 2024"; no revision date)
- https://www.pcbway.com/helpcenter/Engineering_Questions/limited_holes_spacing.html: 9cebdeed859c3c61d4e104185aa5e6a54fb1a5c6ac5864591fbd5993479145e1 (page states last updated 2025-06-25)
- https://www.pcbway.com/pcb_prototype/PCB_Manufacturing_tolerances.html: 51142afe99832cc0ee41af19bf7d4c552a2658da466f04850523fab09bb66948
- https://m.pcbway.com/capabilities.html (summary, observational): d7be6170136dad3df83304c5e34dab85775f7df80e632d8de8eae85969da3357
Values (mm): track 0.127; clearance 0.1524 (both outer 1 oz values, applied to all layers); hole 0.2; via ring 0.15; PTH component ring 0.254; via pad 0.5 (derived 0.2 + 2 x 0.15); hole-to-hole 0.45 (all pairs); inner hole-to-copper 0.1778; copper-to-edge 0.25; silk width 0.15, height 0.8.
Source disagreements (recorded, stricter value chosen, no weaker value taken)
- PTH ring: capabilities 10 mil (0.254) vs tolerances 0.20 -> 0.254.
- Hole spacing: capabilities 16 mil (0.4064 PTH) and 11 mil (0.2794 via) vs limited-holes page 0.45 PTH and 0.30 via -> 0.45 for all pairs. The via-to-via 0.30 is looser; applying 0.45 is conservative and over-restrictive for via arrays.
- Drill: capabilities 0.15 (surcharge below 0.2) vs mobile 0.2 (surcharge below 0.3) -> 0.2 satisfies both.
- Via annular ring: 0.15 on both pages (agreement).
Verified (evidence attached by hash in the review folder)
- Parser: all 57 generated
.kicad_drufiles (55 vendor + 2 InstaPCB outputs) pass KiCad 10 custom-rule parsing: 57/57 valid, 0 errors. SHA-256 of validation record: d0548e8a70ee3b77f4af003a688fa00f30a5b64c3b6e5b6cda0b468733b7c5e7. - Tests:
test_constraints.py8/8 (from #2 on the reconciled tree) andtest_pcbway_4l.py10/10. - Native DRC on coupons (KiCad 10 via bridge, zone refill on). Violating coupon cpn_v4 (sha256 of .kicad_pcb e2d4ff74...): named error categories fired for via annular, PTH annular, hole-to-inner-copper, clearance (track and pad pairs), copper edge, hole-to-hole (x2), via hole size, silk thickness, silk height, track width, via diameter. Compliant coupon cpn_v4c (sha256 25722c7a...): zero rule-named violations. Evidence: drc-cpn_v4-reconciled.json (db118c4a...), drc-cpn_v4c-reconciled.json (9ade2f0c...).
- Generated 4-layer file sha256 0a78e98068efa90f8c69f61b918945c4211123bc21e957ad5885154562f9890b.
Merge blockers (explicit; do not merge until resolved)
- Pad-to-zone clearance is NOT verified. The coupon pad 0.10 mm from a filled pour produced no violation, even with an explicit filled polygon. The pour may not be evaluated in this DRC setting. Needs a reviewed explanation or fix before this is claimed.
- Solder-mask bridge is NOT verified. Setting
board.design_settings.rules.solder_mask_min_widthto 0.1016 did not produce a violation at a 0.08 mm gap.solder_mask_bridgeis not a custom-rule constraint. - Netclass-conditioned rules: none are emitted. Not tested. Issue #5 (ADOM) notes unconditioned clearance can reduce a stricter zone or netclass spacing. This record has no netclass-specific limits.
- Board-side
rules checktreats the project.kicad_druas present-only: a deliberately invalid file passed. This is outside this PR and should go to issue #6. - Maintainers must regenerate outputs after merge. Every emitted rule gains
(severity error), so all published files change. - The record is single-source for the 0.45 mm hole spacing and the 0.254 mm ring beyond the capabilities page; the limited-holes page is the official PCBWay 2025 source. Needs maintainer confirmation.
🤖 Generated with Claude Code
Diff Skip to comments (2)
@@ -1,573 +1,565 @@⋯ 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))",- " )" 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'\"))",- "",- ]+ out = ["(version 1)", "", "# " + description_text(vendor, rec).replace("\n", "\n# "), ""]++ def rule(name, kind, minimum, condition=None, layer=None, severity=None):+ if minimum is None:+ return+ rows = [f'(rule "{name}"', f' (severity {severity or "error"})', 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"))+ # KiCad reports hole_to_hole as a warning unless the rule states severity: a warning does not fail a board+ rule("min-hole-to-hole", "hole_to_hole", g(rec, "holeToHole"), severity="error")+ rule("min-hole-to-inner-copper", "hole_clearance", g(rec, "minHoleToInnerCopper", "mm"))+ rule("min-silk-height", "text_height", g(rec, "silkMinHeightMm", "mm"), "A.Type == 'Text' || A.Type == 'Text Box'", "*.SilkS")+ 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", []), }- if rec.get("silkscreenPolicy"):- setup["silkscreenPolicy"] = rec["silkscreenPolicy"] return json.dumps(setup, indent=2) + "\n" ⋯ 122 unchanged lines ⋯ if __name__ == "__main__": main()+@@ -0,0 +1,50 @@+"""PCBWay standard 4-layer 1 oz: sourced limits must appear in the generated KiCad rules, each as an explicit error.+Source archive SHA-256 values are the ones recorded in the PCBDS r7 proof. Syntax-only passes are NOT accepted as proof: see DRC coupon evidence."""+import unittest, importlib.util, json, re+from pathlib import Path+BASE = Path(__file__).resolve().parents[1]+spec = importlib.util.spec_from_file_location('rules', BASE / 'tools' / 'generate.py')+g = importlib.util.module_from_spec(spec); spec.loader.exec_module(g)+REC = [r for r in json.loads((BASE / 'rules/records/pcbway.json').read_text())['records'] if r['process'] == 'standard' and r['layerCount'] == 4][0]+TXT = g.gen_kicad_dru('PCBWay', REC)+STD_SHA = '694ce30266e8c0bde8173d590c3ac71e6d928eb730fad3555e6a1dd298e0a980'+LIM_SHA = '9cebdeed859c3c61d4e104185aa5e6a54fb1a5c6ac5864591fbd5993479145e1'+TOL_SHA = '51142afe99832cc0ee41af19bf7d4c552a2658da466f04850523fab09bb66948'+def rule(name):+ m = re.search(r'\(rule "%s"\n(.*?)(?:\n\n|\Z)' % re.escape(name), TXT, flags=re.S)+ return m.group(1) if m else None+class PCBWay4L(unittest.TestCase):+ def test_no_invalid_keyword_or_layer_qualifier(self):+ self.assertNotIn('(constraint hole ', TXT)+ self.assertNotIn('(layer "inner")', TXT)+ def test_every_rule_is_explicit_error(self):+ blocks = re.findall(r'\(rule "[^"]+"\n(.*?)(?:\n\n|\Z)', TXT, flags=re.S)+ self.assertTrue(blocks)+ for b in blocks: self.assertIn('(severity error)', b)+ def test_track_and_spacing_outer_1oz_applied_globally(self):+ self.assertIn('(constraint track_width (min 0.127mm))', rule('min-track-width'))+ self.assertIn('(constraint clearance (min 0.1524mm))', rule('min-clearance'))+ def test_via_and_pth_annular_rings_are_separate(self):+ self.assertIn("A.Type == 'Via'", rule('min-via-annular-width'))+ self.assertIn('(constraint annular_width (min 0.15mm))', rule('min-via-annular-width'))+ self.assertIn("A.Pad_Type == 'Through-hole'", rule('min-pad-annular-width'))+ self.assertIn('(constraint annular_width (min 0.254mm))', rule('min-pad-annular-width'))+ def test_hole_spacing_at_least_0_45mm_for_all_pairs(self):+ self.assertIn('(constraint hole_to_hole (min 0.45mm))', rule('min-hole-to-hole'))+ self.assertIsNone(re.search(r'hole_to_hole[^\n]*condition', TXT))+ def test_inner_hole_to_copper_7mil_is_emitted(self):+ self.assertIn('(constraint hole_clearance (min 0.1778mm))', rule('min-hole-to-inner-copper'))+ def test_silk_height_and_width(self):+ self.assertIn('(constraint text_height (min 0.8mm))', rule('min-silk-height'))+ self.assertIn('(constraint text_thickness (min 0.15mm))', rule('min-silk-thickness'))+ def test_copper_edge_and_drill_and_via_diameter(self):+ self.assertIn('(constraint edge_clearance (min 0.25mm))', rule('min-edge-clearance'))+ self.assertIn('(constraint hole_size (min 0.2mm))', rule('min-hole'))+ self.assertIn('(constraint via_diameter (min 0.5mm))', rule('min-via-diameter'))+ def test_record_carries_source_archive_hashes(self):+ shas = [s.get('archiveSha256') for s in REC.get('sources', [])]+ for need in (STD_SHA, LIM_SHA, TOL_SHA): self.assertIn(need, shas)+ def test_pth_ring_is_strictest_of_sources(self):+ self.assertGreaterEqual(REC['annularRing']['padMm'], 0.254) # tolerances page 0.20 is weaker and is not used+if __name__ == '__main__': unittest.main(verbosity=2)+@@ -1,616 +1,653 @@⋯ 105 unchanged lines ⋯ "process": "standard", "layerCount": 4, "minTraceWidth": {- "mm": 0.1016,- "mil": 4.0,- "condition": "inner layer, 1oz (35um) copper, before compensation; inner 0.5oz: 4mil, 2oz: 6mil, 3oz: 8mil. Outer layer 1oz: 5mil"- },- "minSpacing": { "mm": 0.127, "mil": 5.0,- "condition": "inner layer, 1oz (35um) copper, before compensation; inner 0.5oz: 5mil (4/4mil class), 2oz: 8mil, 3oz: 11mil. Outer layer 1oz: 6mil"+ "condition": "4L 1oz: outer conventional 5mil (0.127) applied to every layer; inner 4mil (0.1016) is looser, not used. Sources: capabilities table (std sha 694ce30266e8); mobile page suggests 6mil. " },+ "minSpacing": {+ "mm": 0.1524,+ "mil": 6.0,+ "condition": "4L 1oz: outer conventional 6mil (0.1524) applied to every layer; inner 5mil (0.127) looser, not used. Sources: capabilities table (std sha 694ce30266e8); mobile page suggests 6mil."+ }, "minDrill": { "mm": 0.2, "mil": 7.87⋯ 1 unchanged line ⋯ "minVia": { "drillMm": 0.2, "drillMil": 7.87,- "padMm": 0.454,- "padMil": 17.87+ "padMm": 0.5,+ "padMil": 19.69,+ "note": "derived: drill 0.2 + 2 x 0.15 via ring (vendor page does not publish a via pad)" }, "annularRing": {- "viaMm": 0.127,- "viaMil": 5.0,- "note": "via, 1oz outer copper; component hole 1oz: 10mil"+ "viaMm": 0.15,+ "viaMil": 6.0,+ "padMm": 0.254,+ "padMil": 10.0,+ "note": "viaMm: pads with vias, 0.15mm (6mil): capabilities page AND manufacturing-tolerances page (0.15 via ring) agree. padMm: 1oz outer component-hole ring 10mil (0.254) from the capabilities table; the manufacturing-tolerances page gives 0.20 for the PTH ring, which is WEAKER and not used. Source disagreement recorded in notes." }, "copperToBoardEdge": { "mm": 0.25,⋯ 1 unchanged line ⋯ "condition": "line to board edge, CNC routing" }, "holeToHole": {- "mm": 0.2794,- "mil": 11.0,- "condition": "via hole to via hole, via diameter 0.45mm or less; component holes: 16mil (0.4064mm)"+ "mm": 0.45,+ "mil": 17.72,+ "condition": "different-net plated holes: 0.45mm (limited_holes_spacing page, last updated 2025-06-25) - stricter than the capabilities table (16mil = 0.4064 PTH; 11mil = 0.2794 via). Via-to-via 0.30mm on that page is looser than 0.45 and is not used separately: 0.45 applies to all pairs (conservative)." }, "maskExpansion": { "mm": 0.0508,⋯ 49 unchanged lines ⋯ { "url": "https://www.pcbway.com/capabilities.html", "dateRead": "2026-09-03"+ },+ {+ "url": "https://www.pcbway.com/capabilities.html",+ "dateRead": "2026-11-02",+ "archiveSha256": "694ce30266e8c0bde8173d590c3ac71e6d928eb730fad3555e6a1dd298e0a980",+ "note": "observational archive; page footer '© 2024 PCBWay' only; no vendor revision date"+ },+ {+ "url": "https://www.pcbway.com/helpcenter/Engineering_Questions/limited_holes_spacing.html",+ "dateRead": "2026-11-02",+ "archiveSha256": "9cebdeed859c3c61d4e104185aa5e6a54fb1a5c6ac5864591fbd5993479145e1",+ "note": "official PCBWay Engineering Questions page; states last updated 2025-06-25"+ },+ {+ "url": "https://www.pcbway.com/pcb_prototype/PCB_Manufacturing_tolerances.html",+ "dateRead": "2026-11-02",+ "archiveSha256": "51142afe99832cc0ee41af19bf7d4c552a2658da466f04850523fab09bb66948",+ "note": "official PCBWay tolerances page; PTH ring 0.20 and via ring 0.15 (PTH ring weaker than capabilities table, not used)"+ },+ {+ "url": "https://m.pcbway.com/capabilities.html",+ "dateRead": "2026-11-02",+ "archiveSha256": "d7be6170136dad3df83304c5e34dab85775f7df80e632d8de8eae85969da3357",+ "note": "mobile capabilities page (summary only): drill 0.2 min, trace/spacing 4mil min with 6mil suggested, annular 0.15" }- ]+ ],+ "minHoleToInnerCopper": {+ "mm": 0.1778,+ "mil": 7.0,+ "condition": "4-layer inner-layer isolation ring (hole to inner layer conductor, before compensation) 7mil, per capabilities table 'Minimum isolation ring of Inner layer' 4L >= 7mil (archive sha 694ce302...)"+ },+ "silkMinHeightMm": {+ "mm": 0.8,+ "condition": "minimum legend character height 0.8mm (width:height 1:5), per capabilities page silkscreen row (archive sha 694ce302...)"+ } }, { "process": "standard",⋯ 412 unchanged lines ⋯ "https://www.pcbway.com/advanced-pcb.html" ] }+
Comments
Log in to comment.