Created
May 11, 2026 18:14
-
-
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.
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
| """ | |
| 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