#!/usr/bin/env python3 """Compensate Freerouting 2.3.0's KiCad-10 DSN clearance scale mismatch. KiCad exports widths and clearances in micrometres. Freerouting 2.3.0 keeps the widths but internally imports DSN clearance values at one quarter of the intended physical value. Scaling only ``(clearance N)`` entries by four makes the router's physical rules match the source KiCad board. Coordinates, track widths, vias, drills, padstacks, and keepouts are deliberately untouched. """ from __future__ import annotations import argparse import re from pathlib import Path CLEARANCE = re.compile(r"(\(clearance\s+)(\d+(?:\.\d+)?)") def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("input", type=Path) parser.add_argument("output", type=Path) parser.add_argument("--factor", type=float, default=4.0) args = parser.parse_args() source = args.input.read_text() original = [float(match.group(2)) for match in CLEARANCE.finditer(source)] if not original: raise RuntimeError(f"No clearance rules found in {args.input}") def scale(match: re.Match[str]) -> str: value = float(match.group(2)) * args.factor rendered = str(int(value)) if value.is_integer() else f"{value:g}" return match.group(1) + rendered converted, count = CLEARANCE.subn(scale, source) scaled = [float(match.group(2)) for match in CLEARANCE.finditer(converted)] assert count == len(original) == len(scaled) assert all(after == before * args.factor for before, after in zip(original, scaled, strict=True)) # A conversion must never touch the board geometry or trace-width rules. assert source.count("(width ") == converted.count("(width ") assert source.count("(polygon ") == converted.count("(polygon ") args.output.write_text(converted) print(f"Scaled {count} clearance rules by {args.factor:g}x") print(f"Saved {args.output}") if __name__ == "__main__": main()