Skip to content

Instantly share code, notes, and snippets.

@Friedjof
Created May 11, 2026 18:14
Show Gist options
  • Select an option

  • Save Friedjof/70e06ea8b18f571395a16dac9c3094ec to your computer and use it in GitHub Desktop.

Select an option

Save Friedjof/70e06ea8b18f571395a16dac9c3094ec to your computer and use it in GitHub Desktop.
STL-Objekt fürs Lasercutten in ein Raster duplizieren (grid oder brick/hexagonal). Python + numpy-stl, uv-fertig.
"""
stl-grid — STL-Objekt fürs Lasercutten in ein Raster duplizieren
================================================================
Nimmt eine STL-Datei (z. B. eine flache 2D-Form mit Dicke) und legt sie
in einem Raster ab — entweder als simples Grid oder als verschachteltes
Brick-/Bienenwabenmuster, das bei ovalen/ellipsenförmigen Teilen spürbar
weniger Verschnitt verursacht. Das Ergebnis bleibt 3D (STL).
Quickstart (mit `uv`)
---------------------
uv init stl-grid && cd stl-grid
uv add numpy-stl numpy
# main.py hier rein, dann:
uv run main.py elipsen_2026-04-08_v1.stl
Das schreibt `elipsen_2026-04-08_v1_brick_20x10.stl` neben das Original
(20 Spalten × 10 Reihen = 200 Stück, 2 mm Abstand, Brick-Layout).
Optionen
--------
-c / --cols Spalten (X-Richtung) default 20
-r / --rows Reihen (Y-Richtung) default 10
-g / --gap Mindestabstand in mm default 2.0
-l / --layout brick | grid default brick
-o / --output eigener Zieldateiname
Beispiele
---------
# 200 Stück, dichteste Ellipsen-Packung (Default):
uv run main.py elipsen_2026-04-08_v1.stl
# 5 × 4 Stück mit 3 mm Abstand, simples Raster:
uv run main.py elipsen_2026-04-08_v1.stl -c 5 -r 4 -g 3 -l grid
# Eigene Zieldatei:
uv run main.py elipsen_2026-04-08_v1.stl -o platte_lasercut.stl
Brick-Layout
------------
Jede zweite Reihe wird um eine halbe lange Achse versetzt; der Reihen-
abstand schrumpft auf `b·√3` statt `2b` (b = halbe kurze Achse). Der
Mindestabstand der Konturen bleibt überall `--gap`. Für Ellipsen ist
das die theoretisch dichteste achsparallele Packung.
"""
import argparse
import math
from pathlib import Path
import numpy as np
from stl import mesh
def duplicate_grid(
input_path: Path,
output_path: Path,
cols: int,
rows: int,
gap: float,
layout: str,
) -> None:
src = mesh.Mesh.from_file(str(input_path))
pts = src.vectors.reshape(-1, 3)
bb_min = pts.min(axis=0)
bb_max = pts.max(axis=0)
size_x, size_y = (bb_max - bb_min)[:2]
a = size_x / 2.0
b = size_y / 2.0
base = src.vectors.copy()
base[:, :, 0] -= bb_min[0]
base[:, :, 1] -= bb_min[1]
if layout == "brick":
# Hexagonale Packung achsparalleler Ellipsen: jede 2. Reihe um eine
# halbe lange Achse in X versetzt; der Reihenabstand sinkt auf b'·√3
# (mit b' = b + gap/2). Die Konturen halten dabei mindestens `gap` Abstand.
a_eff = a + gap / 2.0
b_eff = b + gap / 2.0
step_x = 2.0 * a_eff
step_y = b_eff * math.sqrt(3.0)
row_offset_x = a_eff
elif layout == "grid":
step_x = size_x + gap
step_y = size_y + gap
row_offset_x = 0.0
else:
raise ValueError(f"unbekanntes Layout: {layout}")
n_tri = base.shape[0]
total = cols * rows
out = np.empty((total * n_tri, 3, 3), dtype=base.dtype)
i = 0
for r in range(rows):
ox = row_offset_x if (r % 2) else 0.0
for c in range(cols):
chunk = base.copy()
chunk[:, :, 0] += c * step_x + ox
chunk[:, :, 1] += r * step_y
out[i * n_tri : (i + 1) * n_tri] = chunk
i += 1
dst = mesh.Mesh(np.zeros(out.shape[0], dtype=mesh.Mesh.dtype))
dst.vectors = out
dst.save(str(output_path))
plate_x = (cols - 1) * step_x + size_x + (row_offset_x if rows > 1 else 0.0)
plate_y = (rows - 1) * step_y + size_y
area = plate_x * plate_y / 100.0 # cm²
print(f"Einzelteil: {size_x:.2f} x {size_y:.2f} mm")
print(f"Layout: {layout}")
print(f"Raster: {cols} x {rows} = {total} Stück")
print(f"Abstand: {gap} mm")
print(f"Plattenmaß: {plate_x:.2f} x {plate_y:.2f} mm ({area:.0f} cm²)")
print(f"Geschrieben: {output_path}")
def main() -> None:
p = argparse.ArgumentParser(description="Dupliziert ein STL-Objekt in ein Raster (z. B. fürs Lasern).")
p.add_argument("input", type=Path, help="Pfad zur Quell-STL")
p.add_argument("-o", "--output", type=Path, help="Pfad zur Ziel-STL (default: <input>_<layout>_CxR.stl)")
p.add_argument("-c", "--cols", type=int, default=20, help="Spalten (X), default 20")
p.add_argument("-r", "--rows", type=int, default=10, help="Reihen (Y), default 10")
p.add_argument("-g", "--gap", type=float, default=2.0, help="Mindestabstand zwischen Konturen in mm, default 2.0")
p.add_argument(
"-l",
"--layout",
choices=("brick", "grid"),
default="brick",
help="brick = hexagonal versetzt (weniger Verschnitt für Ellipsen), grid = einfaches Raster",
)
args = p.parse_args()
out = args.output or args.input.with_name(
f"{args.input.stem}_{args.layout}_{args.cols}x{args.rows}.stl"
)
duplicate_grid(args.input, out, args.cols, args.rows, args.gap, args.layout)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment