my-kicad-lib/scripts/center_logo.py

185 lines
7.7 KiB
Python

#!/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)