Files
homelab-docs/Printing/Nameplates/add_filament_change_to_3mf.py

175 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""
Add M600 filament change command to all objects in an Orca Slicer 3MF file.
This script modifies a 3MF file (which is a ZIP archive containing XML) to add
a height range modifier at 1.5mm (end of blue base layer) with M600 command
for all zipper pull objects.
Usage:
python add_filament_change_to_3mf.py input.3mf [output.3mf]
If output.3mf is not specified, creates input_modified.3mf
"""
import sys
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
import shutil
import tempfile
import os
def add_height_range_to_3mf(input_file, output_file=None, height_mm=1.5):
"""
Add height range modifier with M600 at specified height to all objects in 3MF.
Args:
input_file: Path to input 3MF file
output_file: Path to output 3MF file (optional)
height_mm: Height in mm where filament change occurs (default: 1.5)
"""
input_path = Path(input_file)
if not input_path.exists():
print(f"Error: Input file '{input_file}' not found!")
return False
# Determine output filename
if output_file is None:
output_path = input_path.parent / f"{input_path.stem}_modified.3mf"
else:
output_path = Path(output_file)
print(f"Processing: {input_path}")
print(f"Output will be: {output_path}")
print(f"Filament change height: {height_mm}mm")
print("-" * 50)
# Create a temporary directory to work in
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Extract the 3MF (it's a ZIP file)
print("Extracting 3MF file...")
with zipfile.ZipFile(input_path, 'r') as zip_ref:
zip_ref.extractall(temp_path)
# Find and modify the 3D model file (usually 3D/3dmodel.model)
model_file = temp_path / "3D" / "3dmodel.model"
if not model_file.exists():
print("Error: Could not find 3dmodel.model in 3MF file!")
return False
print(f"Modifying {model_file}...")
# Parse the XML
tree = ET.parse(model_file)
root = tree.getroot()
# Define namespaces (3MF uses namespaces)
namespaces = {
'': 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02',
'p': 'http://schemas.microsoft.com/3dmanufacturing/production/2015/06',
's': 'http://schemas.orca-3d.com/3mf/2023/06'
}
# Register namespaces for output
for prefix, uri in namespaces.items():
if prefix:
ET.register_namespace(prefix, uri)
else:
ET.register_namespace('', uri)
# Find all objects (build items)
# In Orca Slicer 3MF, objects are in <build><item> tags
build_elem = root.find('.//build', namespaces)
if build_elem is None:
print("Warning: No <build> element found. Looking for items directly...")
items = root.findall('.//item', namespaces)
else:
items = build_elem.findall('.//item', namespaces)
if not items:
print("Error: No items found in 3MF file!")
return False
print(f"Found {len(items)} object(s) in the file")
# Count modified items
modified_count = 0
# Add height range modifier to each item
for idx, item in enumerate(items, 1):
object_id = item.get('objectid', f'unknown_{idx}')
print(f" Processing object {idx}/{len(items)} (ID: {object_id})...")
# Check if this item already has metadata for height range
# In Orca Slicer, this is typically stored as metadata
# We need to add the height range modifier metadata
# Note: The exact XML structure for height range modifiers in Orca Slicer
# may vary. This is a generic approach that adds metadata.
# You may need to adjust based on actual Orca Slicer 3MF structure.
# Create or find metadata container
metadata_group = item.find('metadatagroup', namespaces)
if metadata_group is None:
metadata_group = ET.SubElement(item, 'metadatagroup')
# Add height range modifier metadata
# Format: height range from 0 to height_mm = blue (original)
# height range from height_mm to top = white (with M600)
height_modifier = ET.SubElement(metadata_group, 'metadata')
height_modifier.set('name', 'height_range_modifier')
height_modifier.text = f'{{"ranges":[{{"min":0,"max":{height_mm},"color":"RoyalBlue"}},{{"min":{height_mm},"max":999,"color":"white","gcode":"M600"}}]}}'
modified_count += 1
print(f"\nModified {modified_count} object(s)")
# Save the modified XML back to the file
print("Saving modified model file...")
tree.write(model_file, encoding='utf-8', xml_declaration=True)
# Re-create the 3MF (ZIP) file with all contents
print("Creating new 3MF file...")
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zip_out:
# Walk through the temp directory and add all files
for root_dir, dirs, files in os.walk(temp_path):
for file in files:
file_path = Path(root_dir) / file
arcname = file_path.relative_to(temp_path)
zip_out.write(file_path, arcname)
print(f"\nSuccess! Modified 3MF saved to: {output_path}")
print(f"\nNext steps:")
print(f"1. Open {output_path.name} in Orca Slicer")
print(f"2. Verify the height range modifiers are present")
print(f"3. Slice and check for M600 commands in the G-code")
return True
def main():
if len(sys.argv) < 2:
print("Usage: python add_filament_change_to_3mf.py input.3mf [output.3mf]")
print("\nAdds M600 filament change at 1.5mm to all objects in a 3MF file.")
print("If output.3mf is not specified, creates input_modified.3mf")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
success = add_height_range_to_3mf(input_file, output_file)
if not success:
sys.exit(1)
if __name__ == "__main__":
main()