#!/usr/bin/env python3 """ KiCad Footprint Logo Centering Script Automatically centers all fp_poly elements in a KiCad footprint to (0,0) and adds/updates courtyard layer with 0.1mm margin Supports wildcards and multiple files """ import re import sys import glob import os def center_footprint_logo(file_path): """Centers all fp_poly elements in a KiCad footprint file and manages courtyard""" with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # Find all coordinate pairs in fp_poly elements xy_pattern = r'\(xy (-?[\d.]+) (-?[\d.]+)\)' coordinates = re.findall(xy_pattern, content) if not coordinates: print("No coordinates found in file") return # Convert to floats and find bounds coords = [(float(x), float(y)) for x, y in coordinates] min_x = min(coord[0] for coord in coords) max_x = max(coord[0] for coord in coords) min_y = min(coord[1] for coord in coords) max_y = max(coord[1] for coord in coords) # Calculate center and offset center_x = (min_x + max_x) / 2 center_y = (min_y + max_y) / 2 offset_x = -center_x offset_y = -center_y print(f"Logo bounds: X=[{min_x:.3f}, {max_x:.3f}], Y=[{min_y:.3f}, {max_y:.3f}]") print(f"Current center: ({center_x:.3f}, {center_y:.3f})") print(f"Applying offset: ({offset_x:.3f}, {offset_y:.3f})") # Function to offset coordinates def offset_coord(match): x = float(match.group(1)) + offset_x y = float(match.group(2)) + offset_y return f"(xy {x:.6f} {y:.6f})" # Apply offset to all coordinates new_content = re.sub(xy_pattern, offset_coord, content) # Calculate new bounds after centering new_min_x = min_x + offset_x new_max_x = max_x + offset_x new_min_y = min_y + offset_y new_max_y = max_y + offset_y # Check for existing courtyard (simple line-based approach) lines = new_content.split('\n') courtyard_lines = [i for i, line in enumerate(lines) if 'F.CrtYd' in line] replace_courtyard = False if courtyard_lines: print(f"\n⚠️ Found {len(courtyard_lines)} courtyard line(s) (F.CrtYd references):") for i in courtyard_lines[:3]: # Show max 3 line numbers print(f" Line {i+1}: {lines[i].strip()[:60]}...") if len(courtyard_lines) > 3: print(f" ... and {len(courtyard_lines) - 3} more") response = input("Remove ALL courtyard elements? (y/N): ").lower() replace_courtyard = response in ['y', 'yes'] if not replace_courtyard: print("Keeping existing courtyards") # Write back to file without courtyard changes with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) print(f"✅ Logo centered in {file_path}") print(f" Final logo bounds: X=[{new_min_x:.3f}, {new_max_x:.3f}], Y=[{new_min_y:.3f}, {new_max_y:.3f}]") return if not courtyard_lines or replace_courtyard: # Calculate courtyard with 0.1mm margin margin = 0.1 courtyard_min_x = new_min_x - margin courtyard_max_x = new_max_x + margin courtyard_min_y = new_min_y - margin courtyard_max_y = new_max_y + margin # Create new courtyard courtyard_rect = f''' (fp_rect (start {courtyard_min_x:.6f} {courtyard_min_y:.6f}) (end {courtyard_max_x:.6f} {courtyard_max_y:.6f}) (stroke (width 0.05) (type solid)) (fill none) (layer "F.CrtYd"))''' if courtyard_lines and replace_courtyard: # Remove all lines containing F.CrtYd and related fp_rect structures lines = new_content.split('\n') filtered_lines = [] skip_next = False for i, line in enumerate(lines): if 'F.CrtYd' in line: # Skip this line and look backwards to remove the fp_rect start j = len(filtered_lines) - 1 while j >= 0 and '(fp_rect' not in filtered_lines[j]: j -= 1 if j >= 0 and '(fp_rect' in filtered_lines[j]: filtered_lines = filtered_lines[:j] # Remove fp_rect start too elif line.strip().startswith('(stroke') or line.strip().startswith('(fill'): # Skip stroke/fill lines that belong to removed fp_rect if i > 0 and any('F.CrtYd' in lines[k] for k in range(max(0, i-3), min(len(lines), i+3))): continue else: filtered_lines.append(line) else: filtered_lines.append(line) new_content = '\n'.join(filtered_lines) # Clean up multiple empty lines new_content = re.sub(r'\n\s*\n\s*\n', '\n\n', new_content) # Add single new courtyard before closing parenthesis new_content = new_content.rstrip().rstrip(')') + '\n' + courtyard_rect + '\n)' print(f"✅ Replaced {len(courtyard_lines)} courtyard elements with new one: {courtyard_min_x:.3f},{courtyard_min_y:.3f} to {courtyard_max_x:.3f},{courtyard_max_y:.3f}") else: # Add new courtyard before the closing parenthesis new_content = new_content.rstrip().rstrip(')') + '\n' + courtyard_rect + '\n)' print(f"✅ Added courtyard: {courtyard_min_x:.3f},{courtyard_min_y:.3f} to {courtyard_max_x:.3f},{courtyard_max_y:.3f}") # Write back to file with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) print(f"✅ Logo centered in {file_path}") print(f" Final logo bounds: X=[{new_min_x:.3f}, {new_max_x:.3f}], Y=[{new_min_y:.3f}, {new_max_y:.3f}]") if __name__ == "__main__": if len(sys.argv) > 1: # Use file path from command line argument (supports wildcards) pattern = sys.argv[1] # Expand wildcards if '*' in pattern or '?' in pattern: files = glob.glob(pattern) if not files: print(f"No files found matching pattern: {pattern}") sys.exit(1) print(f"Found {len(files)} file(s) matching pattern: {pattern}") else: files = [pattern] # Process each file success_count = 0 error_count = 0 for i, file_path in enumerate(files): if not os.path.exists(file_path): print(f"❌ File not found: {file_path}") error_count += 1 continue print(f"\n{'='*60}") print(f"Processing file {i+1}/{len(files)}: {os.path.basename(file_path)}") print(f"{'='*60}") try: center_footprint_logo(file_path) success_count += 1 except Exception as e: print(f"❌ Error processing {file_path}: {e}") error_count += 1 continue # Summary with appropriate icon total_files = success_count + error_count if error_count == 0: print(f"\n✅ Successfully processed all {success_count} file(s)") elif success_count == 0: print(f"\n❌ Failed to process all {total_files} file(s)") else: print(f"\n⚠️ Processed {success_count}/{total_files} file(s) successfully, {error_count} failed") else: # Default to 5mm logo footprint_file = r"c:\Users\iteedi\OneDrive - Stadlerrail AG\Dokumente\kicad_libs\my-kicad-libs\footprints\Logos_Personal.pretty\Eduard_Iten_Logo_5mm.kicad_mod" center_footprint_logo(footprint_file)