Skip to content

Instantly share code, notes, and snippets.

@haliphax
Last active July 29, 2026 02:44
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.
"""
# stdlib
import argparse
import json
import math
# 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(
"-a",
"--alternate",
action="store_true",
default=False,
help="Alternate rotation of every other mesh.",
)
parser.add_argument(
"-o", "--output", default="stacked.stl", help="Output STL file path."
)
args = parser.parse_args()
meshes_to_stack = []
global_gap = args.gap
alternate = args.alternate
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", [])
alternate = config.get("alternate", args.alternate)
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 idx, m in enumerate(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())
use_z = current_z
# Flip if alternating
if alternate and idx % 2 == 1:
m_copy.rotate([0, 1, 0], math.radians(180))
use_z += h
# Translate
m_copy.translate([0, 0, use_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