#!/usr/bin/env python3 """Optionally scale only DSN clearance entries by an explicit factor. The bundled Freerouting 2.3.0 build reads KiCad 10 micrometre clearance values directly, so the safe default is 1.0 (no scaling). This utility is retained for reproducible experiments with other router builds; never apply a non-unity factor without comparing the imported physical rule values first. """ 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=1.0) args = parser.parse_args() if args.factor <= 0: raise ValueError("--factor must be positive") 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()