Add SVG combination tool and plate dimension options
Created combine_svg_vertical.py to vertically stack SVG files with centering
and configurable spacing. Enhanced svg_logo_to_stl.py with plate dimensions
and margin options for better nameplate creation.
combine_svg_vertical.py features:
- Vertically combines two SVG files (icon above text)
- Automatically centers smaller element
- Configurable spacing between elements (default: 20px)
- Configurable margin around composition (default: 10px)
- Preserves individual SVG structure in groups
svg_logo_to_stl.py enhancements:
- Added --plate-width and --plate-height options
- Logo scales to fit within plate dimensions (maintaining aspect ratio)
- Added --margin option for spacing around logo on plate
- Automatic centering of logo on plate
- Reports actual dimensions: plate, logo, margin, and offset
Example usage:
# Combine icon and text
python combine_svg_vertical.py um_grey.svg um_black.svg combined.svg --spacing 114
# Center on 256x256mm plate with 10mm margin
python svg_logo_to_stl.py combined.svg output \
--plate-width 256 --plate-height 256 --margin 10 \
--base-thickness 1.5 --feature-height 2.5
Files:
- combine_svg_vertical.py: Vertical SVG stacking tool
- README_combine_svg.md: Documentation for combine tool
- underground-magnetics-combined.svg: Example combined logo
- svg_logo_to_stl.py: Enhanced with plate dimensions
- command.txt: Updated with new parameters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
145
combine_svg_vertical.py
Executable file
145
combine_svg_vertical.py
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Combine two SVG files vertically (top over bottom) with centering and spacing.
|
||||
Useful for combining separated logo elements for 3D printing.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_svg(svg_path: Path):
|
||||
"""Load SVG and extract its dimensions and content"""
|
||||
tree = ET.parse(svg_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Get dimensions from viewBox or width/height attributes
|
||||
viewbox = root.get('viewBox')
|
||||
if viewbox:
|
||||
parts = viewbox.split()
|
||||
x, y, width, height = map(float, parts)
|
||||
else:
|
||||
width = float(root.get('width', 0))
|
||||
height = float(root.get('height', 0))
|
||||
x, y = 0, 0
|
||||
|
||||
return {
|
||||
'root': root,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'viewbox_x': x,
|
||||
'viewbox_y': y
|
||||
}
|
||||
|
||||
|
||||
def combine_svgs_vertical(top_svg_path: Path, bottom_svg_path: Path, output_path: Path,
|
||||
spacing: float = 20, margin: float = 10):
|
||||
"""
|
||||
Combine two SVGs vertically with the top one centered above the bottom one.
|
||||
|
||||
Args:
|
||||
top_svg_path: Path to top SVG (e.g., icon)
|
||||
bottom_svg_path: Path to bottom SVG (e.g., text)
|
||||
output_path: Path for output combined SVG
|
||||
spacing: Vertical space between top and bottom elements (default: 20)
|
||||
margin: Margin around the entire composition (default: 10)
|
||||
"""
|
||||
# Load both SVGs
|
||||
top_svg = load_svg(top_svg_path)
|
||||
bottom_svg = load_svg(bottom_svg_path)
|
||||
|
||||
# Calculate combined dimensions
|
||||
max_width = max(top_svg['width'], bottom_svg['width'])
|
||||
combined_height = top_svg['height'] + spacing + bottom_svg['height']
|
||||
|
||||
# Add margins
|
||||
canvas_width = max_width + (2 * margin)
|
||||
canvas_height = combined_height + (2 * margin)
|
||||
|
||||
# Calculate centering offsets
|
||||
top_x_offset = margin + (max_width - top_svg['width']) / 2
|
||||
top_y_offset = margin
|
||||
|
||||
bottom_x_offset = margin + (max_width - bottom_svg['width']) / 2
|
||||
bottom_y_offset = margin + top_svg['height'] + spacing
|
||||
|
||||
# Create new SVG root
|
||||
combined = ET.Element('svg', {
|
||||
'xmlns': 'http://www.w3.org/2000/svg',
|
||||
'width': str(canvas_width),
|
||||
'height': str(canvas_height),
|
||||
'viewBox': f'0 0 {canvas_width} {canvas_height}'
|
||||
})
|
||||
|
||||
# Add comment for clarity
|
||||
comment = ET.Comment(f' Combined: {top_svg_path.name} (top) + {bottom_svg_path.name} (bottom) ')
|
||||
combined.append(comment)
|
||||
|
||||
# Create group for top SVG with transform
|
||||
top_group = ET.SubElement(combined, 'g', {
|
||||
'id': 'top',
|
||||
'transform': f'translate({top_x_offset}, {top_y_offset})'
|
||||
})
|
||||
|
||||
# Copy all elements from top SVG
|
||||
for child in top_svg['root']:
|
||||
if child.tag != '{http://www.w3.org/2000/svg}metadata':
|
||||
top_group.append(child)
|
||||
|
||||
# Create group for bottom SVG with transform
|
||||
bottom_group = ET.SubElement(combined, 'g', {
|
||||
'id': 'bottom',
|
||||
'transform': f'translate({bottom_x_offset}, {bottom_y_offset})'
|
||||
})
|
||||
|
||||
# Copy all elements from bottom SVG
|
||||
for child in bottom_svg['root']:
|
||||
if child.tag != '{http://www.w3.org/2000/svg}metadata':
|
||||
bottom_group.append(child)
|
||||
|
||||
# Write output
|
||||
tree = ET.ElementTree(combined)
|
||||
ET.indent(tree, space=' ')
|
||||
tree.write(output_path, encoding='utf-8', xml_declaration=True)
|
||||
|
||||
print(f"Created {output_path}")
|
||||
print(f" Top ({top_svg_path.name}): {top_svg['width']:.0f}x{top_svg['height']:.0f} at ({top_x_offset:.1f}, {top_y_offset:.1f})")
|
||||
print(f" Bottom ({bottom_svg_path.name}): {bottom_svg['width']:.0f}x{bottom_svg['height']:.0f} at ({bottom_x_offset:.1f}, {bottom_y_offset:.1f})")
|
||||
print(f" Canvas: {canvas_width:.0f}x{canvas_height:.0f}")
|
||||
print(f" Spacing: {spacing:.0f}, Margin: {margin:.0f}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Combine two SVG files vertically with centering and spacing'
|
||||
)
|
||||
parser.add_argument('top', help='Top SVG file (e.g., icon)')
|
||||
parser.add_argument('bottom', help='Bottom SVG file (e.g., text)')
|
||||
parser.add_argument('output', help='Output SVG file')
|
||||
parser.add_argument('--spacing', '-s', type=float, default=20,
|
||||
help='Vertical spacing between elements (default: 20)')
|
||||
parser.add_argument('--margin', '-m', type=float, default=10,
|
||||
help='Margin around entire composition (default: 10)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
top_path = Path(args.top)
|
||||
bottom_path = Path(args.bottom)
|
||||
output_path = Path(args.output)
|
||||
|
||||
if not top_path.exists():
|
||||
print(f"ERROR: Top SVG file not found: {top_path}")
|
||||
return 1
|
||||
|
||||
if not bottom_path.exists():
|
||||
print(f"ERROR: Bottom SVG file not found: {bottom_path}")
|
||||
return 1
|
||||
|
||||
combine_svgs_vertical(top_path, bottom_path, output_path, args.spacing, args.margin)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user