feat: improve logo symbols with unified stackable design
- Make all symbol boxes same size (12.7mm x 5.08mm) for perfect stacking - Separate symbol graphics from text for better control - Widen footprint silkscreen boxes to prevent text clipping - Eduard Iten, Created By, and CC logos now perfectly stackable - All logos maintain proper exclude_from_bom settings
This commit is contained in:
185
scripts/center_logo.py
Normal file
185
scripts/center_logo.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user