0
mirror of https://github.com/incutec-hw/OpenFC.git synced 2026-08-08 13:54:35 +00:00
Files
incutec-OpenFC/hardware/tools/set_edgecuts_width.py

149 lines
3.8 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
import shutil
import sys
from pathlib import Path
GR_START_RE = re.compile(r"^\s*\(gr_\w+\b")
LAYER_EDGE_CUTS = '(layer "Edge.Cuts")'
WIDTH_LINE_RE = re.compile(r"^(\s*\(width\s+)([-+]?[0-9]*\.?[0-9]+)(\s*\)\s*)$")
STROKE_START_RE = re.compile(r"\(\s*stroke\b")
def _format_width(width_mm: float) -> str:
# Match KiCad's typical compact formatting in saved files.
return f"{width_mm:g}"
def update_gr_block(lines: list[str], width_mm: float) -> tuple[list[str], int]:
if not any(LAYER_EDGE_CUTS in line for line in lines):
return lines, 0
out: list[str] = []
changes = 0
local_depth = 0
in_stroke = False
stroke_start_depth: int | None = None
width_str = _format_width(width_mm)
for line in lines:
if not in_stroke and STROKE_START_RE.search(line):
in_stroke = True
stroke_start_depth = local_depth
if in_stroke:
match = WIDTH_LINE_RE.match(line)
if match:
new_line = f"{match.group(1)}{width_str}{match.group(3)}"
if new_line != line:
line = new_line
changes += 1
out.append(line)
local_depth += line.count("(") - line.count(")")
if in_stroke and stroke_start_depth is not None and local_depth <= stroke_start_depth:
in_stroke = False
stroke_start_depth = None
return out, changes
def update_file(contents: list[str], width_mm: float) -> tuple[list[str], int]:
out: list[str] = []
changes = 0
global_depth = 0
in_block = False
block_start_depth: int | None = None
block_lines: list[str] = []
def flush_block() -> None:
nonlocal changes, block_lines
updated, block_changes = update_gr_block(block_lines, width_mm)
out.extend(updated)
changes += block_changes
block_lines = []
for line in contents:
if not in_block and GR_START_RE.match(line):
in_block = True
block_start_depth = global_depth
block_lines = [line]
elif in_block:
block_lines.append(line)
else:
out.append(line)
global_depth += line.count("(") - line.count(")")
if in_block and block_start_depth is not None and global_depth == block_start_depth:
in_block = False
block_start_depth = None
flush_block()
if in_block:
raise ValueError("Unclosed gr_* block while parsing; file may be malformed.")
return out, changes
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description='Set stroke width for gr_* objects on the "Edge.Cuts" layer in a KiCad .kicad_pcb file.',
)
parser.add_argument("pcb", type=Path, help="Path to .kicad_pcb file")
parser.add_argument("--width-mm", type=float, default=0.05, help="Target stroke width in mm (default: 0.05)")
parser.add_argument(
"--write",
action="store_true",
help="Write changes back to the PCB file (default is dry-run)",
)
parser.add_argument(
"--no-backup",
action="store_true",
help="Do not create a .bak copy when using --write",
)
args = parser.parse_args(argv)
if args.width_mm <= 0:
print("--width-mm must be > 0", file=sys.stderr)
return 2
pcb_path: Path = args.pcb
if pcb_path.suffix != ".kicad_pcb":
print(f"warning: {pcb_path} does not end with .kicad_pcb", file=sys.stderr)
contents = pcb_path.read_text(encoding="utf-8").splitlines(keepends=True)
updated, changes = update_file(contents, args.width_mm)
if changes == 0:
print("No Edge.Cuts stroke widths needed updating.")
return 0
if not args.write:
print(f"Would update {changes} stroke width line(s). Re-run with --write to apply.")
return 0
if not args.no_backup:
backup_path = pcb_path.with_suffix(pcb_path.suffix + ".bak")
if backup_path.exists():
print(f"Backup already exists: {backup_path}", file=sys.stderr)
return 2
shutil.copy2(pcb_path, backup_path)
pcb_path.write_text("".join(updated), encoding="utf-8")
print(f"Updated {changes} stroke width line(s) in {pcb_path}.")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))