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:
112
README_combine_svg.md
Normal file
112
README_combine_svg.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Combine SVG Vertical
|
||||||
|
|
||||||
|
A script to combine two SVG files vertically with automatic centering and configurable spacing.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This tool is designed for preparing multi-part logos for 3D printing by:
|
||||||
|
- Placing an icon/logo above text
|
||||||
|
- Automatically centering the smaller element
|
||||||
|
- Adding configurable spacing between elements
|
||||||
|
- Maintaining proper dimensions for STL conversion
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python combine_svg_vertical.py <top_svg> <bottom_svg> <output_svg> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
- `top` - Top SVG file (e.g., icon or logo)
|
||||||
|
- `bottom` - Bottom SVG file (e.g., text)
|
||||||
|
- `output` - Output combined SVG file
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
- `--spacing, -s` - Vertical spacing between elements in pixels (default: 20)
|
||||||
|
- `--margin, -m` - Margin around entire composition in pixels (default: 10)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Usage (Underground Magnetics Logo)
|
||||||
|
```bash
|
||||||
|
python combine_svg_vertical.py um_grey.svg um_black.svg underground-magnetics-combined.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
- Grey "UM" icon (413×390) centered above black text (3070×233)
|
||||||
|
- 20px spacing between icon and text
|
||||||
|
- 10px margin around composition
|
||||||
|
- Final canvas: 3090×663px
|
||||||
|
|
||||||
|
### Custom Spacing
|
||||||
|
```bash
|
||||||
|
# Larger spacing between elements
|
||||||
|
python combine_svg_vertical.py icon.svg text.svg combined.svg --spacing 50
|
||||||
|
|
||||||
|
# Larger margin around composition
|
||||||
|
python combine_svg_vertical.py icon.svg text.svg combined.svg --margin 30
|
||||||
|
|
||||||
|
# Both custom
|
||||||
|
python combine_svg_vertical.py icon.svg text.svg combined.svg -s 40 -m 20
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Load SVGs** - Reads dimensions from viewBox or width/height attributes
|
||||||
|
2. **Calculate Layout** - Determines positioning based on:
|
||||||
|
- Maximum width of both elements
|
||||||
|
- Combined height + spacing
|
||||||
|
- Centering offsets for smaller element
|
||||||
|
3. **Create Groups** - Wraps each SVG in a `<g>` group with transform
|
||||||
|
4. **Combine** - Creates new SVG canvas with both elements positioned
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<svg width="..." height="..." viewBox="...">
|
||||||
|
<!-- Combined: top.svg (top) + bottom.svg (bottom) -->
|
||||||
|
<g id="top" transform="translate(x_offset, y_offset)">
|
||||||
|
<!-- All elements from top SVG -->
|
||||||
|
</g>
|
||||||
|
<g id="bottom" transform="translate(x_offset, y_offset)">
|
||||||
|
<!-- All elements from bottom SVG -->
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with 3D Workflow
|
||||||
|
|
||||||
|
After combining SVGs, you can convert to STL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Convert combined logo to 3D model
|
||||||
|
python svg_logo_to_stl.py underground-magnetics-combined.svg um_logo \
|
||||||
|
--width-mm 100 \
|
||||||
|
--base-thickness 1.5 \
|
||||||
|
--feature-height 2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a single 3D model with both the icon and text at the same height (not stacked vertically in 3D, just in the 2D layout).
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- **Spacing**: Use `--spacing` to adjust the gap between icon and text
|
||||||
|
- Small spacing (10-20): Compact look
|
||||||
|
- Medium spacing (20-40): Balanced (default)
|
||||||
|
- Large spacing (50+): Separated elements
|
||||||
|
|
||||||
|
- **Margin**: Use `--margin` to add breathing room around the entire composition
|
||||||
|
- Useful when the combined SVG will be cropped or bounded
|
||||||
|
|
||||||
|
- **Order Matters**: The first argument is always placed on top
|
||||||
|
- Icon above text: `combine_svg_vertical.py icon.svg text.svg ...`
|
||||||
|
- Text above icon: `combine_svg_vertical.py text.svg icon.svg ...`
|
||||||
|
|
||||||
|
## File Size
|
||||||
|
|
||||||
|
The combined SVG file size is approximately the sum of the input files:
|
||||||
|
- um_grey.svg: 728B
|
||||||
|
- um_black.svg: 6.1KB
|
||||||
|
- **Combined**: 7.0KB
|
||||||
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())
|
||||||
44
command.txt
44
command.txt
@@ -1,28 +1,46 @@
|
|||||||
# SVG to STL Conversion Command
|
# SVG to Multi-Color 3D Model Converter
|
||||||
|
# Automatically detects all colors in SVG and creates one part per color
|
||||||
# Note: The DYLD_LIBRARY_PATH is needed for Cairo to work on macOS
|
# Note: The DYLD_LIBRARY_PATH is needed for Cairo to work on macOS
|
||||||
|
|
||||||
|
# Basic usage - specify width, logo scales proportionally
|
||||||
export DYLD_LIBRARY_PATH="/opt/homebrew/lib:$DYLD_LIBRARY_PATH" && source .venv/bin/activate && python svg_logo_to_stl.py "Color logo - no background.svg" logo_output --width-mm 254 --base-thickness 1.5 --feature-height 2.5
|
export DYLD_LIBRARY_PATH="/opt/homebrew/lib:$DYLD_LIBRARY_PATH" && source .venv/bin/activate && python svg_logo_to_stl.py "Color logo - no background.svg" logo_output --width-mm 254 --base-thickness 1.5 --feature-height 2.5
|
||||||
|
|
||||||
|
# Plate dimensions - logo scales and centers on specified plate size with margin
|
||||||
|
python svg_logo_to_stl.py underground-magnetics-combined.svg um_logo --plate-width 256 --plate-height 256 --margin 10 --base-thickness 1.5 --feature-height 2.5
|
||||||
|
|
||||||
# Or if you've already activated the venv and set the library path:
|
# Or if you've already activated the venv and set the library path:
|
||||||
python svg_logo_to_stl.py "Color logo - no background.svg" logo_output --width-mm 254 --base-thickness 1.5 --feature-height 2.5
|
python svg_logo_to_stl.py "Color logo - no background.svg" logo_output --width-mm 254 --base-thickness 1.5 --feature-height 2.5
|
||||||
|
|
||||||
|
# Works with ANY SVG file! The script will:
|
||||||
|
# 1. Automatically detect all unique colors
|
||||||
|
# 2. Create one part per color
|
||||||
|
# 3. Generate assembly with separate selectable parts
|
||||||
|
|
||||||
# Output files:
|
# Output files:
|
||||||
# - logo_output_base.stl - Black base plate (Z: 0 to 1.5mm)
|
# - logo_output_base.stl - Black base plate
|
||||||
# - logo_output_green_icon.stl - Green microchip icon at top (Z: 1.5 to 4.0mm)
|
# - logo_output_{color}.stl - One STL per detected color
|
||||||
# - logo_output_white.stl - White "SPARKSOFT DESIGN" text (Z: 1.5 to 4.0mm)
|
# - logo_output_assembled.stl - All parts combined (single mesh)
|
||||||
# - logo_output_green_bottom.stl - Green "EMBEDDED SOLUTIONS" text (Z: 1.5 to 4.0mm)
|
|
||||||
# - logo_output_assembled.stl - All parts combined (single object)
|
|
||||||
# - logo_output_assembled.3mf - All parts as single mesh (for paint tool)
|
# - logo_output_assembled.3mf - All parts as single mesh (for paint tool)
|
||||||
# - logo_output_parts.3mf - 4 separate selectable parts (BEST for multi-color!)
|
# - logo_output_parts.3mf - Separate selectable parts (BEST for multi-color!)
|
||||||
|
|
||||||
# Import logo_output_parts.3mf into BambuStudio
|
# Import logo_output_parts.3mf into BambuStudio
|
||||||
# You'll see 4 separate parts that can each be assigned different filaments:
|
# Each detected color becomes a separate selectable part
|
||||||
|
# Assign different filaments to each part for multi-color printing
|
||||||
|
|
||||||
|
# Example for this logo:
|
||||||
# 1. base - black base plate
|
# 1. base - black base plate
|
||||||
# 2. green_icon - microchip icon
|
# 2. white - "SPARKSOFT DESIGN" text
|
||||||
# 3. white_text - middle text
|
# 3. green - microchip icon + "EMBEDDED SOLUTIONS" text
|
||||||
# 4. green_text - bottom text
|
|
||||||
# Parts do NOT overlap in XY space, preventing slicing issues
|
# Total model height: base_thickness + feature_height (default: 0.8mm + 1.8mm = 2.6mm)
|
||||||
# Total model height: 4.0mm (1.5mm base + 2.5mm features)
|
|
||||||
|
|
||||||
# IMPORTANT: Scale must be 254mm or larger for 0.4mm nozzle
|
# IMPORTANT: Scale must be 254mm or larger for 0.4mm nozzle
|
||||||
# Smaller sizes cause features to be too small for proper toolpath generation
|
# Smaller sizes cause features to be too small for proper toolpath generation
|
||||||
|
|
||||||
|
# Parameters:
|
||||||
|
# --width-mm: Logo width in mm (if plate dimensions not specified)
|
||||||
|
# --plate-width: Plate width in mm (logo will be scaled and centered)
|
||||||
|
# --plate-height: Plate height in mm (logo will be scaled and centered)
|
||||||
|
# --margin: Margin around logo when using plate dimensions (default: 0)
|
||||||
|
# --base-thickness: Base plate thickness in mm (default: 0.8)
|
||||||
|
# --feature-height: Raised feature height in mm (default: 1.8)
|
||||||
|
|||||||
@@ -37,11 +37,76 @@ def color_mask(image: np.ndarray, target_rgb: tuple[int, int, int], tolerance: i
|
|||||||
return alpha & (diff <= tolerance)
|
return alpha & (diff <= tolerance)
|
||||||
|
|
||||||
|
|
||||||
def white_color_mask(image: np.ndarray, min_brightness: int = 220) -> np.ndarray:
|
def detect_color_clusters(image: np.ndarray, tolerance: int = 80, min_pixels: int = 100):
|
||||||
alpha = image[..., 3] > 32
|
"""
|
||||||
rgb = image[..., :3]
|
Automatically detect distinct color regions in an image.
|
||||||
brightness = np.min(rgb, axis=-1)
|
Returns list of (color_name, target_rgb, mask) tuples.
|
||||||
return alpha & (brightness >= min_brightness)
|
"""
|
||||||
|
img_array = np.array(image)
|
||||||
|
alpha = img_array[..., 3] > 32
|
||||||
|
rgb_pixels = img_array[alpha][:, :3]
|
||||||
|
|
||||||
|
if len(rgb_pixels) == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Get unique colors and their counts
|
||||||
|
unique_colors, counts = np.unique(rgb_pixels, axis=0, return_counts=True)
|
||||||
|
|
||||||
|
# Sort by count (most common first)
|
||||||
|
sorted_indices = np.argsort(-counts)
|
||||||
|
unique_colors = unique_colors[sorted_indices]
|
||||||
|
counts = counts[sorted_indices]
|
||||||
|
|
||||||
|
# Cluster similar colors together
|
||||||
|
color_clusters = []
|
||||||
|
used = set()
|
||||||
|
|
||||||
|
for i, color in enumerate(unique_colors):
|
||||||
|
if i in used:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find all colors within tolerance of this one
|
||||||
|
cluster_mask = np.zeros(img_array.shape[:2], dtype=bool)
|
||||||
|
cluster_pixels = 0
|
||||||
|
|
||||||
|
for j, other_color in enumerate(unique_colors):
|
||||||
|
if j in used:
|
||||||
|
continue
|
||||||
|
diff = np.linalg.norm(color.astype(np.int16) - other_color.astype(np.int16))
|
||||||
|
if diff <= tolerance:
|
||||||
|
mask = color_mask(img_array, tuple(other_color), tolerance=tolerance)
|
||||||
|
cluster_mask |= mask
|
||||||
|
cluster_pixels += counts[j]
|
||||||
|
used.add(j)
|
||||||
|
|
||||||
|
if cluster_pixels >= min_pixels:
|
||||||
|
# Generate color name
|
||||||
|
r, g, b = color
|
||||||
|
if r > 200 and g > 200 and b > 200:
|
||||||
|
color_name = 'white'
|
||||||
|
elif r < 50 and g < 50 and b < 50:
|
||||||
|
color_name = 'black'
|
||||||
|
elif r > max(g, b):
|
||||||
|
color_name = 'red'
|
||||||
|
elif g > max(r, b):
|
||||||
|
color_name = 'green'
|
||||||
|
elif b > max(r, g):
|
||||||
|
color_name = 'blue'
|
||||||
|
elif r > 200 and g > 200:
|
||||||
|
color_name = 'yellow'
|
||||||
|
elif r > 200 and b > 200:
|
||||||
|
color_name = 'magenta'
|
||||||
|
elif g > 200 and b > 200:
|
||||||
|
color_name = 'cyan'
|
||||||
|
else:
|
||||||
|
color_name = f'color_{len(color_clusters)+1}'
|
||||||
|
|
||||||
|
color_clusters.append((color_name, tuple(color), cluster_mask, cluster_pixels))
|
||||||
|
|
||||||
|
# Sort by pixel count (largest first)
|
||||||
|
color_clusters.sort(key=lambda x: x[3], reverse=True)
|
||||||
|
|
||||||
|
return [(name, rgb, mask) for name, rgb, mask, _ in color_clusters]
|
||||||
|
|
||||||
|
|
||||||
def signed_area(coords: np.ndarray) -> float:
|
def signed_area(coords: np.ndarray) -> float:
|
||||||
@@ -158,55 +223,91 @@ def create_extruded_mesh(polygons, height_mm: float, scale: float, y_flip: bool
|
|||||||
return trimesh.util.concatenate(watertight_meshes)
|
return trimesh.util.concatenate(watertight_meshes)
|
||||||
|
|
||||||
|
|
||||||
def build_logo_meshes(svg_path: Path, width_mm: float, base_thickness: float, feature_height: float, png_width: int):
|
def build_logo_meshes(svg_path: Path, width_mm: float, base_thickness: float, feature_height: float, png_width: int,
|
||||||
|
plate_width_mm: float = None, plate_height_mm: float = None, margin_mm: float = 0):
|
||||||
|
"""
|
||||||
|
Generalized function to convert any SVG to 3D meshes.
|
||||||
|
Automatically detects colors and creates one part per color.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
svg_path: Path to SVG file
|
||||||
|
width_mm: Target width in mm (if plate dimensions not specified)
|
||||||
|
base_thickness: Thickness of base plate in mm
|
||||||
|
feature_height: Height of raised features in mm
|
||||||
|
png_width: Resolution for SVG rasterization
|
||||||
|
plate_width_mm: Optional plate width - logo will be scaled and centered
|
||||||
|
plate_height_mm: Optional plate height - logo will be scaled and centered
|
||||||
|
margin_mm: Margin around logo when using plate dimensions
|
||||||
|
|
||||||
|
Returns (base_mesh, color_parts_dict) where color_parts_dict maps color names to meshes.
|
||||||
|
"""
|
||||||
image = render_svg_to_image(svg_path, pixel_width=png_width)
|
image = render_svg_to_image(svg_path, pixel_width=png_width)
|
||||||
width_px, height_px = image.size
|
width_px, height_px = image.size
|
||||||
scale = width_mm / width_px
|
|
||||||
height_mm = height_px * scale
|
|
||||||
|
|
||||||
img_array = np.array(image)
|
# Calculate scaling based on whether plate dimensions are specified
|
||||||
green_mask = clean_mask(color_mask(img_array, (57, 233, 145), tolerance=80))
|
if plate_width_mm is not None and plate_height_mm is not None:
|
||||||
white_mask = clean_mask(white_color_mask(img_array, min_brightness=220))
|
# Calculate available space after margins
|
||||||
|
available_width = plate_width_mm - (2 * margin_mm)
|
||||||
|
available_height = plate_height_mm - (2 * margin_mm)
|
||||||
|
|
||||||
# Split green mask into top (icon) and bottom (text) regions
|
# Scale to fit within available space (maintaining aspect ratio)
|
||||||
# Find where white text is located to use as separator
|
scale_x = available_width / width_px
|
||||||
white_rows = np.any(white_mask, axis=1)
|
scale_y = available_height / height_px
|
||||||
white_y_coords = np.where(white_rows)[0]
|
scale = min(scale_x, scale_y)
|
||||||
|
|
||||||
if len(white_y_coords) > 0:
|
# Actual logo dimensions after scaling
|
||||||
white_y_mid = (white_y_coords[0] + white_y_coords[-1]) // 2
|
logo_width_mm = width_px * scale
|
||||||
|
logo_height_mm = height_px * scale
|
||||||
|
|
||||||
# Green icon: everything above white text
|
# Calculate centering offsets
|
||||||
green_icon_mask = green_mask.copy()
|
x_offset = (plate_width_mm - logo_width_mm) / 2.0
|
||||||
green_icon_mask[white_y_coords[0]:, :] = False # Zero out everything from white text onwards
|
y_offset = (plate_height_mm - logo_height_mm) / 2.0
|
||||||
|
|
||||||
# Green bottom text: everything below white text
|
# Use plate dimensions for base
|
||||||
green_bottom_mask = green_mask.copy()
|
width_mm = plate_width_mm
|
||||||
green_bottom_mask[:white_y_coords[-1], :] = False # Zero out everything before end of white text
|
height_mm = plate_height_mm
|
||||||
|
|
||||||
green_icon_polys = mask_to_polygons(green_icon_mask)
|
|
||||||
green_bottom_polys = mask_to_polygons(green_bottom_mask)
|
|
||||||
else:
|
else:
|
||||||
# No white text found, treat all green as one
|
# Original behavior: use specified width, calculate height
|
||||||
green_icon_polys = mask_to_polygons(green_mask)
|
scale = width_mm / width_px
|
||||||
green_bottom_polys = []
|
logo_width_mm = width_mm
|
||||||
|
logo_height_mm = height_px * scale
|
||||||
|
height_mm = logo_height_mm
|
||||||
|
x_offset = 0
|
||||||
|
y_offset = 0
|
||||||
|
|
||||||
white_polys = mask_to_polygons(white_mask)
|
# Automatically detect all color regions
|
||||||
|
color_clusters = detect_color_clusters(image, tolerance=80, min_pixels=100)
|
||||||
|
|
||||||
|
print(f"Detected {len(color_clusters)} color regions:")
|
||||||
|
for name, rgb, mask in color_clusters:
|
||||||
|
pixel_count = np.sum(mask)
|
||||||
|
print(f" {name}: RGB{rgb} - {pixel_count:,} pixels")
|
||||||
|
|
||||||
|
# Create base plate
|
||||||
base = trimesh.creation.box(extents=(width_mm, height_mm, base_thickness))
|
base = trimesh.creation.box(extents=(width_mm, height_mm, base_thickness))
|
||||||
base.apply_translation((width_mm / 2.0, height_mm / 2.0, base_thickness / 2.0))
|
base.apply_translation((width_mm / 2.0, height_mm / 2.0, base_thickness / 2.0))
|
||||||
|
|
||||||
# Features sit exactly on top of the base (no overlap to avoid slicing issues)
|
# Create mesh for each color
|
||||||
green_icon_mesh = create_extruded_mesh(green_icon_polys, feature_height, scale)
|
color_parts = {}
|
||||||
green_bottom_mesh = create_extruded_mesh(green_bottom_polys, feature_height, scale)
|
for name, rgb, mask in color_clusters:
|
||||||
white_mesh = create_extruded_mesh(white_polys, feature_height, scale)
|
cleaned_mask = clean_mask(mask)
|
||||||
|
polygons = mask_to_polygons(cleaned_mask)
|
||||||
|
|
||||||
for mesh in (green_icon_mesh, green_bottom_mesh, white_mesh):
|
if polygons:
|
||||||
|
mesh = create_extruded_mesh(polygons, feature_height, scale)
|
||||||
if mesh is not None:
|
if mesh is not None:
|
||||||
# Position features to start exactly at the top of the base
|
# Position features to start exactly at the top of the base
|
||||||
mesh.apply_translation((0.0, height_mm, base_thickness))
|
# Apply centering offset when plate dimensions are used
|
||||||
|
mesh.apply_translation((x_offset, height_mm - y_offset, base_thickness))
|
||||||
|
color_parts[name] = mesh
|
||||||
|
|
||||||
return base, green_icon_mesh, green_bottom_mesh, white_mesh
|
if plate_width_mm is not None and plate_height_mm is not None:
|
||||||
|
print(f"\nPlate dimensions: {plate_width_mm:.1f} x {plate_height_mm:.1f} mm")
|
||||||
|
print(f"Logo dimensions: {logo_width_mm:.1f} x {logo_height_mm:.1f} mm")
|
||||||
|
print(f"Margin: {margin_mm:.1f} mm")
|
||||||
|
print(f"Centering offset: ({x_offset:.1f}, {y_offset:.1f}) mm")
|
||||||
|
|
||||||
|
return base, color_parts
|
||||||
|
|
||||||
|
|
||||||
def _mesh_to_3mf_object(mesh: trimesh.Trimesh, object_id: int, name: str, material_id: int) -> ET.Element:
|
def _mesh_to_3mf_object(mesh: trimesh.Trimesh, object_id: int, name: str, material_id: int) -> ET.Element:
|
||||||
@@ -234,21 +335,36 @@ def _mesh_to_3mf_object(mesh: trimesh.Trimesh, object_id: int, name: str, materi
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def _add_basematerials(resources: ET.Element) -> int:
|
def _add_basematerials(resources: ET.Element, color_names: list[str]) -> dict[str, int]:
|
||||||
|
"""
|
||||||
|
Add basematerials for all colors.
|
||||||
|
Returns dict mapping color name to material ID.
|
||||||
|
"""
|
||||||
basematerials = ET.SubElement(resources, 'basematerials', {'id': '1'})
|
basematerials = ET.SubElement(resources, 'basematerials', {'id': '1'})
|
||||||
|
|
||||||
|
# Color palette for display
|
||||||
|
color_palette = {
|
||||||
|
'base': '#000000',
|
||||||
|
'black': '#000000',
|
||||||
|
'white': '#FFFFFF',
|
||||||
|
'red': '#FF0000',
|
||||||
|
'green': '#00FF00',
|
||||||
|
'blue': '#0000FF',
|
||||||
|
'yellow': '#FFFF00',
|
||||||
|
'magenta': '#FF00FF',
|
||||||
|
'cyan': '#00FFFF',
|
||||||
|
}
|
||||||
|
|
||||||
|
material_map = {}
|
||||||
|
for i, name in enumerate(['base'] + color_names):
|
||||||
|
display_color = color_palette.get(name, '#808080') # Default to gray
|
||||||
ET.SubElement(basematerials, 'base', {
|
ET.SubElement(basematerials, 'base', {
|
||||||
'name': 'Base',
|
'name': name.capitalize(),
|
||||||
'displaycolor': '#000000'
|
'displaycolor': display_color
|
||||||
})
|
})
|
||||||
ET.SubElement(basematerials, 'base', {
|
material_map[name] = i
|
||||||
'name': 'Green',
|
|
||||||
'displaycolor': '#39e991'
|
return material_map
|
||||||
})
|
|
||||||
ET.SubElement(basematerials, 'base', {
|
|
||||||
'name': 'White',
|
|
||||||
'displaycolor': '#ffffff'
|
|
||||||
})
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def save_3mf(meshes: dict[str, trimesh.Trimesh], path: Path):
|
def save_3mf(meshes: dict[str, trimesh.Trimesh], path: Path):
|
||||||
@@ -258,11 +374,13 @@ def save_3mf(meshes: dict[str, trimesh.Trimesh], path: Path):
|
|||||||
'unit': 'millimeter'
|
'unit': 'millimeter'
|
||||||
})
|
})
|
||||||
resources = ET.SubElement(model, 'resources')
|
resources = ET.SubElement(model, 'resources')
|
||||||
_add_basematerials(resources)
|
|
||||||
|
# Get list of color names (excluding 'base')
|
||||||
|
color_names = [name for name in meshes.keys() if name != 'base' and meshes[name] is not None]
|
||||||
|
material_map = _add_basematerials(resources, color_names)
|
||||||
|
|
||||||
object_ids = []
|
object_ids = []
|
||||||
current_id = 1
|
current_id = 1
|
||||||
material_map = {'base': 0, 'green_icon': 1, 'green_text': 1, 'white_text': 2, 'logo': 0}
|
|
||||||
for name, mesh in meshes.items():
|
for name, mesh in meshes.items():
|
||||||
if mesh is None:
|
if mesh is None:
|
||||||
continue
|
continue
|
||||||
@@ -311,12 +429,14 @@ def save_3mf_parts(meshes: dict[str, trimesh.Trimesh], path: Path):
|
|||||||
'unit': 'millimeter'
|
'unit': 'millimeter'
|
||||||
})
|
})
|
||||||
resources = ET.SubElement(model, 'resources')
|
resources = ET.SubElement(model, 'resources')
|
||||||
basematerials_id = _add_basematerials(resources)
|
|
||||||
|
# Get list of color names (excluding 'base')
|
||||||
|
color_names = [name for name in meshes.keys() if name != 'base' and meshes[name] is not None]
|
||||||
|
material_map = _add_basematerials(resources, color_names)
|
||||||
|
|
||||||
# Create separate object entries for each mesh
|
# Create separate object entries for each mesh
|
||||||
object_ids = []
|
object_ids = []
|
||||||
current_id = 1
|
current_id = 1
|
||||||
material_map = {'base': 0, 'green_icon': 1, 'green_text': 1, 'white_text': 2}
|
|
||||||
for name, mesh in meshes.items():
|
for name, mesh in meshes.items():
|
||||||
if mesh is None:
|
if mesh is None:
|
||||||
continue
|
continue
|
||||||
@@ -367,39 +487,46 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description="Convert a color SVG logo into a layered STL for multi-color printing.")
|
parser = argparse.ArgumentParser(description="Convert a color SVG logo into a layered STL for multi-color printing.")
|
||||||
parser.add_argument("svg", help="Input SVG file path")
|
parser.add_argument("svg", help="Input SVG file path")
|
||||||
parser.add_argument("output", help="Base output file path (without extension is fine)")
|
parser.add_argument("output", help="Base output file path (without extension is fine)")
|
||||||
parser.add_argument("--width-mm", type=float, default=100.0, help="Final model width in millimeters")
|
parser.add_argument("--width-mm", type=float, default=100.0, help="Final model width in millimeters (if plate dimensions not specified)")
|
||||||
parser.add_argument("--base-thickness", type=float, default=0.8, help="Thickness of the black base in mm")
|
parser.add_argument("--base-thickness", type=float, default=0.8, help="Thickness of the black base in mm")
|
||||||
parser.add_argument("--feature-height", type=float, default=1.8, help="Height of the raised logo features above the base in mm")
|
parser.add_argument("--feature-height", type=float, default=1.8, help="Height of the raised logo features above the base in mm")
|
||||||
parser.add_argument("--png-width", type=int, default=2048, help="Rasterization width for SVG rendering")
|
parser.add_argument("--png-width", type=int, default=2048, help="Rasterization width for SVG rendering")
|
||||||
|
parser.add_argument("--plate-width", type=float, help="Plate width in mm - logo will be scaled and centered")
|
||||||
|
parser.add_argument("--plate-height", type=float, help="Plate height in mm - logo will be scaled and centered")
|
||||||
|
parser.add_argument("--margin", type=float, default=0, help="Margin around logo when using plate dimensions (default: 0)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
svg_path = Path(args.svg)
|
svg_path = Path(args.svg)
|
||||||
output_prefix = Path(args.output)
|
output_prefix = Path(args.output)
|
||||||
output_prefix.parent.mkdir(parents=True, exist_ok=True)
|
output_prefix.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
base_mesh, green_icon_mesh, green_bottom_mesh, white_mesh = build_logo_meshes(
|
base_mesh, color_parts = build_logo_meshes(
|
||||||
svg_path,
|
svg_path,
|
||||||
args.width_mm,
|
args.width_mm,
|
||||||
args.base_thickness,
|
args.base_thickness,
|
||||||
args.feature_height,
|
args.feature_height,
|
||||||
args.png_width,
|
args.png_width,
|
||||||
|
plate_width_mm=args.plate_width,
|
||||||
|
plate_height_mm=args.plate_height,
|
||||||
|
margin_mm=args.margin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Save individual STL files for each color part
|
||||||
base_path = output_prefix.with_name(output_prefix.stem + "_base.stl")
|
base_path = output_prefix.with_name(output_prefix.stem + "_base.stl")
|
||||||
green_icon_path = output_prefix.with_name(output_prefix.stem + "_green_icon.stl")
|
|
||||||
green_bottom_path = output_prefix.with_name(output_prefix.stem + "_green_bottom.stl")
|
|
||||||
white_path = output_prefix.with_name(output_prefix.stem + "_white.stl")
|
|
||||||
assembled_stl_path = output_prefix.with_name(output_prefix.stem + "_assembled.stl")
|
assembled_stl_path = output_prefix.with_name(output_prefix.stem + "_assembled.stl")
|
||||||
assembled_3mf_path = output_prefix.with_name(output_prefix.stem + "_assembled.3mf")
|
assembled_3mf_path = output_prefix.with_name(output_prefix.stem + "_assembled.3mf")
|
||||||
parts_3mf_path = output_prefix.with_name(output_prefix.stem + "_parts.3mf")
|
parts_3mf_path = output_prefix.with_name(output_prefix.stem + "_parts.3mf")
|
||||||
|
|
||||||
saved_base = save_mesh(base_mesh, base_path)
|
saved_base = save_mesh(base_mesh, base_path)
|
||||||
saved_green_icon = save_mesh(green_icon_mesh, green_icon_path)
|
saved_parts = {}
|
||||||
saved_green_bottom = save_mesh(green_bottom_mesh, green_bottom_path)
|
for color_name, mesh in color_parts.items():
|
||||||
saved_white = save_mesh(white_mesh, white_path)
|
part_path = output_prefix.with_name(output_prefix.stem + f"_{color_name}.stl")
|
||||||
|
if save_mesh(mesh, part_path):
|
||||||
|
saved_parts[color_name] = part_path
|
||||||
|
|
||||||
# Concatenate all meshes into a single object
|
# Concatenate all meshes into a single object
|
||||||
assembled_mesh = trimesh.util.concatenate([m for m in (base_mesh, green_icon_mesh, green_bottom_mesh, white_mesh) if m is not None])
|
all_meshes = [base_mesh] + list(color_parts.values())
|
||||||
|
assembled_mesh = trimesh.util.concatenate([m for m in all_meshes if m is not None])
|
||||||
assembled_mesh.export(assembled_stl_path)
|
assembled_mesh.export(assembled_stl_path)
|
||||||
|
|
||||||
# For 3MF assembled version, export as a SINGLE mesh object
|
# For 3MF assembled version, export as a SINGLE mesh object
|
||||||
@@ -409,25 +536,19 @@ def main():
|
|||||||
}, assembled_3mf_path)
|
}, assembled_3mf_path)
|
||||||
|
|
||||||
# Export as separate parts in assembly for multi-color selection
|
# Export as separate parts in assembly for multi-color selection
|
||||||
save_3mf_parts({
|
parts_dict = {'base': base_mesh}
|
||||||
'base': base_mesh,
|
parts_dict.update(color_parts)
|
||||||
'green_icon': green_icon_mesh,
|
save_3mf_parts(parts_dict, parts_3mf_path)
|
||||||
'white_text': white_mesh,
|
|
||||||
'green_text': green_bottom_mesh,
|
|
||||||
}, parts_3mf_path)
|
|
||||||
|
|
||||||
print("Created files:")
|
print("\nCreated files:")
|
||||||
if saved_base:
|
if saved_base:
|
||||||
print(f" Base STL: {saved_base}")
|
print(f" Base STL: {saved_base}")
|
||||||
if saved_green_icon:
|
for color_name, part_path in saved_parts.items():
|
||||||
print(f" Green icon STL: {saved_green_icon}")
|
print(f" {color_name.capitalize()} STL: {part_path}")
|
||||||
if saved_green_bottom:
|
|
||||||
print(f" Green bottom text STL: {saved_green_bottom}")
|
|
||||||
if saved_white:
|
|
||||||
print(f" White text STL: {saved_white}")
|
|
||||||
print(f" Assembled STL: {assembled_stl_path}")
|
print(f" Assembled STL: {assembled_stl_path}")
|
||||||
print(f" Assembled 3MF: {assembled_3mf_path}")
|
print(f" Assembled 3MF: {assembled_3mf_path}")
|
||||||
print(f" Parts 3MF: {parts_3mf_path}")
|
print(f" Parts 3MF: {parts_3mf_path}")
|
||||||
|
print(f"\nImport {parts_3mf_path.name} into BambuStudio for multi-color printing!")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
31
underground-magnetics-combined.svg
Normal file
31
underground-magnetics-combined.svg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<svg xmlns:ns0="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="3090.0" height="757.0" viewBox="0 0 3090.0 757.0">
|
||||||
|
<!-- Combined: um_grey.svg (top) + um_black.svg (bottom) -->
|
||||||
|
<g id="top" transform="translate(1338.5, 10)">
|
||||||
|
<ns0:path d="M 159,0 L 159,116 L 164,135 L 175,150 L 187,157 L 198,160 L 214,160 L 223,158 L 236,151 L 243,144 L 250,131 L 252,124 L 253,0 Z" fill="#808080" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 0,0 L 0,389 L 99,389 L 100,292 L 107,302 L 122,317 L 135,326 L 161,337 L 178,341 L 198,343 L 234,341 L 263,333 L 279,325 L 294,314 L 306,301 L 312,292 L 313,389 L 412,389 L 412,0 L 314,0 L 314,119 L 311,140 L 305,159 L 299,171 L 290,184 L 272,201 L 261,208 L 241,217 L 212,223 L 200,223 L 182,220 L 153,209 L 134,196 L 120,181 L 111,167 L 102,143 L 98,114 L 98,0 Z" fill="#808080" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
<g id="bottom" transform="translate(10.0, 514.0)">
|
||||||
|
<ns0:path d="M 2952,58 L 2945,66 L 2940,80 L 2940,102 L 2945,116 L 2951,122 L 2965,127 L 3026,129 L 3029,130 L 3035,136 L 3036,146 L 3031,153 L 3023,156 L 2942,156 L 2942,183 L 3037,183 L 3046,181 L 3056,175 L 3064,165 L 3069,150 L 3069,133 L 3063,117 L 3053,108 L 3040,103 L 2983,101 L 2977,97 L 2975,93 L 2975,85 L 2979,79 L 2990,76 L 3062,76 L 3062,49 L 2982,49 L 2964,52 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2820,63 L 2809,76 L 2804,86 L 2799,104 L 2799,128 L 2801,138 L 2808,154 L 2815,164 L 2827,174 L 2837,179 L 2860,183 L 2916,183 L 2916,155 L 2861,155 L 2850,151 L 2840,141 L 2837,136 L 2834,123 L 2834,110 L 2836,99 L 2839,92 L 2846,84 L 2857,79 L 2868,77 L 2916,77 L 2916,49 L 2851,50 L 2835,54 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2735,49 L 2735,183 L 2771,183 L 2771,49 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2501,63 L 2494,70 L 2487,82 L 2483,94 L 2481,107 L 2481,125 L 2483,138 L 2486,148 L 2494,162 L 2508,174 L 2527,181 L 2541,183 L 2599,183 L 2599,156 L 2535,155 L 2528,152 L 2523,147 L 2519,135 L 2520,129 L 2599,129 L 2599,103 L 2519,102 L 2519,97 L 2523,86 L 2527,81 L 2533,78 L 2543,76 L 2599,76 L 2599,49 L 2533,50 L 2512,56 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2332,49 L 2332,183 L 2366,183 L 2366,79 L 2368,77 L 2406,78 L 2416,83 L 2420,94 L 2420,183 L 2454,183 L 2454,87 L 2450,72 L 2446,65 L 2440,59 L 2431,54 L 2415,50 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2298,49 L 2228,49 L 2210,51 L 2200,54 L 2188,61 L 2176,74 L 2167,94 L 2164,120 L 2166,135 L 2170,148 L 2179,163 L 2192,175 L 2207,181 L 2224,183 L 2264,184 L 2262,196 L 2258,201 L 2251,205 L 2179,206 L 2179,232 L 2262,231 L 2279,225 L 2291,213 L 2298,192 Z M 2216,81 L 2228,77 L 2264,78 L 2263,156 L 2226,155 L 2213,150 L 2206,143 L 2201,131 L 2200,108 L 2205,92 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2011,49 L 2011,76 L 2081,76 L 2090,78 L 2100,86 L 2103,101 L 2038,103 L 2025,106 L 2015,111 L 2007,120 L 2002,136 L 2003,157 L 2010,171 L 2021,179 L 2038,183 L 2138,183 L 2138,92 L 2134,75 L 2127,64 L 2120,58 L 2112,54 L 2098,50 Z M 2038,137 L 2044,130 L 2051,128 L 2102,128 L 2103,156 L 2045,156 L 2041,154 L 2038,150 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 1370,49 L 1370,183 L 1405,183 L 1406,77 L 1445,78 L 1452,81 L 1455,84 L 1458,93 L 1458,183 L 1492,183 L 1492,86 L 1489,74 L 1485,66 L 1477,58 L 1469,54 L 1453,50 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 1212,49 L 1212,136 L 1214,152 L 1221,167 L 1230,175 L 1251,182 L 1334,183 L 1334,49 L 1300,49 L 1299,154 L 1260,154 L 1253,151 L 1249,147 L 1246,132 L 1246,49 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 1012,52 L 996,49 L 932,49 L 932,183 L 966,183 L 967,74 L 987,74 L 994,79 L 995,104 L 1028,104 L 1028,76 L 1026,66 L 1023,60 L 1018,55 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 898,49 L 828,49 L 805,52 L 795,56 L 786,62 L 774,76 L 768,88 L 765,99 L 764,128 L 768,145 L 779,164 L 793,176 L 806,181 L 823,183 L 863,184 L 863,191 L 860,198 L 850,205 L 778,206 L 778,232 L 861,231 L 880,224 L 886,219 L 891,212 L 896,198 L 898,183 Z M 814,82 L 827,77 L 863,78 L 862,156 L 831,156 L 821,154 L 811,149 L 804,141 L 801,134 L 799,122 L 800,104 L 805,91 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 730,52 L 713,49 L 649,49 L 649,183 L 684,183 L 685,74 L 705,74 L 712,79 L 713,104 L 746,104 L 746,78 L 744,67 L 740,59 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 525,62 L 517,70 L 509,84 L 504,106 L 504,126 L 506,139 L 516,161 L 531,174 L 550,181 L 564,183 L 622,183 L 622,156 L 558,155 L 551,152 L 546,147 L 542,136 L 543,129 L 622,129 L 622,103 L 542,102 L 545,87 L 550,81 L 556,78 L 566,76 L 622,76 L 622,49 L 556,50 L 540,54 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 197,49 L 197,183 L 231,183 L 232,77 L 271,78 L 278,81 L 281,84 L 284,93 L 284,183 L 318,183 L 318,86 L 315,74 L 311,66 L 303,58 L 295,54 L 279,50 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 1081,54 L 1070,61 L 1059,72 L 1051,86 L 1047,99 L 1046,127 L 1050,144 L 1056,156 L 1074,174 L 1086,180 L 1100,184 L 1133,184 L 1145,181 L 1158,175 L 1173,162 L 1182,147 L 1186,134 L 1187,104 L 1183,88 L 1177,76 L 1171,68 L 1162,60 L 1150,53 L 1138,49 L 1126,47 L 1107,47 L 1095,49 Z M 1109,75 L 1124,75 L 1136,80 L 1145,89 L 1151,105 L 1151,128 L 1147,140 L 1143,146 L 1135,153 L 1129,156 L 1119,158 L 1104,156 L 1094,150 L 1088,143 L 1082,126 L 1082,106 L 1087,91 L 1097,80 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2735,0 L 2735,32 L 2771,32 L 2771,0 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 2638,0 L 2638,48 L 2613,49 L 2613,75 L 2638,76 L 2638,147 L 2642,163 L 2651,175 L 2663,181 L 2673,183 L 2710,183 L 2710,157 L 2688,157 L 2681,154 L 2674,145 L 2672,133 L 2672,76 L 2710,75 L 2710,49 L 2673,49 L 2672,0 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 1764,0 L 1764,183 L 1802,183 L 1798,64 L 1799,40 L 1848,183 L 1888,183 L 1923,74 L 1935,41 L 1936,66 L 1933,183 L 1969,183 L 1969,0 L 1914,0 L 1867,141 L 1865,139 L 1819,0 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 1650,0 L 1616,0 L 1615,49 L 1580,49 L 1558,52 L 1546,57 L 1539,62 L 1528,75 L 1523,84 L 1519,97 L 1518,127 L 1520,138 L 1527,155 L 1531,161 L 1544,173 L 1563,181 L 1576,183 L 1650,183 Z M 1564,85 L 1575,79 L 1585,77 L 1616,78 L 1615,155 L 1575,154 L 1566,150 L 1559,143 L 1554,129 L 1554,105 L 1556,97 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 476,0 L 442,0 L 441,49 L 406,49 L 384,52 L 369,59 L 359,68 L 354,75 L 348,87 L 344,103 L 345,133 L 353,155 L 357,161 L 370,173 L 389,181 L 402,183 L 476,183 Z M 390,85 L 401,79 L 411,77 L 442,78 L 441,155 L 406,155 L 392,150 L 386,144 L 383,139 L 380,128 L 380,106 L 383,95 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<ns0:path d="M 0,0 L 0,125 L 2,142 L 10,160 L 22,171 L 35,178 L 53,183 L 71,185 L 87,185 L 110,182 L 123,178 L 140,168 L 149,159 L 153,152 L 158,130 L 158,0 L 121,0 L 121,122 L 119,133 L 115,142 L 109,149 L 97,155 L 83,157 L 66,156 L 54,152 L 44,143 L 40,135 L 37,119 L 37,0 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.0 KiB |
Reference in New Issue
Block a user