Compare commits
2 Commits
7a38de8ef2
...
2719ec244d
| Author | SHA1 | Date | |
|---|---|---|---|
| 2719ec244d | |||
| 8b80f68a19 |
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Output files
|
||||||
|
*.stl
|
||||||
|
*.3mf
|
||||||
|
*_output*
|
||||||
|
logo_output*
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
9
Color logo - no background.svg
Executable file
9
Color logo - no background.svg
Executable file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
62
IMPROVEMENTS.md
Normal file
62
IMPROVEMENTS.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Improvements to split_png_by_brightness.py
|
||||||
|
|
||||||
|
## Version 2 Fixes
|
||||||
|
|
||||||
|
### 1. Proper Hole Handling
|
||||||
|
**Problem**: Letters with enclosed shapes (like "d", "o", "g") were missing their holes, making them appear filled.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Switched from `scikit-image` contour detection to OpenCV's `cv2.findContours` with `RETR_CCOMP` mode
|
||||||
|
- This detects contour hierarchy (parent/child relationships)
|
||||||
|
- Holes are now included in the same SVG path as their parent contour
|
||||||
|
- Using `fill-rule="evenodd"` properly renders the holes
|
||||||
|
|
||||||
|
### 2. Better Color Detection
|
||||||
|
**Problem**: Antialiasing pixels around edges were being included, creating artifacts and incorrect bounding boxes.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Changed from single threshold to tight ranges
|
||||||
|
- Black text: brightness < 40 (was < 50)
|
||||||
|
- Grey icon: brightness 108-118 (was 80-120)
|
||||||
|
- This excludes antialiased edge pixels and prevents the grey icon from spanning the full image width
|
||||||
|
|
||||||
|
### 3. Automatic Bounding Box Cropping
|
||||||
|
**Problem**: SVG files contained a lot of empty space.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Added automatic cropping to bounding box by default
|
||||||
|
- Each SVG is sized to fit its content exactly
|
||||||
|
- `--no-crop` flag available if full canvas is needed
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
**Before (v1)**:
|
||||||
|
- um_black.svg: 139K, 27 separate paths, many filled letters
|
||||||
|
- um_grey.svg: 253K, 1609 paths, antialiasing artifacts, full width (3613px)
|
||||||
|
|
||||||
|
**After (v2)**:
|
||||||
|
- um_black.svg: 6.1K, 21 shape groups with proper holes, cropped to 3070x233
|
||||||
|
- um_grey.svg: 728B, 2 shape groups, clean edges, cropped to 413x390
|
||||||
|
|
||||||
|
## Dependencies Added
|
||||||
|
- opencv-python==4.13.0.92
|
||||||
|
|
||||||
|
## Default Parameters
|
||||||
|
- Black threshold: < 40 brightness
|
||||||
|
- Grey range: 108-118 brightness
|
||||||
|
- Cropping: enabled by default
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```bash
|
||||||
|
# Default (optimized for Underground Magnetics logo)
|
||||||
|
python split_png_by_brightness.py input.png --prefix output
|
||||||
|
|
||||||
|
# Custom thresholds
|
||||||
|
python split_png_by_brightness.py input.png \
|
||||||
|
--dark-threshold 40 \
|
||||||
|
--mid-min 108 \
|
||||||
|
--mid-max 118
|
||||||
|
|
||||||
|
# Disable cropping
|
||||||
|
python split_png_by_brightness.py input.png --no-crop
|
||||||
|
```
|
||||||
66
README_underground_magnetics.md
Normal file
66
README_underground_magnetics.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Underground Magnetics Logo - Color Separation
|
||||||
|
|
||||||
|
Successfully split the Underground Magnetics EPS logo into separate SVG files by color for multi-color 3D printing.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. **EPS to PNG Conversion**: Used ImageMagick to convert `underground-magnetics.eps` to PNG format
|
||||||
|
- Output: `underground-magnetics-0.png` and `underground-magnetics-1.png`
|
||||||
|
|
||||||
|
2. **Color Separation**: Used `split_png_by_brightness.py` to extract separate colors
|
||||||
|
- Analyzed brightness values to distinguish grey icon from black text
|
||||||
|
- Created separate SVG files for each color region
|
||||||
|
|
||||||
|
## Output Files
|
||||||
|
|
||||||
|
- **underground-magnetics_grey.svg** - Grey "UM" icon (126,801 pixels, 1609 paths)
|
||||||
|
- **underground-magnetics_black.svg** - Black "Underground Magnetics" text (237,130 pixels, 27 paths)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To convert these SVG files to 3D models for multi-color printing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Convert grey icon to STL
|
||||||
|
python svg_logo_to_stl.py underground-magnetics_grey.svg um_grey --width-mm 100 --base-thickness 1.5 --feature-height 2.5
|
||||||
|
|
||||||
|
# Convert black text to STL
|
||||||
|
python svg_logo_to_stl.py underground-magnetics_black.svg um_black --width-mm 100 --base-thickness 1.5 --feature-height 2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tools Created
|
||||||
|
|
||||||
|
### split_png_by_brightness.py
|
||||||
|
Splits PNG images into separate SVG files based on brightness/color clusters.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
python split_png_by_brightness.py <input.png> [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--output-dir, -o Output directory (default: same as input)
|
||||||
|
--prefix, -p Prefix for output files (default: input filename)
|
||||||
|
--dark-threshold INT Brightness threshold for dark/black (default: 50)
|
||||||
|
--mid-threshold INT Brightness threshold for mid/grey (default: 150)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
python split_png_by_brightness.py underground-magnetics-0.png --prefix underground-magnetics
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates separate SVG files for each color region detected in the PNG.
|
||||||
|
|
||||||
|
## Color Detection
|
||||||
|
|
||||||
|
The script analyzes pixel brightness to separate colors:
|
||||||
|
- **Dark pixels** (brightness < 50): Black text → `_black.svg`
|
||||||
|
- **Mid-range pixels** (50 ≤ brightness < 150): Grey icon → `_grey.svg`
|
||||||
|
- **Light pixels** (brightness ≥ 150): White background (ignored)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Import the separate SVG files into the 3D conversion tool
|
||||||
|
2. Generate STL files for each color
|
||||||
|
3. Create 3MF assembly with multiple selectable parts
|
||||||
|
4. Import into BambuStudio for multi-color printing
|
||||||
28
command.txt
Normal file
28
command.txt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# SVG to STL Conversion Command
|
||||||
|
# Note: The DYLD_LIBRARY_PATH is needed for Cairo to work on macOS
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Output files:
|
||||||
|
# - logo_output_base.stl - Black base plate (Z: 0 to 1.5mm)
|
||||||
|
# - logo_output_green_icon.stl - Green microchip icon at top (Z: 1.5 to 4.0mm)
|
||||||
|
# - logo_output_white.stl - White "SPARKSOFT DESIGN" text (Z: 1.5 to 4.0mm)
|
||||||
|
# - 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_parts.3mf - 4 separate selectable parts (BEST for multi-color!)
|
||||||
|
|
||||||
|
# Import logo_output_parts.3mf into BambuStudio
|
||||||
|
# You'll see 4 separate parts that can each be assigned different filaments:
|
||||||
|
# 1. base - black base plate
|
||||||
|
# 2. green_icon - microchip icon
|
||||||
|
# 3. white_text - middle text
|
||||||
|
# 4. green_text - bottom text
|
||||||
|
# Parts do NOT overlap in XY space, preventing slicing issues
|
||||||
|
# Total model height: 4.0mm (1.5mm base + 2.5mm features)
|
||||||
|
|
||||||
|
# IMPORTANT: Scale must be 254mm or larger for 0.4mm nozzle
|
||||||
|
# Smaller sizes cause features to be too small for proper toolpath generation
|
||||||
21
requirements.txt
Normal file
21
requirements.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
cairocffi==1.7.1
|
||||||
|
CairoSVG==2.9.0
|
||||||
|
cffi==2.0.0
|
||||||
|
cssselect2==0.9.0
|
||||||
|
defusedxml==0.7.1
|
||||||
|
ImageIO==2.37.3
|
||||||
|
lazy-loader==0.5
|
||||||
|
mapbox_earcut==2.0.0
|
||||||
|
networkx==3.4.2
|
||||||
|
numpy==2.2.6
|
||||||
|
opencv-python==4.13.0.92
|
||||||
|
packaging==26.2
|
||||||
|
pillow==12.2.0
|
||||||
|
pycparser==3.0
|
||||||
|
scikit-image==0.25.2
|
||||||
|
scipy==1.15.3
|
||||||
|
shapely==2.1.2
|
||||||
|
tifffile==2025.5.10
|
||||||
|
tinycss2==1.5.1
|
||||||
|
trimesh==4.12.2
|
||||||
|
webencodings==0.5.1
|
||||||
265
split_png_by_brightness.py
Normal file
265
split_png_by_brightness.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Split a PNG file into multiple SVG files based on brightness/color clusters.
|
||||||
|
Useful for multi-color 3D printing when the source is a rasterized logo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
from pathlib import Path
|
||||||
|
import cv2
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
def extract_color_regions(image_path: Path, dark_threshold: int = 30, mid_min: int = 108, mid_max: int = 118):
|
||||||
|
"""
|
||||||
|
Extract different color regions from PNG based on brightness.
|
||||||
|
Uses tighter thresholds to avoid antialiasing artifacts.
|
||||||
|
Returns dict of {color_name: binary_mask}
|
||||||
|
"""
|
||||||
|
img = Image.open(image_path).convert('RGBA')
|
||||||
|
img_array = np.array(img)
|
||||||
|
|
||||||
|
# Get alpha mask (non-transparent pixels)
|
||||||
|
alpha = img_array[..., 3] > 128
|
||||||
|
|
||||||
|
# Calculate brightness for each pixel
|
||||||
|
rgb = img_array[..., :3]
|
||||||
|
brightness = np.mean(rgb, axis=-1)
|
||||||
|
|
||||||
|
# Create masks for different brightness levels
|
||||||
|
masks = {}
|
||||||
|
|
||||||
|
# Dark pixels (black text) - very tight threshold to avoid antialiasing
|
||||||
|
dark_mask = alpha & (brightness < dark_threshold)
|
||||||
|
if np.sum(dark_mask) > 100:
|
||||||
|
masks['black'] = dark_mask
|
||||||
|
|
||||||
|
# Mid-range pixels (grey icon) - tight range to get only the core grey
|
||||||
|
mid_mask = alpha & (brightness >= mid_min) & (brightness <= mid_max)
|
||||||
|
if np.sum(mid_mask) > 100:
|
||||||
|
masks['grey'] = mid_mask
|
||||||
|
|
||||||
|
return masks
|
||||||
|
|
||||||
|
|
||||||
|
def compute_contour_area(contour):
|
||||||
|
"""Compute the signed area of a contour (positive = clockwise, negative = counter-clockwise)"""
|
||||||
|
if len(contour) < 3:
|
||||||
|
return 0
|
||||||
|
# Shoelace formula
|
||||||
|
x = contour[:, 0]
|
||||||
|
y = contour[:, 1]
|
||||||
|
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
|
||||||
|
|
||||||
|
|
||||||
|
def mask_to_svg_paths_with_holes(mask: np.ndarray, simplify_epsilon: float = 1.0):
|
||||||
|
"""
|
||||||
|
Convert a binary mask to SVG path data with proper hole handling.
|
||||||
|
Returns list of path d attributes with fill-rule evenodd.
|
||||||
|
"""
|
||||||
|
# Convert to uint8 for OpenCV
|
||||||
|
mask_uint8 = (mask * 255).astype(np.uint8)
|
||||||
|
|
||||||
|
# Find contours with hierarchy (to detect holes)
|
||||||
|
contours, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
if hierarchy is None or len(contours) == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Simplify contours
|
||||||
|
simplified_contours = []
|
||||||
|
for contour in contours:
|
||||||
|
if simplify_epsilon > 0:
|
||||||
|
simplified = cv2.approxPolyDP(contour, simplify_epsilon, True)
|
||||||
|
else:
|
||||||
|
simplified = contour
|
||||||
|
simplified_contours.append(simplified)
|
||||||
|
|
||||||
|
# Group contours by parent/child relationship
|
||||||
|
# hierarchy format: [Next, Previous, First_Child, Parent]
|
||||||
|
hierarchy = hierarchy[0]
|
||||||
|
|
||||||
|
# Find all top-level contours (no parent)
|
||||||
|
top_level_indices = [i for i in range(len(hierarchy)) if hierarchy[i][3] == -1]
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
for top_idx in top_level_indices:
|
||||||
|
contour = simplified_contours[top_idx]
|
||||||
|
|
||||||
|
if len(contour) < 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Start with outer contour
|
||||||
|
path_data = contour_to_svg_path(contour)
|
||||||
|
|
||||||
|
# Find all children (holes) of this contour
|
||||||
|
child_idx = hierarchy[top_idx][2]
|
||||||
|
while child_idx != -1:
|
||||||
|
child_contour = simplified_contours[child_idx]
|
||||||
|
if len(child_contour) >= 3:
|
||||||
|
# Add hole to the same path (evenodd fill-rule will handle it)
|
||||||
|
path_data += " " + contour_to_svg_path(child_contour)
|
||||||
|
|
||||||
|
# Move to next sibling
|
||||||
|
child_idx = hierarchy[child_idx][0]
|
||||||
|
|
||||||
|
paths.append(path_data)
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def contour_to_svg_path(contour):
|
||||||
|
"""Convert OpenCV contour to SVG path data"""
|
||||||
|
# OpenCV contours are shape (N, 1, 2)
|
||||||
|
points = contour.reshape(-1, 2)
|
||||||
|
|
||||||
|
if len(points) < 2:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
path_data = f"M {points[0, 0]},{points[0, 1]}"
|
||||||
|
for point in points[1:]:
|
||||||
|
path_data += f" L {point[0]},{point[1]}"
|
||||||
|
path_data += " Z"
|
||||||
|
|
||||||
|
return path_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_mask_bounds(mask: np.ndarray):
|
||||||
|
"""Get the bounding box of a binary mask"""
|
||||||
|
rows = np.any(mask, axis=1)
|
||||||
|
cols = np.any(mask, axis=0)
|
||||||
|
|
||||||
|
if not np.any(rows) or not np.any(cols):
|
||||||
|
return None
|
||||||
|
|
||||||
|
y_min, y_max = np.where(rows)[0][[0, -1]]
|
||||||
|
x_min, x_max = np.where(cols)[0][[0, -1]]
|
||||||
|
|
||||||
|
return (x_min, y_min, x_max + 1, y_max + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_svg_from_mask(mask: np.ndarray, output_path: Path, color: str, image_size: tuple, crop_to_bounds: bool = True):
|
||||||
|
"""Create SVG file from binary mask with proper hole handling and optional cropping"""
|
||||||
|
width, height = image_size
|
||||||
|
|
||||||
|
# Get bounding box of the mask
|
||||||
|
if crop_to_bounds:
|
||||||
|
bounds = get_mask_bounds(mask)
|
||||||
|
if bounds is None:
|
||||||
|
print(f"WARNING: No content found for {color}")
|
||||||
|
return
|
||||||
|
|
||||||
|
x_min, y_min, x_max, y_max = bounds
|
||||||
|
cropped_width = x_max - x_min
|
||||||
|
cropped_height = y_max - y_min
|
||||||
|
|
||||||
|
# Crop the mask
|
||||||
|
cropped_mask = mask[y_min:y_max, x_min:x_max]
|
||||||
|
else:
|
||||||
|
x_min, y_min = 0, 0
|
||||||
|
cropped_width, cropped_height = width, height
|
||||||
|
cropped_mask = mask
|
||||||
|
|
||||||
|
# Create SVG root with cropped dimensions
|
||||||
|
svg = ET.Element('svg', {
|
||||||
|
'xmlns': 'http://www.w3.org/2000/svg',
|
||||||
|
'width': str(cropped_width),
|
||||||
|
'height': str(cropped_height),
|
||||||
|
'viewBox': f'0 0 {cropped_width} {cropped_height}'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Convert mask to paths with hole detection
|
||||||
|
paths = mask_to_svg_paths_with_holes(cropped_mask, simplify_epsilon=1.0)
|
||||||
|
|
||||||
|
# Color mapping
|
||||||
|
color_hex = {
|
||||||
|
'black': '#000000',
|
||||||
|
'grey': '#808080',
|
||||||
|
'gray': '#808080',
|
||||||
|
'white': '#FFFFFF',
|
||||||
|
}.get(color, color)
|
||||||
|
|
||||||
|
# Add paths to SVG (no translation needed since we cropped the mask)
|
||||||
|
for path_data in paths:
|
||||||
|
ET.SubElement(svg, 'path', {
|
||||||
|
'd': path_data,
|
||||||
|
'fill': color_hex,
|
||||||
|
'fill-rule': 'evenodd'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Write SVG
|
||||||
|
tree = ET.ElementTree(svg)
|
||||||
|
ET.indent(tree, space=' ')
|
||||||
|
tree.write(output_path, encoding='utf-8', xml_declaration=True)
|
||||||
|
|
||||||
|
if crop_to_bounds:
|
||||||
|
print(f"Created {output_path} with color {color} ({len(paths)} shape groups, cropped to {cropped_width}x{cropped_height})")
|
||||||
|
else:
|
||||||
|
print(f"Created {output_path} with color {color} ({len(paths)} shape groups)")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Split PNG into separate SVG files by brightness/color'
|
||||||
|
)
|
||||||
|
parser.add_argument('input', help='Input PNG file')
|
||||||
|
parser.add_argument('--output-dir', '-o', help='Output directory (default: same as input)')
|
||||||
|
parser.add_argument('--prefix', '-p', help='Prefix for output files (default: input filename)')
|
||||||
|
parser.add_argument('--dark-threshold', type=int, default=30,
|
||||||
|
help='Brightness threshold for dark/black elements (default: 30)')
|
||||||
|
parser.add_argument('--mid-min', type=int, default=108,
|
||||||
|
help='Minimum brightness for mid/grey elements (default: 108)')
|
||||||
|
parser.add_argument('--mid-max', type=int, default=118,
|
||||||
|
help='Maximum brightness for mid/grey elements (default: 118)')
|
||||||
|
parser.add_argument('--no-crop', action='store_true',
|
||||||
|
help='Do not crop to bounding box (default: crop enabled)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
input_path = Path(args.input)
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
print(f"ERROR: File not found: {input_path}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Setup output directory
|
||||||
|
if args.output_dir:
|
||||||
|
output_dir = Path(args.output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
else:
|
||||||
|
output_dir = input_path.parent
|
||||||
|
|
||||||
|
# Setup prefix
|
||||||
|
prefix = args.prefix if args.prefix else input_path.stem
|
||||||
|
|
||||||
|
# Load image to get size
|
||||||
|
img = Image.open(input_path)
|
||||||
|
image_size = img.size
|
||||||
|
|
||||||
|
# Extract color regions
|
||||||
|
print(f"Analyzing colors in {input_path}...")
|
||||||
|
masks = extract_color_regions(input_path, args.dark_threshold, args.mid_min, args.mid_max)
|
||||||
|
|
||||||
|
if not masks:
|
||||||
|
print("No color regions found!")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\nFound {len(masks)} color regions:")
|
||||||
|
for color, mask in masks.items():
|
||||||
|
pixel_count = np.sum(mask)
|
||||||
|
print(f" {color}: {pixel_count:,} pixels")
|
||||||
|
|
||||||
|
# Create SVG for each color
|
||||||
|
print(f"\nCreating separate SVG files...")
|
||||||
|
crop_enabled = not args.no_crop
|
||||||
|
for color, mask in masks.items():
|
||||||
|
output_path = output_dir / f"{prefix}_{color}.svg"
|
||||||
|
create_svg_from_mask(mask, output_path, color, image_size, crop_to_bounds=crop_enabled)
|
||||||
|
|
||||||
|
print(f"\nDone! Created {len(masks)} SVG files in {output_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -20,9 +20,10 @@ def render_svg_to_image(svg_path: Path, pixel_width: int = 2048) -> Image.Image:
|
|||||||
return Image.open(png_output).convert("RGBA")
|
return Image.open(png_output).convert("RGBA")
|
||||||
|
|
||||||
|
|
||||||
def clean_mask(mask: np.ndarray, min_size: int = 32) -> np.ndarray:
|
def clean_mask(mask: np.ndarray, min_size: int = 10) -> np.ndarray:
|
||||||
|
# Use smaller min_size to preserve small features like parts of the icon
|
||||||
if min_size > 0:
|
if min_size > 0:
|
||||||
cleaned = morphology.remove_small_objects(mask, max_size=min_size - 1)
|
cleaned = morphology.remove_small_objects(mask, min_size=min_size)
|
||||||
else:
|
else:
|
||||||
cleaned = mask
|
cleaned = mask
|
||||||
structure = np.ones((3, 3), dtype=bool)
|
structure = np.ones((3, 3), dtype=bool)
|
||||||
@@ -49,8 +50,10 @@ def signed_area(coords: np.ndarray) -> float:
|
|||||||
return 0.5 * np.sum(x[:-1] * y[1:] - x[1:] * y[:-1])
|
return 0.5 * np.sum(x[:-1] * y[1:] - x[1:] * y[:-1])
|
||||||
|
|
||||||
|
|
||||||
def mask_to_polygons(mask: np.ndarray, min_area: float = 10.0, simplify_tolerance: float = 1.0):
|
def mask_to_polygons(mask: np.ndarray, min_area: float = 1.0, simplify_tolerance: float = 0.25, debug=False):
|
||||||
contours = measure.find_contours(mask.astype(np.uint8), 0.5)
|
contours = measure.find_contours(mask.astype(np.uint8), 0.5)
|
||||||
|
if debug:
|
||||||
|
print(f" Found {len(contours)} contours")
|
||||||
shapes: list[tuple[Polygon, float]] = []
|
shapes: list[tuple[Polygon, float]] = []
|
||||||
for contour in contours:
|
for contour in contours:
|
||||||
if contour.shape[0] < 4:
|
if contour.shape[0] < 4:
|
||||||
@@ -86,15 +89,22 @@ def mask_to_polygons(mask: np.ndarray, min_area: float = 10.0, simplify_toleranc
|
|||||||
hole_list.append(hole.exterior.coords)
|
hole_list.append(hole.exterior.coords)
|
||||||
assigned_holes.add(hole)
|
assigned_holes.add(hole)
|
||||||
poly = Polygon(exterior.exterior.coords, hole_list)
|
poly = Polygon(exterior.exterior.coords, hole_list)
|
||||||
poly = poly.simplify(simplify_tolerance)
|
poly = poly.simplify(simplify_tolerance, preserve_topology=True)
|
||||||
if poly.is_valid and poly.area >= min_area:
|
if poly.is_valid and poly.area >= min_area:
|
||||||
polygons.append(poly)
|
polygons.append(poly)
|
||||||
|
elif debug:
|
||||||
|
print(f" Skipped polygon: valid={poly.is_valid}, area={poly.area:.1f}")
|
||||||
|
|
||||||
for hole in holes:
|
for hole in holes:
|
||||||
if hole not in assigned_holes:
|
if hole not in assigned_holes:
|
||||||
poly = hole.simplify(simplify_tolerance)
|
poly = hole.simplify(simplify_tolerance, preserve_topology=True)
|
||||||
if poly.is_valid and poly.area >= min_area:
|
if poly.is_valid and poly.area >= min_area:
|
||||||
polygons.append(poly)
|
polygons.append(poly)
|
||||||
|
elif debug:
|
||||||
|
print(f" Skipped unassigned hole: valid={poly.is_valid}, area={poly.area:.1f}")
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print(f" Output: {len(polygons)} polygons (from {len(exteriors)} exteriors, {len(holes)} holes)")
|
||||||
|
|
||||||
return polygons
|
return polygons
|
||||||
|
|
||||||
@@ -115,7 +125,37 @@ def create_extruded_mesh(polygons, height_mm: float, scale: float, y_flip: bool
|
|||||||
meshes.append(mesh)
|
meshes.append(mesh)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
return trimesh.util.concatenate(meshes) if meshes else None
|
|
||||||
|
if not meshes:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Process each mesh individually to ensure they're watertight
|
||||||
|
watertight_meshes = []
|
||||||
|
skipped_count = 0
|
||||||
|
for mesh in meshes:
|
||||||
|
# Try to make it watertight
|
||||||
|
trimesh.repair.fill_holes(mesh)
|
||||||
|
trimesh.repair.fix_normals(mesh)
|
||||||
|
trimesh.repair.fix_winding(mesh)
|
||||||
|
|
||||||
|
# If still not watertight, try to split and fix components
|
||||||
|
if not mesh.is_watertight:
|
||||||
|
# Split into connected components
|
||||||
|
components = mesh.split(only_watertight=False)
|
||||||
|
for comp in components:
|
||||||
|
trimesh.repair.fill_holes(comp)
|
||||||
|
if comp.is_watertight or comp.is_volume:
|
||||||
|
watertight_meshes.append(comp)
|
||||||
|
else:
|
||||||
|
skipped_count += 1
|
||||||
|
else:
|
||||||
|
watertight_meshes.append(mesh)
|
||||||
|
|
||||||
|
if not watertight_meshes:
|
||||||
|
# Fallback: return combined mesh even if not perfect
|
||||||
|
return trimesh.util.concatenate(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):
|
||||||
@@ -128,19 +168,45 @@ def build_logo_meshes(svg_path: Path, width_mm: float, base_thickness: float, fe
|
|||||||
green_mask = clean_mask(color_mask(img_array, (57, 233, 145), tolerance=80))
|
green_mask = clean_mask(color_mask(img_array, (57, 233, 145), tolerance=80))
|
||||||
white_mask = clean_mask(white_color_mask(img_array, min_brightness=220))
|
white_mask = clean_mask(white_color_mask(img_array, min_brightness=220))
|
||||||
|
|
||||||
green_polys = mask_to_polygons(green_mask)
|
# Split green mask into top (icon) and bottom (text) regions
|
||||||
|
# Find where white text is located to use as separator
|
||||||
|
white_rows = np.any(white_mask, axis=1)
|
||||||
|
white_y_coords = np.where(white_rows)[0]
|
||||||
|
|
||||||
|
if len(white_y_coords) > 0:
|
||||||
|
white_y_mid = (white_y_coords[0] + white_y_coords[-1]) // 2
|
||||||
|
|
||||||
|
# Green icon: everything above white text
|
||||||
|
green_icon_mask = green_mask.copy()
|
||||||
|
green_icon_mask[white_y_coords[0]:, :] = False # Zero out everything from white text onwards
|
||||||
|
|
||||||
|
# Green bottom text: everything below white text
|
||||||
|
green_bottom_mask = green_mask.copy()
|
||||||
|
green_bottom_mask[:white_y_coords[-1], :] = False # Zero out everything before end of white text
|
||||||
|
|
||||||
|
green_icon_polys = mask_to_polygons(green_icon_mask)
|
||||||
|
green_bottom_polys = mask_to_polygons(green_bottom_mask)
|
||||||
|
else:
|
||||||
|
# No white text found, treat all green as one
|
||||||
|
green_icon_polys = mask_to_polygons(green_mask)
|
||||||
|
green_bottom_polys = []
|
||||||
|
|
||||||
white_polys = mask_to_polygons(white_mask)
|
white_polys = mask_to_polygons(white_mask)
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
green_mesh = create_extruded_mesh(green_polys, feature_height, scale)
|
# Features sit exactly on top of the base (no overlap to avoid slicing issues)
|
||||||
|
green_icon_mesh = create_extruded_mesh(green_icon_polys, feature_height, scale)
|
||||||
|
green_bottom_mesh = create_extruded_mesh(green_bottom_polys, feature_height, scale)
|
||||||
white_mesh = create_extruded_mesh(white_polys, feature_height, scale)
|
white_mesh = create_extruded_mesh(white_polys, feature_height, scale)
|
||||||
for mesh in (green_mesh, white_mesh):
|
|
||||||
|
for mesh in (green_icon_mesh, green_bottom_mesh, white_mesh):
|
||||||
if mesh is not None:
|
if mesh is not None:
|
||||||
|
# Position features to start exactly at the top of the base
|
||||||
mesh.apply_translation((0.0, height_mm, base_thickness))
|
mesh.apply_translation((0.0, height_mm, base_thickness))
|
||||||
|
|
||||||
return base, green_mesh, white_mesh
|
return base, green_icon_mesh, green_bottom_mesh, white_mesh
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -196,16 +262,23 @@ def save_3mf(meshes: dict[str, trimesh.Trimesh], path: Path):
|
|||||||
|
|
||||||
object_ids = []
|
object_ids = []
|
||||||
current_id = 1
|
current_id = 1
|
||||||
material_map = {'base': 1, 'green': 2, 'white': 3}
|
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
|
||||||
material_id = material_map.get(name, 1)
|
material_id = material_map.get(name, 0)
|
||||||
resource = _mesh_to_3mf_object(mesh, current_id, name, material_id)
|
resource = _mesh_to_3mf_object(mesh, current_id, name, material_id)
|
||||||
resources.append(resource)
|
resources.append(resource)
|
||||||
object_ids.append(current_id)
|
object_ids.append(current_id)
|
||||||
current_id += 1
|
current_id += 1
|
||||||
|
|
||||||
|
build = ET.SubElement(model, 'build')
|
||||||
|
|
||||||
|
# If we have only one object, add it directly to build (no assembly)
|
||||||
|
# If we have multiple objects, create an assembly
|
||||||
|
if len(object_ids) == 1:
|
||||||
|
ET.SubElement(build, 'item', {'objectid': str(object_ids[0])})
|
||||||
|
else:
|
||||||
assembly_id = current_id
|
assembly_id = current_id
|
||||||
assembly = ET.SubElement(resources, 'object', {
|
assembly = ET.SubElement(resources, 'object', {
|
||||||
'id': str(assembly_id),
|
'id': str(assembly_id),
|
||||||
@@ -217,8 +290,6 @@ def save_3mf(meshes: dict[str, trimesh.Trimesh], path: Path):
|
|||||||
ET.SubElement(components_el, 'component', {
|
ET.SubElement(components_el, 'component', {
|
||||||
'objectid': str(object_id)
|
'objectid': str(object_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
build = ET.SubElement(model, 'build')
|
|
||||||
ET.SubElement(build, 'item', {'objectid': str(assembly_id)})
|
ET.SubElement(build, 'item', {'objectid': str(assembly_id)})
|
||||||
|
|
||||||
xml_data = ET.tostring(model, encoding='utf-8', xml_declaration=True)
|
xml_data = ET.tostring(model, encoding='utf-8', xml_declaration=True)
|
||||||
@@ -230,29 +301,52 @@ def save_3mf(meshes: dict[str, trimesh.Trimesh], path: Path):
|
|||||||
|
|
||||||
|
|
||||||
def save_3mf_parts(meshes: dict[str, trimesh.Trimesh], path: Path):
|
def save_3mf_parts(meshes: dict[str, trimesh.Trimesh], path: Path):
|
||||||
|
"""
|
||||||
|
Save meshes as separate objects in a 3MF assembly, similar to nameplate.3mf.
|
||||||
|
Each mesh becomes a separate selectable component in BambuStudio.
|
||||||
|
"""
|
||||||
model = ET.Element('model', {
|
model = ET.Element('model', {
|
||||||
'xmlns': 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02',
|
'xmlns': 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02',
|
||||||
'xmlns:m': 'http://schemas.microsoft.com/3dmanufacturing/material/2015/02',
|
'xmlns:m': 'http://schemas.microsoft.com/3dmanufacturing/material/2015/02',
|
||||||
'unit': 'millimeter'
|
'unit': 'millimeter'
|
||||||
})
|
})
|
||||||
resources = ET.SubElement(model, 'resources')
|
resources = ET.SubElement(model, 'resources')
|
||||||
_add_basematerials(resources)
|
basematerials_id = _add_basematerials(resources)
|
||||||
|
|
||||||
|
# Create separate object entries for each mesh
|
||||||
object_ids = []
|
object_ids = []
|
||||||
current_id = 1
|
current_id = 1
|
||||||
material_map = {'base': 1, 'green': 2, 'white': 3}
|
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
|
||||||
material_id = material_map.get(name, 1)
|
material_id = material_map.get(name, 0)
|
||||||
resource = _mesh_to_3mf_object(mesh, current_id, name, material_id)
|
resource = _mesh_to_3mf_object(mesh, current_id, name, material_id)
|
||||||
resources.append(resource)
|
resources.append(resource)
|
||||||
object_ids.append(current_id)
|
object_ids.append(current_id)
|
||||||
current_id += 1
|
current_id += 1
|
||||||
|
|
||||||
build = ET.SubElement(model, 'build')
|
# Create an assembly object that references all the individual objects
|
||||||
|
# This is key: the assembly is what gets added to the build, not the individual objects
|
||||||
|
assembly_id = current_id
|
||||||
|
assembly = ET.SubElement(resources, 'object', {
|
||||||
|
'id': str(assembly_id),
|
||||||
|
'name': 'logo_assembly',
|
||||||
|
'type': 'model'
|
||||||
|
})
|
||||||
|
components_el = ET.SubElement(assembly, 'components')
|
||||||
|
|
||||||
|
# Add each object as a component in the assembly
|
||||||
|
# Identity transform (no translation/rotation)
|
||||||
for object_id in object_ids:
|
for object_id in object_ids:
|
||||||
ET.SubElement(build, 'item', {'objectid': str(object_id)})
|
ET.SubElement(components_el, 'component', {
|
||||||
|
'objectid': str(object_id),
|
||||||
|
'transform': '1 0 0 0 1 0 0 0 1 0 0 0' # Identity matrix
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add only the assembly to the build (not the individual objects)
|
||||||
|
build = ET.SubElement(model, 'build')
|
||||||
|
ET.SubElement(build, 'item', {'objectid': str(assembly_id)})
|
||||||
|
|
||||||
xml_data = ET.tostring(model, encoding='utf-8', xml_declaration=True)
|
xml_data = ET.tostring(model, encoding='utf-8', xml_declaration=True)
|
||||||
with zipfile.ZipFile(path, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(path, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
@@ -283,7 +377,7 @@ def main():
|
|||||||
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_mesh, white_mesh = build_logo_meshes(
|
base_mesh, green_icon_mesh, green_bottom_mesh, white_mesh = build_logo_meshes(
|
||||||
svg_path,
|
svg_path,
|
||||||
args.width_mm,
|
args.width_mm,
|
||||||
args.base_thickness,
|
args.base_thickness,
|
||||||
@@ -292,37 +386,45 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
base_path = output_prefix.with_name(output_prefix.stem + "_base.stl")
|
base_path = output_prefix.with_name(output_prefix.stem + "_base.stl")
|
||||||
green_path = output_prefix.with_name(output_prefix.stem + "_green.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")
|
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 = save_mesh(green_mesh, green_path)
|
saved_green_icon = save_mesh(green_icon_mesh, green_icon_path)
|
||||||
|
saved_green_bottom = save_mesh(green_bottom_mesh, green_bottom_path)
|
||||||
saved_white = save_mesh(white_mesh, white_path)
|
saved_white = save_mesh(white_mesh, white_path)
|
||||||
|
|
||||||
assembled_mesh = trimesh.util.concatenate([m for m in (base_mesh, green_mesh, white_mesh) if m is not None])
|
# 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])
|
||||||
assembled_mesh.export(assembled_stl_path)
|
assembled_mesh.export(assembled_stl_path)
|
||||||
|
|
||||||
|
# For 3MF assembled version, export as a SINGLE mesh object
|
||||||
|
# BambuStudio can then use the paint tool to assign colors
|
||||||
save_3mf({
|
save_3mf({
|
||||||
'base': base_mesh,
|
'logo': assembled_mesh,
|
||||||
'green': green_mesh,
|
|
||||||
'white': white_mesh,
|
|
||||||
}, assembled_3mf_path)
|
}, assembled_3mf_path)
|
||||||
|
|
||||||
|
# Export as separate parts in assembly for multi-color selection
|
||||||
save_3mf_parts({
|
save_3mf_parts({
|
||||||
'base': base_mesh,
|
'base': base_mesh,
|
||||||
'green': green_mesh,
|
'green_icon': green_icon_mesh,
|
||||||
'white': white_mesh,
|
'white_text': white_mesh,
|
||||||
|
'green_text': green_bottom_mesh,
|
||||||
}, parts_3mf_path)
|
}, parts_3mf_path)
|
||||||
|
|
||||||
print("Created files:")
|
print("Created files:")
|
||||||
if saved_base:
|
if saved_base:
|
||||||
print(f" Base STL: {saved_base}")
|
print(f" Base STL: {saved_base}")
|
||||||
if saved_green:
|
if saved_green_icon:
|
||||||
print(f" Green STL: {saved_green}")
|
print(f" Green icon STL: {saved_green_icon}")
|
||||||
|
if saved_green_bottom:
|
||||||
|
print(f" Green bottom text STL: {saved_green_bottom}")
|
||||||
if saved_white:
|
if saved_white:
|
||||||
print(f" White STL: {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}")
|
||||||
|
|||||||
24
um_black.svg
Normal file
24
um_black.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="3070" height="233" viewBox="0 0 3070 233">
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<path d="M 2735,49 L 2735,183 L 2771,183 L 2771,49 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<path d="M 2735,0 L 2735,32 L 2771,32 L 2771,0 Z" fill="#000000" fill-rule="evenodd" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
5
um_grey.svg
Normal file
5
um_grey.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="413" height="390" viewBox="0 0 413 390">
|
||||||
|
<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" />
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 728 B |
BIN
underground-magnetics.eps
Normal file
BIN
underground-magnetics.eps
Normal file
Binary file not shown.
Reference in New Issue
Block a user