Last active
June 8, 2026 18:24
-
-
Save haliphax/e63eb8efba86406862fc71d06a11cda1 to your computer and use it in GitHub Desktop.
STL generator for breakaway stacks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # dependencies = ["numpy-stl"] | |
| # /// | |
| """ | |
| This script is for creating stacks of more-or-less flat meshes that have | |
| symmetrical top and bottom faces (e.g. Multiboard). The gap (default 0.2mm) | |
| should be set to your slicer's layer height. The resulting file should be | |
| printed with 3 walls, 15% infill, with ironing of all top surfaces enabled. | |
| Example JSON configuration file: | |
| ```json | |
| { | |
| "gap": 0.2, | |
| "output": "stacked.stl", | |
| "files": [ | |
| ["mesh1.stl", 2], | |
| ["mesh2.stl", 1], | |
| ["mesh3.stl", 5] | |
| ] | |
| } | |
| ``` | |
| """ | |
| # stdlib | |
| import argparse | |
| import json | |
| # 3rd party | |
| from stl import mesh | |
| from numpy import concatenate | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Create stacks of STL meshes, optionally loading a configuration from JSON." | |
| ) | |
| parser.add_argument( | |
| "config_file", | |
| nargs="?", | |
| help="Path to a JSON file defining the stack configuration." | |
| ) | |
| parser.add_argument( | |
| "-f", "--file", | |
| help="Input STL file path (ignored if config_file is provided)." | |
| ) | |
| parser.add_argument( | |
| "-c", "--count", | |
| type=int, | |
| default=4, | |
| help="Number of times to stack the mesh (ignored if config_file is provided)." | |
| ) | |
| parser.add_argument( | |
| "-g", "--gap", | |
| type=float, | |
| default=0.2, | |
| help="Gap between meshes in mm (ignored if config_file is provided)." | |
| ) | |
| parser.add_argument( | |
| "-o", "--output", | |
| default="stacked.stl", | |
| help="Output STL file path." | |
| ) | |
| args = parser.parse_args() | |
| meshes_to_stack = [] | |
| global_gap = args.gap | |
| output_file = args.output # Default to command line argument | |
| if args.config_file: | |
| # Load from JSON | |
| try: | |
| with open(args.config_file, 'r') as f: | |
| config = json.load(f) | |
| global_gap = config.get("gap", args.gap) | |
| meshes_data = config.get("files", []) | |
| output_file = config.get("output", args.output) # Override with JSON if present | |
| for item in meshes_data: | |
| if isinstance(item, list) and len(item) >= 2: | |
| file_path, count = item[0], item[1] | |
| for _ in range(count): | |
| try: | |
| m = mesh.Mesh.from_file(file_path) | |
| meshes_to_stack.append(m) | |
| except Exception as e: | |
| print(f"Error loading {file_path}: {e}") | |
| else: | |
| print(f"Skipping invalid mesh entry: {item}") | |
| except Exception as e: | |
| print(f"Error reading config file: {e}") | |
| return | |
| else: | |
| # Single mesh mode | |
| file_path = args.file or "mesh.stl" | |
| try: | |
| m = mesh.Mesh.from_file(file_path) | |
| for _ in range(args.count): | |
| meshes_to_stack.append(m) | |
| except Exception as e: | |
| print(f"Error loading {file_path}: {e}") | |
| return | |
| if not meshes_to_stack: | |
| print("No meshes to stack.") | |
| return | |
| # Clone meshes and apply offsets | |
| current_z = 0.0 | |
| processed_meshes = [] | |
| for m in meshes_to_stack: | |
| # Calculate height of this specific mesh | |
| h = m.z.max() - m.z.min() | |
| # Clone to avoid modifying original | |
| m_copy = mesh.Mesh(m.data.copy()) | |
| # Translate | |
| m_copy.translate([0, 0, current_z]) | |
| processed_meshes.append(m_copy) | |
| # Increment Z for the next one | |
| current_z += h + global_gap | |
| # output stacked STL | |
| combined = mesh.Mesh(concatenate([m.data for m in processed_meshes])) | |
| combined.save(output_file) | |
| print(f"Saved stacked mesh to {output_file}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment