#!/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 tags build_elem = root.find('.//build', namespaces) if build_elem is None: print("Warning: No 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()