Copper design-rule check
Stage 4 of the ECAD campaign is the copper DRC — the geometric check that turns a board’s copper into a pass/fail report against a rule table. It is a region query the exact 2D machinery answers with no tolerance: clearance is grow each net’s copper by half the clearance and require the grown regions disjoint, where an empty intersection PROVES the clearance (the same tamper-mesh construction the anti-drill mesh rests on), measured against a closed form rather than eyeballed.
The load-bearing rule: the netlist decides what should connect
A short is copper of different nets electrically connected; copper of the same net touching is the intended connection and is never flagged. That is the one-declaration rule doing real work — the geometry and the netlist have to agree, and a DRC that could not tell an intended join from a short would be useless. Because a pad’s net is its pin’s net (stage 2’s pin↔pad identity), the DRC reads the schematic to know which is which.
The rules
DrcRuleSet is the standard PCB rule table — minimum copper-to-copper clearance, trace
width, annular ring, drill-to-copper, copper-to-board-edge, and an acute-angle /
acid-trap threshold — all in the model’s millimetres. The Default values are nominal
IPC-2221-ish figures; ⚠ verify them against your fabricator’s capability sheet, exactly as
StandardHoles and SheetMaterials are flagged. Because every threshold is relative, a rule
set and a board that pass still pass after a uniform scale of both (DrcRuleSet.Scaled).
PcbDrc.Check(layout, rules) returns a DrcReport that names, locates and measures every
violation (a report that only said “fail” would be useless — the PcbLayoutCheck house style), and
lists the ratsnest — signal nets the copper does not yet connect — as information, not a
fault, since a bare-pads board before routing is unrouted, not wrong.
Multi-layer, and where traces come in
Clearance, shorts, trace width and acute angles are per layer (top copper does not clear
against bottom copper in-plane); drill-to-copper is cross-layer, since a drill goes through the
whole stack. Trace width and the acid-trap rule genuinely want conductors — the copper today is
pads, so those rules run on whatever copper a layer carries (a deliberately-thin pad, a sharp
corner) and fully engage once stage-5 routing produces traces, since a trace is a stroked
centre-line region through the same CopperFeature type the DRC already reads.
// --- a placed board (the stage-2 fixture), checked clean with an unrouted ratsnest ---var resistor = new PartDefinition("R_0805", "R", new[] { new Pin("1", PinType.Passive), new Pin("2", PinType.Passive) }, new Footprint("R0805", new[] { Pad.Smd("1", new Vector2d(-1.0, 0), 1.2, 1.4), Pad.Smd("2", new Vector2d(1.0, 0), 1.2, 1.4), }));var header = new PartDefinition("HDR_1x2", "J", new[] { new Pin("1", PinType.Passive), new Pin("2", PinType.Passive) }, new Footprint("HDR254", new[] { Pad.ThroughHole("1", new Vector2d(-1.27, 0), pad: 1.6, drill: 0.9), Pad.ThroughHole("2", new Vector2d(1.27, 0), pad: 1.6, drill: 0.9), }));
var sch = new Schematic("blinky");var r = sch.Add("R1", resistor, "330");var j = sch.Add("J1", header);sch.Connect("VCC", j.Pin("1"), r.Pin("1"));sch.Connect("SIG", r.Pin("2"), j.Pin("2"));
var board = PcbBoard.Rectangle(50, 40, 1.6);var layout = new PcbLayout(sch, board);layout.Place("R1", 6, 0);layout.Place("J1", -8, 0, rotationDegrees: 90);
var clean = PcbDrc.Check(layout);Console.WriteLine($"placed board: {clean.Violations.Count} violations, ratsnest = [{string.Join(", ", clean.Ratsnest)}]");// -> 0 violations; VCC and SIG are unrouted (pads only, no traces) — information, not a fault.
// --- a board with a deliberate clearance violation AND a short ---// Ø1 pads (radius 0.5). A→B centres 1.10 apart => 0.10 gap < 0.15 clearance (a near miss).// C and D overlap (centres 0.5 apart) => a short between two different nets.CopperFeature Pad2(string net, string src, double x, double y) => new("Top", net, src, CurvedRegion2d.Disc(new Vector2d(x, y), 0.5));
var rules = DrcRuleSet.Default; // 0.15 mm clearance, etc.var bad = new PcbCopperModel(board, new[] { Pad2("VCC", "R1.1", 0, 0), Pad2("GND", "R2.1", 1.10, 0), // clearance Pad2("NET3", "R3.1", 10, 0), Pad2("NET4", "R4.1", 10.5, 0), // short});
var report = PcbDrc.Check(bad, rules);foreach (var v in report.Violations) Console.WriteLine($" {v.Rule}: {v.Message}");// The clearance violation's measured gap matches the closed form (1.10 - 1.0 = 0.10).var near = report.OfRule(DrcRule.Clearance).First();Console.WriteLine($"clearance measured {near.Measured:g3} mm against {near.Required:g3} mm required");
// --- move the near miss a hair apart: it passes (the empty-intersection proof) ---var good = new PcbCopperModel(board, new[] { Pad2("VCC", "R1.1", 0, 0), Pad2("GND", "R2.1", 1.16, 0), // 0.16 gap >= clearance});Console.WriteLine($"a hair further apart: {PcbDrc.Check(good, rules).Violations.Count} violations");IPC-6012 class presets, and cross-checking a spec against its class
DrcRuleSet.ForIpcClass(1|2|3) is a preset for an IPC-6012 performance class, following the IPC
producibility levels (Level A ↔ class 1, Level B ↔ class 2, Level C ↔ class 3). A DRC minimum is a
floor the design must clear, and a stricter class REQUIRES more copper — a larger clearance,
annular ring and edge keep-out — so every minimum grows with the class and class 3 is the
strictest (the DRC flags progressively more). That is the IPC-6012 direction for a minimum annular
ring exactly (Level C leaves the most copper). Class 2 is field-identical to the Class-2-ish
Default, so the preset spreads around it. ⚠ These are nominal transcribed figures — verify
against your fabricator’s capability sheet (flagged like StandardHoles / SheetMaterials /
Default); the class→level mapping is a nominal convention.
Because a preset is an ordinary DrcRuleSet, it drives PcbDrc with no change to the check — a
gap that clears class 2 fails the stricter class 3. And DrcRuleSet.CheckSpec(spec) cross-checks a
PcbFabricationSpec’s own stated minimum trace width / clearance against the
class it claims: a spec naming a strict class but stating a minimum LOOSER (finer) than that class’s
floor is inconsistent — it names the class yet permits features below its minimum — so the mismatch
is flagged, naming the stated value and the class minimum. A spec whose stated minimums meet its
class conforms; a spec that states no class, or a class but no minimum to compare, is
NotCheckable with a reason (never invented into a pass or a fail).
// The presets — higher class = stricter (larger required minimums). ⚠ nominal, verify-against-datasheet.var c1 = DrcRuleSet.ForIpcClass(1);var c2 = DrcRuleSet.ForIpcClass(2);var c3 = DrcRuleSet.ForIpcClass(3);Console.WriteLine($"min clearance 1/2/3 = {c1.MinCopperClearance} / {c2.MinCopperClearance} / {c3.MinCopperClearance} mm");Console.WriteLine($"min trace 1/2/3 = {c1.MinTraceWidth} / {c2.MinTraceWidth} / {c3.MinTraceWidth} mm");Console.WriteLine($"class 2 IS the Class-2-ish Default: {c2 == DrcRuleSet.Default}");
// A preset drives PcbDrc: a 0.18 mm gap PASSES class 2 (floor 0.15) and FAILS class 3 (floor 0.20).var board = PcbBoard.Rectangle(50, 40, 1.6);CopperFeature Pad(string net, double x) => new("Top", net, net + ".1", CurvedRegion2d.Disc(new Vector2d(x, 0), 0.5));var model = new PcbCopperModel(board, new[] { Pad("A", 0), Pad("B", 1.18) });Console.WriteLine($"0.18 mm gap: class 2 clearance violations = {PcbDrc.Check(model, c2).OfRule(DrcRule.Clearance).Count()}, class 3 = {PcbDrc.Check(model, c3).OfRule(DrcRule.Clearance).Count()}");
// Cross-check a spec's stated minimums against the class it claims.var spec = new PcbFabricationSpec { Ipc6012Class = 3, MinTraceWidthMm = 0.15, MinClearanceMm = 0.15 };var check = DrcRuleSet.CheckSpec(spec);Console.WriteLine($"spec claims class 3 with 0.15 mm mins: {check.Result}");foreach (var issue in check.Issues) Console.WriteLine($" - {issue}");
// No class stated => "not checkable", not a verdict invented from nothing.var noClass = DrcRuleSet.CheckSpec(new PcbFabricationSpec { MinTraceWidthMm = 0.15 });Console.WriteLine($"no class stated: {noClass.Result} ({noClass.Summary})");The incremental seam for routing
A stage-5 router costs a candidate route with PcbDrc.Violates(model, candidate, rules) — the
violations a single new CopperFeature would introduce against the model as it stands (clearance
and shorts on its layer, copper-to-edge, width and acute angles), without re-running the whole
board. Same-net copper the candidate joins is never a short, so a router that connects a net’s pads
is rewarded, not penalised — the ratsnest shrinks as the shorts stay clear.
Verification
The bar is higher than usual because ECAD fails plausibly (the tamper-mesh guarantee): a known clearance violation is found and a near miss at clearance + ε passes, both measured against the closed-form gap; a short names both nets while two same-net overlapping pads are never flagged; annular ring, copper-to-edge and drill-to-copper are checked from both sides of their limit; and a rule set and board that pass still pass after a uniform scale (relative, not absolute, tolerances). The clearance is proven directly — the grown regions’ intersection is asserted empty on a passing board and non-empty on a failing one.