185 lines
7.7 KiB
Python
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) |