Fix SVG to STL conversion for multi-color 3D printing
Split green features into separate non-overlapping parts to fix slicing issues. Key changes: - Split green mask into icon (top) and text (bottom) using white text as separator - Create 4 separate parts: base, green_icon, white_text, green_text - Parts positioned at same Z height but don't overlap in XY space - Assembly structure matches nameplate.3mf (components with identity transforms) - Fixed polygon detection: reduced min_size, added preserve_topology - Added mesh repair logic to ensure watertight meshes - Updated material maps for new part names Files: - svg_logo_to_stl.py: Main conversion script with green split logic - Color logo - no background.svg: Source SVG file - command.txt: Usage documentation with working parameters - requirements.txt: Python dependencies - .gitignore: Exclude output files and debug images Minimum scale: 254mm width for 0.4mm nozzle (smaller features too thin to print) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
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 |
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
|
||||||
20
requirements.txt
Normal file
20
requirements.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user