Skip to content

Instantly share code, notes, and snippets.

@haliphax
Last active June 8, 2026 18:24
Show Gist options
  • Select an option

  • Save haliphax/e63eb8efba86406862fc71d06a11cda1 to your computer and use it in GitHub Desktop.

Select an option

Save haliphax/e63eb8efba86406862fc71d06a11cda1 to your computer and use it in GitHub Desktop.
STL generator for breakaway stacks
#!/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