Einleitung

OpenSCAD ermöglicht es, dreidimensionale Modelle mittels der sogenannten Konstruktiven Festkörpergeometrie (engl. Constructive Solid Geometry, CSG) zu erstellen. Die Idee ist hierbei, komplexe Geometrien durch die Kombination einer begrenzten Menge an einfachen Grundelementen wie etwa Kugeln, Zylindern oder Quadern zu erzeugen - also ein bisschen so, wie man früher als Kind mit Bausteinen gespielt hat.

Das Besondere an OpenSCAD ist, dass man die Geometrie über eine rein textuelle Beschreibung spezifiziert und nicht etwa mit der Maus in einem grafischen Editor arbeitet. Diese Herangehensweise prädestiniert OpenSCAD für eine ganze Reihe an Anwendungsfällen, die in Systemen mit einem interaktiven Benutzungsschema aufwendiger umzusetzen wären. Zum Beispiel:

Natürlich gibt es auch Anwendungsfälle, für die OpenSCAD nicht geeignet ist. Hierzu zählt zum Beispiel die Erzeugung künstlerischer bzw. fotorealistischer 3D-Grafiken und Animationen. Der ungewöhnliche Ansatz, Geometrie textuell zu beschreiben mag zunächst den Eindruck erwecken, dass es schwierig und aufwendig sei, auf diese Weise zu arbeiten. Glücklicherweise ist dem nicht so. Die nötige Lernkurve ist viel flacher als sie auf den ersten Blick erscheint. Hat man einmal die wenigen Grundprinzipien dieser Arbeitsweise verinnerlicht, kann man auch komplexe Geometrien ohne große Probleme erstellen. Das vorliegende Buch wird Ihnen dabei helfen, diese Grundprinzipien schnell und einfach anhand praxisnaher Beispielprojekte zu erlernen.

Die Beispielprojekte richten sich insbesondere an diejenigen, die dreidimensionale Objekte für ihren 3D-Drucker oder ihre CNC-Fräse konstruieren möchten. Gerade in diesem Anwendungsbereich glänzt OpenSCAD aufgrund der hohen Parametrisierbarkeit.

Verfügbarkeit von OpenSCAD

OpenSCAD ist eine frei verfügbare, quelloffene Software, die Sie kostenlos von der Webseite www.openscad.org herunterladen können. Im Downloadbereich der Webseite werden Versionen für Windows, MacOS und Linux angeboten. Dieses Buch bezieht sich auf die OpenSCAD-Version 2019.05.

Programmübersicht

Bevor wir uns im nächsten Kapitel mit der grundsätzlichen Funktionsweise von OpenSCAD beschäftigen, verschaffen wir uns an dieser Stelle erst einmal einen Überblick über die Bedienoberfläche des Programms und einige grundlegende Standardfunktionen und Einstellungen.

Der Startbildschirm

Abbildung 1.: Das Startfenster von OpenSCAD

Nach dem Start von OpenSCAD wird man von einem kleinen Startfenster begrüßt (Abbildung 1.). Die Schaltfläche New (dt. Neu) startet das Programm mit einer leeren, noch nicht abgespeicherten Datei. Mit der Schaltfläche Open (dt. Öffnen) gelangt man zu einem Dateiauswahldialog, über den man eine bereits existierende Datei auswählen und laden kann. OpenSCAD-Dateien enden auf die Dateiendung .scad. Es handelt sich hierbei um einfache Textdateien. Die Schaltfläche Help (dt. Hilfe) öffnet einen Internetbrowser mit der Online-Hilfe des Programms. Unterhalb dieser drei Schaltflächen gibt es zwei Listen. Die Liste auf der linken Seite zeigt die zuletzt mit OpenSCAD bearbeiteten Dateien an. Wählt man eine dieser Dateien aus, wird die Schaltfläche Open Recent (recent » kürzlich) unterhalb der Liste aktiv und mit einem Klick auf diese Schaltfläche wird dann die entsprechende Datei geöffnet. Dies ist leider wenig intuitiv, da man schnell dabei ist, nach der Auswahl einfach auf die Schaltfläche Open oberhalb der Liste zu klicken (was nicht zum gewünschten Ergebnis führt). Die Liste auf der rechten Seite offeriert eine Reihe von thematisch sortierten Beispielen. Vor jedem Thema wird ein kleines Dreieck angezeigt. Ein Klick auf das Dreieck “klappt” das Thema auf und man kann eine der Beispieldateien auswählen. Dies aktiviert die Schaltfläche Open Example (example » Beispiel) unterhalb der Liste, mit der man das ausgewählte Beispiel öffnen kann. Alternativ kann man in beiden Listen eine ausgewählte Datei auch dadurch öffnen, dass man sie doppelklickt. Möchte man nicht bei jedem Start von OpenSCAD von diesem Startfenster begrüßt werden, kann man dies durch Ankreuzen des Auswahlfeldes Don’t show again (dt. Nicht erneut anzeigen) unten links erreichen.

Für’s erste starten wir OpenSCAD mit einer neuen, leeren Datei (Schaltfläche New) und schauen uns die Bedienoberfläche des Programms einmal an.

Die Bedienoberfläche

Abbildung 1.: Die Bedienoberfläche von OpenSCAD

Die Bedienoberfläche (Abbildung 1.) ist in vier Bereiche unterteilt. Der erste Bereich befindet sich auf der linken Seite. Es handelt sich hierbei um einen einfachen Texteditor, der die zentrale Eingabeschnittstelle für OpenSCAD darstellt. Jegliche Modellierung von Geometrien findet hier statt.

Der zweite Bereich befindet sich in der Mitte des Programmfensters. Es handelt sich hierbei um ein reines Ausgabefenster, dass eine dreidimensionale Darstellung der erzeugten Geometrien bereit hält. Mit Hilfe der Maus kann die Ansicht verändert werden. Bei gedrückter linker Maustaste kann die Darstellung rotiert werden. Hält man die rechte Maustaste gedrückt, wird die Ansicht verschoben. Mit Hilfe des Mausrads kann in die Ansicht hinein- und herausgezoomt werden. Weitere Anpassungen der Darstellung stehen über die Symbolleiste unterhalb des Ausgabefensters sowie über den Menüpunkt View (dt. Ansicht) im Programmmenü (oben links) zur Verfügung. Die genauen Details dieser Funktionen werden wir im Laufe der Beispielprojekte erkunden und nutzen.

Der dritte Bereich ist die sogenannte Konsolenausgabe (engl. Console) des Programms. Sie befindet sich unterhalb des 3D-Ausgabefensters. Hier erscheinen textuelle Rückmeldungen des Programms nachdem bzw. während bestimmte Aktionen ausgeführt werden. Die Rückmeldungen helfen zum Beispiel dabei, Fehler in der Geometriemodellierung zu finden oder zu erkennen, ob ein Rechenlauf abgeschlossen ist.

Der vierte Bereich findet sich auf der rechten Seite des Programmfensters und ist mit dem englischen Begriff Customizer (dt. Konfigurator) überschrieben. Wie der Name andeutet, dient dieser Programmbereich zur Konfiguration bzw. Anpassung des jeweils geladenen Modells. OpenSCAD interpretiert hierfür bestimmte globale Variablen des Modells als Parameter und erzeugt für diese Parameter automatisch eine entsprechende Parameterliste in der man die Werte der Parameter einstellen kann. Anschließend kann die Parameterliste als sogenanntes Preset (dt. Voreinstellung) abgespeichert und zu einem späteren Zeitpunkt wieder geladen werden. Erfahrungsgemäß nutzt man dieses Fenster nur bei ganz bestimmten Anwendungsfällen. Es wird daher nur gelegentlich gebraucht.

Abbildung 1.: Anpassung der Bereiche

Die Aufteilung der Bedienoberfläche in diese vier Bereiche kann je nach Bedarf eingestellt werden. Führt man die Maus auf die Grenze zwischen zwei Bereichen (Abbildung 1.), so kann man diese Grenze bei gedrückter linker Maustaste verschieben. Möchte man einen Bereich ausblenden, so kann man dies durch Anklicken des eingerahmten kleinen “x” in der oberen, rechten Ecke des Bereichs bewirken. Alternativ kann man unter dem Menüpunkt View im Programmmenü die einzelnen Bereiche mit den Unterpunkten Hide editor, Hide console und Hide Customizer (hide » verstecken) aus- und einblenden. Möchte man die relative Position eines Bereiches innerhalb der Bedienoberfläche verändern (z.B. den Texteditor auf die rechte Seite verschieben), klickt man mit der Maus den Bereich im oberen Teil an (rote Schattierungen in Abbildung 1.) und verschiebt ihn mit gedrückter linker Maustaste.

Das Programmmenü

Das Programmmenü von OpenSCAD entspricht weitestgehend dem anderer Programme. Daher wollen wir an dieser Stelle nur eine kurze Übersicht geben und gehen auf spezifische Funktionen erst im weiteren Verlauf ein.

Unter dem Menüpunkt File (dt. Datei) finden sich wie erwartet Funktionen zum Erstellen (New), Öffnen (Open), Speichern (Save, Save As) und Schließen (Close) von Modelldateien. Darüber hinaus hat man Zugriff auf eine Liste zuletzt bearbeiteter Dateien (Recent Files), die Exportfunktionen (Export) sowie das Bibliotheksverzeichnis (Show Library Folder) von OpenSCAD.

Der Menüpunkt Edit (dt. Bearbeiten) stellt eine ganze Reihe an Funktionen bereit, die sich auf den Texteditor-Bereich beziehen. Hier finden sich u.a. typische Funktionen wie Kopieren (Copy), Einfügen (Paste) oder Suchen und Ersetzen (Find and Replace). Jenseits dieser Textfunktionen findet man hier auch den Zugang zu den globalen OpenSCAD-Einstellungen (Preferences).

Der nächste Menüpunkt, Design, enthält zwei ganz zentrale Funktionen von OpenSCAD, die wir sehr häufig nutzen werden: Preview (dt. Vorschau) und Render (dt Berechnen). Die Vorschau-Funktion erzeugt möglichst schnell eine dreidimensionale Voransicht der aktuellen Geometrie und stellt diese im Ausgabefenster dar. In den allermeisten Fällen ist das Resultat dieser schnellen Vorschau ohne sichtbare Fehler und unterstützt das Arbeiten ungemein. Die so erzeugte Geometrie kann jedoch nicht exportiert werden. Hierfür muss die zweite Funktion, das Berechnen, aufgerufen werden. Die Berechnung einer komplexen Geometrie kann hierbei eine ganze Weile in Anspruch nehmen. Daher führt man diese in der Regel nur dann aus, wenn man die Geometrie exportieren möchte, oder wenn die schnelle Vorschau erkennbare Fehler aufweist. Neben diesen beiden zentralen Funktionen findet sich auch die Auswahlbox Automatic Reload and Preview (dt. Automatisches Neu-Laden und Vorschau) unter dem Menüpunkt Design. Wenn diese Funktion aktiviert ist, beobachtet OpenSCAD die aktuelle Datei. Wenn sich die Datei geändert hat, dann wird die geänderte Datei automatisch in den Editor geladen und eine schnelle Vorschau angestoßen. Im regulären Arbeitsablauf stellt sich dies als äußerst praktisch heraus. Nachdem man eine Änderung an der aktuellen Geometrie im Texteditor vorgenommen hat, ist es üblich, diese Änderung mit der Tastenkombination STRG + S abzuspeichern. Sobald man dies tut, wird die aktuelle Vorschau automatisch aktualisiert und man kann das Ergebnis der Änderung sogleich begutachten. Ohne diese automatische Aktualisierung müsste man nach jeder Änderung die Berechnung der Vorschau manuell auslösen. Die Funktion Automatic Reload and Preview erlaubt es übrigens auch, einfach einen externen Texteditor anstelle des internen zu verwenden.

Der Menüpunkt View (dt. Ansicht) wurde bereits im vorhergehenden Abschnitt angesprochen. Hier finden sich zahlreiche Funktionen, mit denen man Einfluss auf das Ausgabefenster nehmen kann.

Der letzte Menüpunkt, Help (dt. Hilfe), verweist auf verschiedene Online-Quellen, die bei der Benutzung von OpenSCAD helfen können. Besonders hervorzuheben ist hierbei der Punkt Cheat Sheet (dt. Fuschzettel), hinter dem sich eine sehr praktische Zusammenfassung aller OpenSCAD-Befehle und -Funktionen verbirgt. Ein weiterer sehr nützlicher Punkt ist Font List (dt. Schriftartenliste), über den man zu einer Übersicht der in OpenSCAD verfügbaren Schriftarten kommt.

Da man OpenSCAD zu einem großen Teil eher mit der Tastatur als mit der Maus bedient, lohnt es sich, für häufig genutzte Funktionen die zugehörigen Tastenkombinationen zu erlernen. Die Tastenkombinationen werden - sofern vorhanden - in den Menüs auf der rechten Seite eines jeden Funktionseintrags dargestellt. Ein Teil der Funktionen wird auch in Form von Icons (dt. Symbolbildchen) auf der Programmoberfläche angeboten. Lässt man den Mauszeiger für einen kurzen Moment über einem solchen Icon stehen (ohne zu klicken), dann wird ein kleiner Erklärungstext sowie die zugehörige Tastenkombination der dargestellten Funktion angezeigt.

OpenSCAD Grundlagen

Wie eingangs erwähnt werden in OpenSCAD geometrische Modelle durch eine textuelle Spezifikation beschrieben. Auf den ersten Blick ähnelt diese textuelle Beschreibung einem Programm, welches in einer Programmiersprache wie Javascript oder C geschrieben wurde. Gerade wenn man ein wenig Programmiererfahrung hat, kann diese augenscheinliche Ähnlichkeit zu einigen Missverständnissen und fehlgeleiteten Intuitionen führen. Auch wenn die textuelle Beschreibung der Geometrien in OpenSCAD an klassischen Programmcode erinnert, so handelt es sich nicht um ein Programm. Es handelt sich vielmehr um die Spezifikation einer geometrischen Struktur. Im Laufe der folgenden Abschnitte wird dieser Unterschied Schritt für Schritt klarer werden. Der Fokus dieses Kapitels liegt auf der Vermittlung der wesentlichen Konzepte und Funktionsweisen von OpenSCAD anhand ausgewählter Beispiele. Eine vollständige Beschreibung aller Funktionen findet sich am Ende des Buches sowie in der Online-Dokumentation von OpenSCAD.

Grundbausteine

Lassen Sie uns nun endlich loslegen und ein wenig ausprobieren, wie man denn nun eine Geometrie in OpenSCAD beschreiben kann. Wenn Sie es nicht bereits getan haben, wäre jetzt ein guter Zeitpunkt, OpenSCAD mit einer leeren bzw. neuen Datei zu starten. Das OpenSCAD-Programmfenster sollte nun den internen Editor, das Ausgabefenster und die Konsolenansicht zeigen. Den Customizer können sie vorerst ausblenden.

Das Fundament einer jeden Geometrie in OpenSCAD sind die sogenannten Primitive. Dies sind zwei- oder dreidimensionale Grundformen, die wir als Bausteine für unser Modell benutzen können. Lassen Sie uns eine einfache Kugel (engl. sphere) erstellen. Geben Sie hierfür im Editorfenster folgendes ein:

sphere(10);

Wenn Sie nun eine Vorschau berechnen lassen (Taste F5), dann sollte im Ausgabefenster eine Kugel mit einem Radius von 10 Millimetern angezeigt werden.

Abbildung 2.: Erfolgreiche Berechnung der Vorschau

Schauen Sie auch einmal auf das Konsolenfenster. Dort sollten Sie eine ganze Reihe an Ausgaben finden, die im Rahmen der Vorschau produziert wurden. In der vorletzten Zeile dieser Ausgabe sollte ein Compile and preview finished. (dt. Kompilierung und Vorschau beendet) stehen (Abbildung 2.).

Abbildung 2.: Fehler bei der Vorschauberechnung

Wenn anstelle dessen ein rot markiertes ERROR in der Ausgabe steht, lief irgendetwas schief (Abbildung 2.). Vielleicht haben Sie sich vertippt? Oder vielleicht das Semikolon am Ende der Zeile vergessen? Haben Sie sphere vielleicht mit einem großen S anstatt eines kleinen geschrieben? Versuchen Sie den Fehler zu finden und probieren Sie die Vorschauberechnung (F5) erneut.

Jede Grundform in OpenSCAD hat einen eindeutigen Namen, dem immer eine Liste von Parametern in runden Klammern und ein abschließendes Semikolon folgt. Im obigen Beispiel ist der Radius der Kugel der erste Parameter. Jeder Parameter hat auch eine Bezeichnung und im Allgemeinen ist es besser, diese Bezeichnung mit anzugeben. Im Falle der Kugel hat der Radius-Parameter die Bezeichnung r und eine Verwendung dieser Parameterbezeichnung würde so aussehen:

sphere(r = 10);

Auch wenn es etwas mehr Tipparbeit ist, hat die Verwendung der Parameterbezeichnung gleich zwei Vorteile. Zum einen wird die Geometriebeschreibung lesbarer. Zum anderen muss man sich nicht an die exakte Reihenfolge der Parameter erinnern, falls eine Grundform mehrere Parameter hat. Dies ist auch bei der Kugel der Fall. Anstelle des Radius kann man die Kugel auch mit einem Durchmesser definieren:

sphere(d = 10);

Probieren Sie es aus (F5). Die Kugel sollte nun nur noch halb so groß im Ausgabefenster erscheinen. Was passiert wohl, wenn man sowohl einen Radius als auch einen Durchmesser angibt?

sphere(r = 10, d = 10);

Wenn man nun die Vorschau berechnen lässt (F5), dann verändert sich die Kugel nicht und in der Konsole wird eine gelb hinterlegte Warnung ausgegeben: Ignoring radius variable ‘r’ as diameter ’d' is defined too (dt. Ignoriere Radiusvariable ‘r’ da der Durchmesser ’d' ebenfalls definiert ist). Bei der Kugel hat also der Durchmesser Vorrang, wenn zusätzlich auch ein Radius angegeben wurde. Die Reihenfolge der Parameter spielt hier keine Rolle.

Falls Sie Programmiererfahrung haben, mag der Ausdruck sphere(r=10); Sie an einen Methodenaufruf erinnern. Im Kontext von OpenSCAD ist diese Intuition leider eher hinderlich als nützlich. Es ist besser, den Ausdruck nicht als Methodenaufruf zu interpretieren, sondern als eine Aussage über die Existenz einer konkreten Geometrie. In diesem Sinne besagt der Ausdruck sphere(r=10); in etwa: Es existiert eine Kugel mit Radius 10mm.

Bisher haben wir den Radius unserer Kugel mit einem direkten Wert angegeben. Wenn wir wissen, dass wir diesen Wert nie wieder anpassen müssen bzw. wollen, ist dies vollkommen in Ordnung. Möchten wir den Radius der Kugel in unserem Modell konfigurierbar machen, dann sollten wir dem Radius einen eigenen Namen geben. Dies können wir über eine Variable erreichen:

kugelradius = 10;

sphere(r = kugelradius);

In größeren Projekten ist es nützlich, alle Konfigurationsvariablen zu Beginn der Modelldatei zu sammeln. So hat man alle Einstellmöglichkeiten im Blick. Darüber hinaus sollte man es sich gleich angewöhnen, die Variablen (und an geeigneten Stellen auch die Geometrie) zu kommentieren. Einzeilige Kommentare kann man mit // einleiten. In diesem Fall wird alles bis zum Ende der Zeile als Kommentar gewertet. Braucht man mehr Platz, kann man einen Kommentarblock mit /* beginnen und mit */ beenden:

/*
    Dies ist ein OpenSCAD Testprojekt
    ---------------------------------
*/

kugelradius = 10;  // Ein ganz wichtiger Radius

// Die zentrale Kugel unseres Modells
sphere(r = kugelradius);

Auch an dieser Stelle kann Programmiererfahrung hinderlich sein. Unser Modell sieht ja jetzt noch mehr aus wie ein typisches sequentielles Programm! Dem ist aber nicht so. Eine Variable in OpenSCAD repräsentiert keinen Speicherbereich, der im Verlauf eines Programms unterschiedliche Werte annehmen kann, sondern ist erneut nur eine Existenzaussage: Es existiert der Wert 10 mit der Bezeichnung “kugelradius”. Was passiert, wenn wir eine Variable zweimal benutzen? Probieren wir es aus!

kugelradius = 10;  

sphere(r = kugelradius);

kugelradius = 20;

Wenn wir nun die Vorschau berechnen lassen (F5), dann sehen wir, dass die Kugel den Radius 20 annimmt. Gleichzeitig sehen wir in der Konsole eine Warnung: kugelradius was assigned on line 1 but was overwritten on line 5 (dt. kugelradius wurde in Zeile 1 zugewiesen, wurde jedoch in Zeile 5 überschrieben). Wir sehen, dass man einer Variable nur einen Wert geben kann. OpenSCAD verwendet hierfür immer den zuletzt gesetzten Wert (hier 20). Dies bedeutet auch, dass eine Variable nicht “vor” ihrer Verwendung definiert werden muss. Das “vor” steht in Anführungsstrichen, da es in einer OpenSCAD-Geometriebeschreibung kein zeitliches “vor” in diesem Sinne gibt.

Bei der Beschreibung von Werten durch Variablen ist man nicht darauf beschränkt, nur einzelne Werte zu verwenden. Nehmen wir einmal an, die Maße in unserem Modell bräuchten noch einen Korrekturwert. Dies könnten wir so umsetzen:

korrekturwert = 0.7;

kugelradius = 10 + korrekturwert;
seitenrand  =  5 + korrekturwert;
tiefe       = 25 + korrekturwert;

// ... noch viel mehr Konfigurationsvariablen mit korrekturwert

Soweit so gut. Nehmen wir an, wir stellen plötzlich fest, das wir auch noch einen Korrekturfaktor brauchen! Das könnten wir so umsetzen:

korrekturwert   = 0.7;
korrekturfaktor = 1.05;

kugelradius = (10 + korrekturwert) * korrekturfaktor;
seitenrand  = ( 5 + korrekturwert) * korrekturfaktor;
tiefe       = (25 + korrekturwert) * korrekturfaktor;

Sie sehen schon, dass es irgendwie aufwendig ist, jede Konfigurationsvariable für diese Veränderung anfassen zu müssen. Eine elegantere Lösung besteht darin, eine Funktion für die Korrektur zu verwenden:

korrekturwert   = 0.7;
korrekturfaktor = 1.05;

function korrektur(x) = (x + korrekturwert) * korrekturfaktor;

kugelradius = korrektur(10);
seitenrand  = korrektur( 5);
tiefe       = korrektur(25);

Falls wir nun die Korrektur in unserem Beispiel noch einmal anpassen müssen, dann brauchen wir fortan nur noch die Funktion korrektur(x) zu ändern. Funktionsdefinitionen werden mit dem Schlüsselwort function eingeleitet, gefolgt von einem eindeutigen Namen für die Funktion (hier korrektur) und einer Parameterliste in runden Klammern (hier (x)). Die Funktion selbst wird nach einem Gleichheitszeichen geschrieben und endet stets mit einem Semikolon. Neben unseren eigenen Funktionen bietet OpenSCAD eine ganze Reihe vordefinierter Funktionen an (z.B. sin(), cos(), round()), von denen wir einige im Laufe dieses Buches kennenlernen werden.

Bevor wir mit dem nächsten Abschnitt weitermachen, sollten wir kurz rekapitulieren, welche Grundbausteine wir kennengelernt haben:

Transformationen

Im vorherigen Abschnitt haben wir die Grundform Kugel in unserer Modellbeschreibung verwendet. Jetzt benutzen wir zur Abwechslung mal einen Würfel (engl. cube):

cube(size = 10, center = true);

Über den Parameter size definieren wir die Kantenlänge des Würfels. Der Parameter center bestimmt, wo der Würfel seinen Ursprung hat (Abbildung 2.).

Abbildung 2.: Ein Würfel mit und ohne center Parameter

Wie die Kugel zuvor wird der Würfel genau im Ursprung des Koordinatensystems angezeigt. Im Anzeigefenster wird das Koordinatensystem durch drei senkrecht zueinander stehende Linien dargestellt, die jeweils die X-, Y- und Z-Achse repräsentieren. Die positiven Bereiche der einzelnen Achsen werden über durchgezogene Linien dargestellt. Die negativen Bereiche durch gestrichelte Linien. Mit der Tastenkombination STRG + 2 können die Koordinatenachsen ein- und ausgeblendet werden. Achten Sie auch auf das kleine, beschriftete Koordinatenkreuz unten links im Anzeigefenster, welches als Orientierungshilfe dient.

Wenn wir nun unseren Würfel an eine andere Position bewegen wollen, müssen wir hierfür eine sogenannte Transformation verwenden. In diesem Fall eine Verschiebung bzw. Translation:

translate( [20,0,0] ) cube(10,true);

Eine Transformation (hier translate( [20,0,0] )) wirkt sich immer auf das nachfolgende Element aus. In diesem Fall auf unseren Würfel. Als einzigen Parameter bekommt die Transformation translate einen dreidimensionalen Vektor, der die gewünschte Verschiebungsdistanz in X-, Y- und Z-Richtung beschreibt. Vektoren werden in OpenSCAD mit eckigen Klammern geschrieben und die Zahlen werden durch Kommata getrennt.

An dieser Stelle muss auf ein ganz zentrales Funktionsprinzip von OpenSCAD hingewiesen werden. Der gesamte Ausdruck translate(...) cube(...); wird ein eigenständiges geometrisches Objekt, dass so wie jede andere Grundform (Kugel, Würfel, etc.) verwendet werden kann! Dies bedeutet insbesondere, dass wir diesem neuen Objekt weitere Transformationen voran stellen können, z.B. eine Drehung (engl. rotation):

rotate( [0,0,45] ) translate( [20,0,0] ) cube(10,true);

Die Transformation rotate dreht ein geometrisches Objekt um den Ursprung. Als Parameter bekommt die Transformation erneut einen dreidimensionalen Vektor. In diesem Fall enthält der Vektor die gewünschten Rotationswinkel um die X-, Y- und Z-Achse. Und auch hier wird wieder der gesamte Ausdruck rotate(...) translate(...) cube(...); ein neues, eigenständiges geometrisches Objekt. Da das Semikolon das Ende des Objektes markiert, kann man die Transformationen auch in die Zeilen über der Grundform (hier: cube) schreiben, um eine allzu lange Zeilenlänge zu vermeiden.

Abbildung 2.: Beispiel für die Auswirkung der Reihenfolge der Transformationen

Abbildung 2. illustriert, wie unser Endergebnis von der Reihenfolge der Transformationen abhängt. Da die Transformationen in OpenSCAD sich immer auf den Ursprung beziehen, macht es einen großen Unterschied, ob man ein Objekt erst verschiebt und dann rotiert, oder umgekehrt.

Mit geometrischen Grundformen und Transformationen sind wir nun auf einem Stand, der uns in etwa die Modellierungsmöglichkeiten gibt, die man zu Kinderzeiten mit Bausteinen hatte. Im nächsten Abschnitt werden wir diese Fähigkeiten deutlich erweitern.

Kombinationsmöglichkeiten

Nehmen wir einmal an, dass wir eine 5mm starke Platte mit einem Maß von 10cm x 5cm modellieren wollen. Dies könnten wir auf die folgende Art und Weise machen:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

cube( platte );

Wir definieren uns einen dreidimensionalen Vektor platte, der die Maße unserer Platte enthält und übergeben diesen Vektor als Parameter an die Grundform cube (Abbildung 2.).

Abbildung 2.: Grundform cube parametrisiert durch einen Vektor

Angenommen, wir möchten nun an den Ecken der Platte vier Bohrlöcher modellieren. Fangen wir damit an, dass wir erst einmal zwei Variablen definieren. Eine für den Durchmesser der Bohrlöcher und eine für den Abstand der Bohrlöcher vom Rand der Platte:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

cube( platte );

Als nächstes müssen wir OpenSCAD beschreiben, welche Form unsere Löcher haben sollen. Dafür eignet sich die Grundform Zylinder (engl. cylinder):

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

cube( platte );

cylinder( d = bohrloch_durchmesser, h = platte.z );

Der Parameter d der Grundform cylinder bestimmt den Durchmesser, der Parameter h bestimmt die Höhe. Da der Zylinder unser Bohrloch beschreiben soll, übergeben wir für d unsere Variable bohrloch_durchmesser und für h die Z-Koordinate des Plattenvektors, in der die Höhe bzw. Stärke der Platte definiert ist. Anstelle von platte.z hätte man hier auch platte[2] schreiben können.

Abbildung 2.: Grundform cylinder im Ursprung des Koordinatensystems.

Noch befindet sich unser Bohrlochzylinder am falschen Platz (Abbildung 2.) und ist überdies nicht gut von der modellierten Platte zu unterscheiden. Lassen Sie uns den Zylinder verschieben und ihm eine andere Farbe geben:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

cube( platte );

translate
([
	bohrloch_abstand + bohrloch_durchmesser/2,
	bohrloch_abstand + bohrloch_durchmesser/2,
	0
])
color( "red" )
cylinder( d = bohrloch_durchmesser, h = platte.z );

Wir wenden zunächst die Transformation color (dt. einfärben) an und färben damit den Zylinder rot (engl. red). Den roten Zylinder verschieben wir dann mittels der Transformation translate in die untere linke Ecke der Platte. Um unsere Modellbeschreibung etwas übersichtlicher zu halten, haben wir den Parameter der Transformation über mehrere Zeilen verteilt.

Boolesche Operationen

Bevor wir uns um die restlichen drei Bohrlochzylinder kümmern, machen wir erstmal unser erstes Loch in die Platte. Dies geht mit einer sogenannten Booleschen Operation (engl. boolean operation), die zwei oder mehr Geometrien miteinander kombinieren kann. In OpenSCAD stehen drei Arten von Boolescher Operation zur Verfügung: Differenz (engl. difference), Vereinigung (engl. union) und Schnitt (engl. intersection). Für unser Bohrloch benötigen wir die Differenzoperation, da wir den Zylinder von der Platte “abziehen” wollen:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

difference() {

	cube( platte );

	translate
	([
		bohrloch_abstand + bohrloch_durchmesser/2,
		bohrloch_abstand + bohrloch_durchmesser/2,
		0
	])
	color( "red" )
	cylinder( d = bohrloch_durchmesser, h = platte.z );

}

Genau wie eine Transformation wirkt eine Boolesche Operation (hier: difference()) auf das nachfolgende Element. Da Boolsche Operationen mehrere Geometrien miteinander kombinieren, würde es wenig Sinn ergeben, wenn das nachfolgende Element nur aus einer einzelnen Geometrie bestünde - auch wenn dies prinzipiell in OpenSCAD erlaubt ist. Daher wird als nachfolgendes Element eine Geometriemenge definiert, indem man eine Reihe von geometrischen Objekten durch ein Paar geschweifter Klammern { ... } zusammenfasst. Im Falle der Differenzoperation werden vom ersten Objekt der Menge alle folgenden Objekte abgezogen.

Abbildung 2.: Defekte Oberfläche nach Differenzoperation

Abbildung 2. zeigt, dass unser erstes Bohrloch leider nicht ganz in Ordnung ist. Die Oberfläche an der Stelle des Lochs zeigt seltsame Defekte. Insbesondere, wenn man die Ansicht im Anzeigefenster mit der Maus verändert. Diese Defekte sind auf Rundungsfehler bei der Berechnung zurückzuführen, da die Ober- und Unterkante unseres Bohrlochzylinders bündig mit der Ober- und Unterkante der Platte abschließt. Um das Problem zu beheben, müssen wir die Höhe des Zylinders ein kleines bisschen vergrößern und ihn gleichzeitig etwas nach unten bewegen, sodass der Zylinder sowohl oben als auch unten übersteht:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

difference() {

	cube( platte );

	translate
	([
		bohrloch_abstand + bohrloch_durchmesser/2,
		bohrloch_abstand + bohrloch_durchmesser/2,
		-1
	])
	color( "red" )
	cylinder( d = bohrloch_durchmesser, h = platte.z + 2);

}

Nun sieht das entstandene Loch sauber aus (Abbildung 2.). Um die Position des Zylinders zu überprüfen, kann man dem Bohrzylinder-Objekt temporär ein # in der Beschreibung voranstellen. Hierdurch wird der Zylinder halbtransparent in der Vorschau eingeblendet.

Abbildung 2.: Saubere Differenzoperation

Nun können wir uns endlich um die anderen drei Bohrlöcher kümmern. Hierzu kopieren wir einfach unser bestehendes Bohrloch drei mal und passen jeweils die Position an:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

difference() {

	cube( platte );

	randabstand = bohrloch_abstand + bohrloch_durchmesser/2;

	// Bohrloch unten links
	translate
	([
		randabstand,
		randabstand,
		-1
	])
	color( "red" )
	cylinder( d = bohrloch_durchmesser, h = platte.z + 2);

	// Bohrloch unten rechts
	translate
	([
		platte.x - randabstand,
		randabstand,
		-1
	])
	color( "red" )
	cylinder( d = bohrloch_durchmesser, h = platte.z + 2);

	// Bohrloch oben links
	translate
	([
		randabstand,
		platte.y - randabstand,
		-1
	])
	color( "red" )
	cylinder( d = bohrloch_durchmesser, h = platte.z + 2);

	// Bohrloch oben rechts
	translate
	([
		platte.x - randabstand,
		platte.y - randabstand,
		-1
	])
	color( "red" )
	cylinder( d = bohrloch_durchmesser, h = platte.z + 2);
}

Die Geometriemenge, auf die sich nun die Differenzoperation bezieht enthält nun fünf Objekte. Unsere Platte gefolgt von vier Bohrzylindern. Dies führt zum gewünschten Resultat (Abbildung 2.). Jedoch ist nun unsere Geometriebeschreibung sehr umfangreich und damit etwas unübersichtlich geworden.

Abbildung 2.: Eine Platte mit vier Bohrungen und viel kopierter Spezifikation

Schleifen

Immer wenn man sich dabei erwischt, einen Spezifikationsblock mehrfach zu kopieren, um ihn dann jeweils nur minimal zu verändern, ist das ein starker Hinweis darauf, dass es vermutlich eine bessere Möglichkeit gibt, die aktuelle Geometrie zu beschreiben. Dies ist auch in diesem Fall so. Anstatt das Bohrloch viermal zu kopieren, können wir die vier Instanzen des Bohrlochs auch durch eine Schleife beschreiben:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

difference() {

	cube( platte );

	randabstand   = bohrloch_abstand + bohrloch_durchmesser/2;
	x_werte       = [randabstand, platte.x - randabstand];
	y_werte       = [randabstand, platte.y - randabstand];

	// Bohrlöcher
	for (x = x_werte, y = y_werte)
    translate( [x, y, -1] )
    color( "red" )
    cylinder( d = bohrloch_durchmesser, h = platte.z + 2);

}

Schleifen werden in OpenSCAD durch das Schlüsselwort for (dt. für) gefolgt von einer oder mehreren Schleifenvariablen in runden Klammern beschrieben. Den Schleifenvariablen kann entweder ein Feld (engl. array) oder eine Spanne (engl. range) zugewiesen werden. Felder haben wir in Form von Vektoren bereits kennengelernt. Sie unterscheiden sich in ihrer Definition nicht. Eine Spanne sieht in OpenSCAD einem Feld sehr ähnlich: [Startwert : Endwert] bzw. [Startwert : Schrittweite : Endwert]. Man kann eine Spanne als implizites Feld verstehen, bei dem man nur Start- und Endwert angibt und die dazwischen liegenden Werte automatisch berechnet werden. Wird keine Schrittweite angegeben, wird eine Schrittweite von 1 angenommen.

Genau wie Transformationen und Boolesche Operationen beziehen sich For-Schleifen auf das nachfolgende Element. Dieses wird als Vorlage genutzt, um für jede mögliche Kombination der Schleifenvariablen ein neues geometrisches Objekt zu erzeugen. Werden die Schleifenvariablen in der Vorlage benutzt, werden die entsprechenden Werte aus dem zugehörigen Feld bzw. der zugehörigen Spanne jeweils eingesetzt. Das “Ergebnis” einer For-Schleife ist nicht eine Geometriemenge, sondern eine einzelne, vereinigte Geometrie.

Unser Beispiel verwendet die Felder x_werte und y_werte, die den Schleifenvariablen x und y zugewiesen werden. Hier ist eine Version, die zwei Spannen verwendet:

// Maße in mm [Breite, Tiefe, Höhe]
platte = [100,50,5];

bohrloch_abstand     = 4;
bohrloch_durchmesser = 6;

difference() {

	cube( platte );

	randabstand   = bohrloch_abstand + bohrloch_durchmesser/2;
	x_lochdistanz = platte.x - 2*randabstand;
	y_lochdistanz = platte.y - 2*randabstand;
	x_werte       = [randabstand : x_lochdistanz : platte.x - randabstand];
	y_werte       = [randabstand : y_lochdistanz : platte.y - randabstand];

	// Bohrlöcher
	for (x = x_werte, y = y_werte)
    translate( [x, y, -1] )
    color( "red" )
    cylinder( d = bohrloch_durchmesser, h = platte.z + 2);

}

Die Verwendung zweier Spannen ist etwas aufwendiger als die vorherige Version, die zwei Felder nutzte. Dies liegt daran, dass wir erst die passenden Schrittweiten x_lochdistanz und y_lochdistanz berechnen müssen. Im nächsten Abschnitt werden wir diesen Mehraufwand gewinnbringend nutzen. Zuvor lohnt es sich, einmal den Customizer anzuschauen (Abbildung 2.).

Abbildung 2.: Einfache Anpassung unserer Geometrie mit dem Customizer

Unsere Variablen platte, bohrloch_abstand und bohrloch_durchmesser wurden automatisch erkannt und als Bedienelemente in den Customizer eingefügt. Falls der Customizer nichts anzeigt, dann hilft es, die Vorschau erneut berechnen zu lassen und anschließend ggf. auf das kleine Dreieck vor Parameters zu klicken. Man kann nun unterschiedliche Konfigurationen als sogenannte Presets speichern und laden. Diese Funktionalität funktioniert jedoch nur, wenn man die Geometriebeschreibung bereits als .scad-Datei gespeichert hat. Die Presets selbst werden in einer zweiten Datei gespeichert, die den gleichen Namen hat wie die .scad-Datei, jedoch auf .json endet.

Module

Das Geometriemodell unserer Lochplatte ist inzwischen einigermaßen ausgereift. Was uns jetzt noch fehlt ist eine Möglichkeit, unsere Lochplatte komfortabel an mehreren Stellen einzusetzen ohne unsere Geometriebeschreibung kopieren zu müssen. Für diesen Zweck gibt es sogenannte Module in OpenSCAD:

module lochplatte( groesse, loch_dm, loch_abst) {

	difference() {

		cube( groesse );

		randabstand   = loch_abst + loch_dm/2;
		x_lochdistanz = groesse.x - 2*randabstand;
		y_lochdistanz = groesse.y - 2*randabstand;
		x_werte       = [randabstand : x_lochdistanz : groesse.x - randabstand];
		y_werte       = [randabstand : y_lochdistanz : groesse.y - randabstand];

		// Bohrlöcher
		for (x = x_werte, y = y_werte)
	    translate( [x, y, -1] )
	    color( "red" )
	    cylinder( d = loch_dm, h = groesse.z + 2);

	}

}

lochplatte( [100,50,5], 6, 4 );

translate( [0,60,0] )
lochplatte( groesse = [50,50,5], loch_dm = 3, loch_abst = 2 );

translate( [60,60,0] )
lochplatte( [50,50,5], loch_dm = 5, loch_abst = 5 );

Module werden in OpenSCAD durch das Schlüsselwort module eingeleitet. Es folgt der Name des Moduls (hier: lochplatte) und seine Parameterliste in runden Klammern. Wir müssen an dieser Stelle nicht angeben, welcher Art die Parameter sind. Die Art der Parameter wird von OpenSCAD automatisch anhand ihrer Verwendung ermittelt. So ist in unserem Beispiel der Parameter groesse ein dreidimensionaler Vektor, während loch_dm und loch_abst einfache Zahlen sind. Der Inhalt eines Moduls ist eine mit geschweiften Klammern eingefasste Geometriemenge - ganz so wie wir es bereits bei den Booleschen Operatoren kennengelernt haben.

Abbildung 2.: Einfache Wiederverwendbarkeit durch Module

Hat man das Modul einmal definiert, kann man es wie eine der Grundformen (Kugel, Quader, etc.) verwenden. In unserem Beispiel haben wir drei Lochplatten erstellt, von denen wir zwei mittels Verschiebe-Transformationen (translate) arrangiert haben (Abbildung 2.).

Innerhalb des Moduls haben wir unsere ursprüngliche Geometriebeschreibung übernommen und lediglich die globalen Variablen durch die Variablen aus der Parameterliste des Moduls getauscht. Wir haben die letzte Variante aus dem vorhergehenden Abschnitt genommen, da wir diese nun nutzen können, um den Funktionsumfang unseres Moduls so zu erweitern, dass wir die Anzahl der Bohrungen parametrisieren können:

module lochplatte( groesse, loch_dm, loch_abst, anzahl = [2,2] ) {

	difference() {

		cube( groesse );

		randabstand   = loch_abst + loch_dm/2;
		x_lochdistanz = (groesse.x - 2*randabstand) / (anzahl.x - 1);
		y_lochdistanz = (groesse.y - 2*randabstand) / (anzahl.y - 1);
		x_werte       = [randabstand : x_lochdistanz : groesse.x - randabstand + 0.1];
		y_werte       = [randabstand : y_lochdistanz : groesse.y - randabstand + 0.1];

		// Bohrlöcher
		for (x = x_werte, y = y_werte)
	    translate( [x, y, -1] )
	    color( "red" )
	    cylinder( d = loch_dm, h = groesse.z + 2);

	}

}

lochplatte( [100,50,5], 6, 4 );

translate( [0,60,0] )
lochplatte( 
	groesse   = [50,50,5], 
	loch_dm   = 3, 
	loch_abst = 2,
	anzahl    = [4,6]
);

translate( [60,60,0] )
lochplatte( [50,50,5], loch_dm = 5, loch_abst = 5, anzahl = [4,3] );

Wir haben unserem Modul nun einen Parameter anzahl hinzugefügt und dem Parameter einen Standardwert gegeben (hier: [2,2], ein zweidimensionaler Vektor). Da wir für die For-Schleife zwei Spannen (x_werte und y_werte) verwendet haben, müssen wir nur die Schrittweiten x_lochdistanz und y_lochdistanz so anpassen, das die gewünschte Anzahl Löcher in X- und Y-Richtung erzeugt werden. Auch hier können uns Rundungsfehler in die Quere kommen. Daher müssen wir auch den Endwert der For-Schleifen leicht anpassen (+ 0.1). Da die tatsächlich in der Geometrie verwendeten Werte ausschließlich auf Basis der Schrittweiten ermittelt werden, erzeugen wir mit dieser Anpassung keine Ungenauigkeit in unserer Konstruktion.

Abbildung 2.: Einfache Erweiterung unseres Moduls

Da wir den neuen Parameter anzahl unseres Moduls mit einem Standardwert versehen haben, ist das Modul “abwärtskompatibel”. So hat sich die erste Verwendung unseres Moduls nicht verändert (Abbildung 2.).

Fallunterscheidungen

Unser Modul hat noch ein paar Schönheitsfehler. Wenn der Parameter anzahl den Wert 1 enthält, dann enthält die Berechnung der zugehörigen Lochdistanz eine Division durch 0. Dies ist nicht gut und wir sollten uns unbedingt darum kümmern. Wie wäre es, wenn wir im Falle einer Anzahl von 1 das zugehörige Loch mittig platzieren? Um dies zu erreichen, brauchen wir eine Fallunterscheidung sowohl bei der Bestimmung der Lochdistanzen (x_lochdistanz und y_lochdistanz) als auch bei der Festlegung der Wertebereiche (x_werte und y_werte) der Schleifenvariablen:

module lochplatte( groesse, loch_dm, loch_abst, anzahl = [2,2] ) {

	difference() {

		cube( groesse );

		randabstand   = loch_abst + loch_dm/2;
		x_lochdistanz = anzahl.x > 1 ? (groesse.x - 2*randabstand) / (anzahl.x - 1) : 0;
		y_lochdistanz = anzahl.y > 1 ? (groesse.y - 2*randabstand) / (anzahl.y - 1) : 0;
		x_werte       = anzahl.x > 1 ? 
						[randabstand : x_lochdistanz : groesse.x - randabstand + 0.1] :
						[groesse.x / 2];
		y_werte       = anzahl.y > 1 ?
		                [randabstand : y_lochdistanz : groesse.y - randabstand + 0.1] :
		                [groesse.y / 2];

		// Bohrlöcher
		for (x = x_werte, y = y_werte)
	    translate( [x, y, -1] )
	    color( "red" )
	    cylinder( d = loch_dm, h = groesse.z + 2);

	}

}

lochplatte( [100,50,5], 6, 4 );

translate( [0,60,0] )
lochplatte( 
	groesse   = [50,50,5], 
	loch_dm   = 3, 
	loch_abst = 2,
	anzahl    = [1,1]
);

translate( [60,60,0] )
lochplatte( [50,50,5], loch_dm = 5, loch_abst = 5, anzahl = [2,1] );

Die Fallunterscheidungen verwenden den Fragezeichenoperator, der ganz allgemein die Struktur a ? b : c hat. Der Teil a ist immer eine Ja- oder Nein-Frage (hier z.B.: “ist anzahl.x größer 1”). Wenn die Antwort ja ist, wird b als Wert genommen, wenn die Antwort nein ist, wird c als Wert genommen.

Wenn Sie etwas Programmiererfahrung haben, werden Sie vielleicht zuerst an einen if-Ausdruck gedacht haben, um die obige Fallunterscheidung vorzunehmen. Dies ist wieder so ein Fall, wo die “normale” Programmierintuition eher hinderlich ist. Wir können den Variablen in OpenSCAD ja nur einmal einen Wert geben! Der folgende Ausdruck würde in OpenSCAD daher nicht funktionieren:

x_lochdistanz = 0;
if (anzahl.x > 1) {
	x_lochdistanz = (groesse.x - 2*randabstand) / (anzahl.x - 1);
}

Dennoch gibt es auch das Schlüsselwort if in OpenSCAD. Man kann es verwenden, um ganze Teile der Geometriebeschreibung ein- oder auszuschließen. Wir können es in unserem Beispiel dazu nutzen, noch einen weiteren Schönheitsfehler in unserem Modul zu beseitigen. Im Moment würde eine Anzahl von 0 oder irgendeinem negativen Wert auch zu einem einzelnen Loch führen. Das scheint nicht zu passen. Wenn wir bei der Anzahl eine 0 angeben, dann wollen wir offenbar gar kein Loch:

module lochplatte( groesse, loch_dm, loch_abst, anzahl = [2,2] ) {

	if (anzahl.x == 0 || anzahl.y == 0) {

		cube( groesse );

	} else {

		difference() {

			cube( groesse );

			randabstand   = loch_abst + loch_dm/2;
			x_lochdistanz = anzahl.x > 1 ? (groesse.x - 2*randabstand) / (anzahl.x - 1) : 0;
			y_lochdistanz = anzahl.y > 1 ? (groesse.y - 2*randabstand) / (anzahl.y - 1) : 0;
			x_werte       = anzahl.x > 1 ? 
							[randabstand : x_lochdistanz : groesse.x - randabstand + 0.1] :
							[groesse.x / 2];
			y_werte       = anzahl.y > 1 ?
			                [randabstand : y_lochdistanz : groesse.y - randabstand + 0.1] :
			                [groesse.y / 2];

			// Bohrlöcher
			for (x = x_werte, y = y_werte)
		    translate( [x, y, -1] )
		    color( "red" )
		    cylinder( d = loch_dm, h = groesse.z + 2);

		}

	}
}

lochplatte( [100,50,5], 6, 4 );

translate( [0,60,0] )
lochplatte( 
	groesse   = [50,50,5], 
	loch_dm   = 3, 
	loch_abst = 2,
	anzahl    = [0,1]
);

translate( [60,60,0] )
lochplatte( [50,50,5], loch_dm = 5, loch_abst = 5, anzahl = [2,1] );

Hier unterscheiden wir also gleich zu Beginn, ob wir eine Platte mit oder ohne Löcher modellieren wollen und verzweigen unsere Geometriebeschreibung entsprechend. Gleichheit wird mit dem doppelten Gleichheitszeichen (==) ausgedrückt. Die zwei senkrechten Striche (||) haben die Bedeutung eines logischen “oder” im Sinne von “es muss nur eine der beiden Fragen mit ja beantwortet werden”.

Externe Geometrie

Nehmen wir an, wir möchten unsere Lochplatte in einem anderen Projekt wiederverwenden. Auch hier wäre es eine schlechte Idee, die Geometriebeschreibung einfach in das neue Projekt hineinzukopieren. OpenSCAD bietet zwei Befehle an, um andere .scad-Dateien in ein Projekt einzubinden:

include <lochplatte.scad>;

Der include-Befehl lädt eine andere .scad-Datei (hier: lochplatte.scad) komplett in die aktuelle Geometriebeschreibung ein. Dies bedeutet, dass auch die drei Testplatten, die wir unterhalb unseres Moduls definiert haben, in der neuen Geometriebeschreibung auftauchen würden. Um dies zu vermeiden, gibt es in OpenSCAD anstelle des include-Befehls auch noch den Befehl use:

use <lochplatte.scad>;

Verwendet man use anstelle von include werden nur die Module und Funktionen aus der anderen .scad-Datei übernommen. Die Datei, die man innerhalb der spitzen Klammern angibt, muss entweder im gleichen Verzeichnis liegen, wie die Datei der aktuellen Geometriebeschreibung, oder im Library-Folder (dt. Bibliotheksverzeichnis) von OpenSCAD. Über den Menüpunkt File -> Show Library Folder kann man sich das Bibliotheksverzeichnis anzeigen lassen und dort ggf. seine Geometriebibliotheken ablegen. Im Internet lassen sich bereits eine Reihe ganz ausgezeichneter Geometriebibliotheken für OpenSCAD finden. Das Bibliotheksverzeichnis ist der Ort, wo man diese hinkopieren muss, um sie zu verwenden.

Hat man eine Geometrie nicht als OpenSCAD-Geometriebeschreibung vorliegen, kann man eine ganze Reihe von Dateiformaten mit dem import-Schlüsselwort verwenden.

import("logo.svg");

Die geladene Geometrie kann dann wie eine Grundform (Kugel, Quader, etc.) oder ein Modul verwendet werden (z.B. translate(...) import("logo.svg");). OpenSCAD unterstützt DXF und SVG als zweidimensionale Formate und STL, OFF, AMF und 3MF als dreidimensionale Formate.

Zusammenfassung

Herzlichen Glückwunsch! Sie kennen jetzt alle wesentlichen Konzepte und Funktionsweisen von OpenSCAD. Alles weitere sind tatsächlich “nur” noch Details. In den nachfolgenden Beispielprojekten werden wir den Umgang mit diesen Konzepten üben und vertiefen und uns nach und nach die übrigen Details erarbeiten. Sie werden sehen, dass ihr Verständnis der Materie mit jedem Projekt schrittweise besser werden wird, so dass Sie bald eine klare Vorstellung davon haben, wie sie von einer Idee zu einer fertigen Geometriebeschreibung kommen.

Projekt 1: Regalbodenwinkel

In diesem Projekt geht es darum, einen Regalbodenwinkel zu konstruieren. Hierbei sollen sowohl die Längen der beiden Schenkel als auch die Größe und Anzahl der Löcher frei einstellbar sein.

Abbildung 3.: Regalbodenwinkel

Was ist neu?

Wir werden die Boolesche Vereinigung (engl. union) als neue Boolesche Operation kennenlernen. Darüber hinaus werden wir ein paar mathematische Funktionen (Wurzel, Potenz und Arkuscosinus) von OpenSCAD nutzen sowie mehr über die Funktionsweise von Modulen erfahren. Zu guter Letzt werden wir uns die Projektionsfunktion (engl. projection) anschauen und sie dazu verwenden, uns Bohrschablonen für unseren Regalbodenwinkel zu erstellen.

Los geht’s

Lassen Sie uns als erstes ein leeres Modul regalbodenwinkel erstellen und darüber nachdenken, welche Parameter wir benötigen. Wir wollen die Länge jeder Seite, sowie die Größe und Anzahl der Löcher verändern können. Darüber hinaus hat unser Winkel sicher auch eine Breite und wir müssen wissen, welche Materialstärke der Winkel haben soll. Das sind eine Menge Parameter! Damit unsere Parameterliste nicht allzu lang wird, können wir uns vornehmen die Werte jeder Seite (Länge, Lochdurchmesser, Anzahl der Löcher) in einem dreidimensionalen Vektor zusammenfassen:

/*
	Modul Regalbodenwinkel

	Parameter:	
	- seite_a ist ein Vektor [Länge, Lochdurchmesser, Anzahl Löcher]
	- seite_b ist ein Vektor [Länge, Lochdurchmesser, Anzahl Löcher]
	- breite ist die Breite des Winkels
	- dicke ist die Materialstärke des Winkels	
 */

module regalbodenwinkel( seite_a, seite_b, breite, dicke ) {

}

regalbodenwinkel(
  seite_a = [50, 6, 1],
  seite_b = [75, 4, 3],
  breite  = 35,
  dicke   = 4
);

Über unserem Modul haben wir einen mehrzeiligen Kommentar (/* ... */) verfasst, der kurz erläutert, welche Parameter es gibt und welche Art von Eingaben diese Parameter erwarten. Auch wenn niemand anderes ihr Modul nutzen wird, ist ein solcher Kommentar sinnvoll. Wenn Sie in 6 Monaten nochmal einen Regalbodenwinkel brauchen, werden Sie sich über den hilfreichen Kommentar freuen! Unterhalb des Moduls regalbodenwinkel haben wir das Modul einmal mit konkreten Werten instanziiert. Ohne diese konkrete Instanz, würden wir bei der schrittweisen Entwicklung des Moduls keine Geometrie im Ausgabefenster sehen. Darüber hinaus können wir die Instanz nutzen, um während der Entwicklung immer mal wieder die Parameter zu verändern, um zu prüfen, ob sich unsere Geometriebeschreibung so verhält, wie von uns erwartet.

Wenn man den geplanten Regalbodenwinkel betrachtet (Abbildung 3.), dann fällt auf, dass die beiden Seiten des Winkels im Prinzip gleich sind. Sie bestehen aus einem Quader und gleichmäßig über die Mittelachse verteilten Löchern. Wir brauchen diese Geometrie für beide Seiten. Daher lohnt es sich, auch hierfür ein Modul zu schreiben. Da wir dieses Modul nur für den Winkel brauchen, definieren wir es innerhalb des Moduls regalbodenwinkel als Untermodul xloch_platte und verwenden anschließend das Untermodul für Seite A und Seite B des Winkels:

/* ... */
module regalbodenwinkel( seite_a, seite_b, breite, dicke ) {

	module xloch_platte(groesse, l_dm, l_anz, randabstand) {
		
		difference(){

			cube(groesse);

			lochabstand = (groesse.x - randabstand) / (l_anz + 1);

		    for (x = [1:l_anz])
		    translate ([
		        randabstand + x * lochabstand,
		        groesse.y/2,
		        -1
		    ]) cylinder( d = l_dm, h = groesse.z + 2, $fn=18);

		}		
	}

	// Seite A
	xloch_platte(
		[seite_a[0], breite, dicke],
		seite_a[1],
		seite_a[2],
		dicke
	);

	// Seite B
    translate([dicke,0,0])
    rotate([0,-90,0])
	xloch_platte(
		[seite_b[0], breite, dicke],
		seite_b[1],
		seite_b[2],
		dicke
	);

}

/* ... */

Das Untermodul xloch_platte bekommt als Parameter die Größe des Seitenteils als dreidimensionalen Vektor (groesse), den Durchmesser (l_dm) und die Anzahl (l_anz) der Löcher sowie einen randabstand. Letzterer wird benötigt, da an der Stelle, wo die beiden Seitenteile des Winkels aufeinandertreffen, die Oberfläche der Seitenteile durch die Materialstärke der jeweils anderen Seite verkleinert wird. Da wir unsere Löcher entlang dieser Fläche verteilen wollen, müssen wir diese Verkleinerung berücksichtigen. Rein theoretisch hätten wir die Materialstärke auch aus dem Parameter groesse entnehmen können (groesse.z). Die Verwendung eines extra Parameters erleichtert hier einfach die Lesbarkeit bzw. das Verständnis.

Die Platte modellieren wir mit einem einfachen Quader, dem wir den Parameter groesse übergeben (cube(groesse);). Die Löcher erstellen wir mit einer For-Schleife und ziehen diese dann mittels der Booleschen Differenzoperation von der Platte ab. In diesem Beispiel lassen wir die For-Schleife von 1 bis zur Anzahl der Löcher laufen (x = [1:l_anz]) und rechnen die tatsächliche Position des jeweiligen Loches erst in der Verschiebe-Transformation innerhalb der Schleife aus (randabstand + x * lochabstand). Wir starten hierbei immer mit dem Randabstand und addieren dann die jeweils passende Zahl an Lochabständen auf. Den Lochabstand haben wir vor der Schleife definiert. Hierfür haben wir die verfügbare Länge (Länge der Seite minus Randabstand) durch die Anzahl der Lochzwischenräume (Anzahl der Löcher plus 1) geteilt. Für die Löcher selbst verwenden wir einen Zylinder, dem wir als Durchmesser d den Parameter l_dm zuweisen. Die Höhe h ermitteln wir aus der Höhe der Platte (groesse.z) und zwei Millimetern Zugabe, damit unser Loch auch sauber wird. Bei der Verschiebe-Transformation haben wir deswegen als Z-Verschiebung den Wert -1 angegeben. Zusammen mit der Zugabe von 2 Millimetern steht damit der Zylinder ober- und unterhalb der Platte jeweils genau 1 Millimeter vor. Der dritte Parameter $fn=18 des Zylinders ist neu. Hierbei handelt es sich um eine “Spezialvariable” von OpenSCAD mit der man die Feinheit der Geometrie des Zylinders einstellen kann. Je größer der Wert von $fn ist, desto feiner wird die Geometrie. Man sollte es an dieser Stelle nicht übertreiben. Werte von über 100 bei der Variable $fn benötigt man praktisch nie und würden die Geometrie nur unnötig rechenaufwändig machen. Man kann die Variable $fn übrigens auch “global” setzen. In diesem Fall gilt sie für alle Geometrien. Erfahrungsgemäß ist es jedoch besser, sie ganz gezielt dort einzusetzen, wo man die Feinheit der Geometrie erhöhen möchte.

Direkt unterhalb der Moduldefinition von xloch_platte benutzen wir das Modul für die beiden Seiten A und B. Da die Seite B senkrecht auf Seite A steht, müssen wir diese um 90 Grad drehen. In diesem konkreten Fall sind es minus 90 Grad, da wir gegen den Uhrzeigersinn um die Y-Achse drehen wollen. Nach der Drehung steht Seite B noch nicht ganz korrekt. Wir müssen sie noch um die Materialstärke entlang der X-Achse bewegen. Ansonsten würde unsere Seite A zu lang.

Abbildung 3.: Seitenteile des Winkels erstellt durch Untermodul

Unser Regalbodenwinkel sieht jetzt schon ziemlich passabel aus (Abbildung 3.) und wir können uns durch die Veränderung der Parameter vergewissern, dass sich unsere Geometriebeschreibung wie gewünscht verhält. Jetzt fehlen noch zwei Wangen, um unseren Winkel stabiler zu machen. Wir erstellen die Wangen zunächst aus zwei Quadern (cube), die wir unterhalb der Seiten in unserem Modul regalbodenwinkel definieren:

/* ... */
module regalbodenwinkel( seite_a, seite_b, breite, dicke ) {

	module xloch_platte(groesse, l_dm, l_anz, randabstand) {
		/* ... */
	}

	// Seite A
	xloch_platte(
		[seite_a[0], breite, dicke],
		seite_a[1],
		seite_a[2],
		dicke
	);

	// Seite B
    translate([dicke,0,0])
    rotate([0,-90,0])
	xloch_platte(
		[seite_b[0], breite, dicke],
		seite_b[1],
		seite_b[2],
		dicke
	);

	// Wangen
	cube( [seite_a[0], dicke, seite_b[0]] );

	translate( [0, breite-dicke, 0] )
	cube( [seite_a[0], dicke, seite_b[0]] );

}

/* ... */
Abbildung 3.: Rohe Wangen des Winkels

Rein technisch würden unsere Wangen schon jetzt ihren Zweck erfüllen (Abbildung 3.). Jedoch wäre es schöner, wenn unsere Regalbodenwinkel passend abgeschrägte Wangen hätten. Wir können dieses Ziel erreichen, indem wir von unserem bisherigen Winkel einen passend gedrehten Quader abziehen. Damit dies möglich wird, müssen wir unsere bisherige Geometriebeschreibung, die aus vier einzelnen Geometrien besteht (zwei mal xloch_platte und zwei mal cube) zu einer einzigen Geometrie vereinen. Dies können wir durch die Boolesche Vereinigungsoperation (engl. union) erreichen:

/* ... */
module regalbodenwinkel( seite_a, seite_b, breite, dicke ) {

	module xloch_platte(groesse, l_dm, l_anz, randabstand) {
		/* ... */
	}

	union() {

		// Seite A
		xloch_platte(
			[seite_a[0], breite, dicke],
			seite_a[1],
			seite_a[2],
			dicke
		);

		// Seite B
	    translate([dicke,0,0])
	    rotate([0,-90,0])
		xloch_platte(
			[seite_b[0], breite, dicke],
			seite_b[1],
			seite_b[2],
			dicke
		);

		// Wangen
		cube( [seite_a[0], dicke, seite_b[0]] );

		translate( [0, breite-dicke, 0] )
		cube( [seite_a[0], dicke, seite_b[0]] );

	}

}

/* ... */

Wie schon bei der Booleschen Differenz werden auch bei der Booleschen Vereinigung die einzelnen Geometrien mit Hilfe von geschweiften Klammern ({ ... }) zusammengefasst. Anstelle des Schlüsselwortes difference kommt hier union zum Einsatz. Jetzt kann von allen vier Geometrien gleichzeitig ein Quader abgezogen werden, um den gewünschten Effekt zu erreichen.

Abbildung 3.: Gesucht ist die Diagonale und der Winkel

Jetzt stellt sich nur die Frage, wie groß der Quader sein muss und in welchem Winkel er gekippt werden muss (Abbildung 3.). Hier können uns vage Erinnerungen an den Matheunterricht helfen. Laut Pythagoras entspricht im rechtwinkligen Dreieck die Summe der Seitenquadrate dem Quadrat der Hauptseite. Wenn man also die Wurzel aus der Summe der Seitenquadrate zieht, bekommt man die Länge der gesuchten Diagonalen heraus. Den Winkel kann man über den Arkuscosinus ermitteln. Wie man z.B. bei Wikipedia nachschlagen kann, ist der Cosinus eines Winkels gleich der Ankathete geteilt durch die Hypothenuse. Die Hypothenuse ist hier unsere Diagonale, die Ankathete ist die Seite, an der der Winkel anliegt (im Unterschied zur Gegenkathete, die dem Winkel gegenüberliegt). Beide Werte, die Länge der Diagonalen und der Winkel der Diagonalen, können wir mit den von OpenSCAD zur Verfügung gestellten mathematischen Funktionen ausrechnen und dann für die korrekte Positionierung und Drehung des Quaders verwenden:

/* ... */
module regalbodenwinkel( seite_a, seite_b, breite, dicke ) {

	module xloch_platte(groesse, l_dm, l_anz, randabstand) {
		/* ... */
	}

	difference() {

		union() {

			// Seite A
			xloch_platte(
				[seite_a[0], breite, dicke],
				seite_a[1],
				seite_a[2],
				dicke
			);

			// Seite B
		    translate([dicke,0,0])
		    rotate([0,-90,0])
			xloch_platte(
				[seite_b[0], breite, dicke],
				seite_b[1],
				seite_b[2],
				dicke
			);

			// Wangen
			cube( [seite_a[0], dicke, seite_b[0]] );

			translate( [0, breite-dicke, 0] )
			cube( [seite_a[0], dicke, seite_b[0]] );

		}

		diag   = sqrt( pow(seite_a[0], 2) + pow(seite_b[0], 2) );
	  	winkel = acos( seite_a[0] / diag );

	    translate( [seite_a[0], -1, 0] )
	    rotate( [0, -(90-winkel) , 0] )
	    cube( [diag, breite + 2, diag + 2] );

	}

}

/* ... */

Unterhalb der mit union vereinigten Geometriemenge haben wir zunächst unsere Diagonale und unseren Winkel ausgerechnet. Die Funktion sqrt berechnet die Wurzel (engl. square root) und die Funktion pow berechnet die Potenz (engl. power) einer Zahl. Bei der Potenzfunktion ist der erste Parameter die Zahl (hier: seite_a[0] bzw. seite_b[0]), die man potenzieren möchte und der zweite Parameter ist der Exponent (hier: 2). Den Winkel berechnen wir mit Hilfe des Arkuscosinus acos wie oben beschrieben aus dem Quotienten von Ankathete und Hypothenuse (hier: seite_a[0] geteilt durch diag). Im Anschluß erstellen wir einen Quader (cube), der diag lang, breite + 2 breit und diag + 2 hoch ist. Auch hier haben wir sowohl bei der Breite als auch bei der Höhe eine kleine Zugabe von zwei Millimetern, damit unsere Differenzoperation später sauber funktioniert. Wir rotieren den Quader um die Y-Achse entgegen dem Urzeigersinn (daher das -). Als Winkel verwenden wir nicht direkt den Winkel, den wir ausgerechnet haben, sondern 90 Grad minus den Winkel. Dies liegt daran, dass wir eigentlich den in Abbildung 3. blau schattierten Winkel benötigen. Wenn Sie sich besonders gut an ihren Matheunterricht erinnern, dann werden Sie jetzt vielleicht argumentieren, dass wir dann gleich den Arkussinus hätten nehmen sollen, da dieser der Wechselwinkel des von uns benötigten Winkels ist. Und Sie hätten Recht! Nachdem wir unseren Quader rotiert haben, müssen wir ihn nur noch an die richtige Position schieben. Dies geschieht mit der Verschiebe-Transformation (translate). Wie erwartet verschieben wir unseren Quader um die Länge der Seite A entlang der X-Achse. Die Verschiebung um -1 entlang der Y-Achse dient der Absicherung der Differenzoperation und korrespondiert zu der Zugabe um 2 Millimeter in der Breite. Um die Zugabe in der Höhe brauchen wir uns an dieser Stelle nicht zu kümmern, da es reicht, wenn der Quader unseren Winkel in Kipprichtung überragt.

Nachdem der Quader jetzt in der richtigen Position ist, können wir ihn endlich von unserer Regalwinkelgeometrie abziehen. Hierzu fassen wir die zuvor erstellte Vereinigung (union) und unseren gerade erstellten Quader wieder in ein paar geschweifte Klammern ein ({ ... }) und stellen dem ganzen die Boolesche Differenzoperation (difference) voran. Geschafft!

Abbildung 3.: Quader für die Abschrägung der Wangen. Sichtbar gemacht mittels #

Ein kleiner Tipp: wenn wir die Position unseres Quaders jetzt noch einmal kontrollieren wollen, ohne ihn erst wieder aus der Booleschen Differenzoperation herauslösen zu müssen, können wir dem Quader (cube) temporär ein # voranstellen. Wenn wir nun eine Vorschau berechnen lassen, wird der Quader in einer halbtransparenten Farbe dargestellt (Abbildung 3.).

Bohrschablonen erstellen

Nehmen wir einmal an, wir haben unseren Regalbodenwinkel mit einem 3D-Drucker hergestellt und wollen ihn nun an der Wand montieren. Wäre es nicht praktisch, wenn wir jetzt eine Bohrschablone zum ausdrucken hätten? Zu diesem Zweck können wir eine 3D zu 2D Projektion nutzen, die in OpenSCAD über die Projektionstransformation (engl. projection) zur Verfügung steht:

/* ... */

projection(cut = true)
regalbodenwinkel(
  seite_a = [50, 6, 1],
  seite_b = [75, 4, 3],
  breite  = 35,
  dicke   = 4
);

Die Projektionstransformation wirkt wie jede Transformation auf das jeweils nachfolgende Element und projiziert dieses auf die X-Y-Ebene. Dies ergibt so etwas wie den zweidimensionalen Schatten der Geometrie. Übergibt man der Projektionstransformation den Parameter cut = true (so wie hier), dann wird die Geometrie in der X-Y-Ebene geschnitten und nur der Schnitt wird angezeigt. Im Falle unseres Regalbodenwinkels führen beide Varianten zum selben Ergebnis. Damit der Schnitt auch wirklich als 2D Geometrie angezeigt wird, muss man übrigens eine vollständige Berechnung anstoßen (F6) und nicht nur eine Vorschau (F5). Nach der Berechnung (F6) kann die 2D-Geometrie als SVG-Grafik exportiert (File -> Export -> Export as SVG) und mit einem Grafikprogramm (z.B. Inkscape) gedruckt werden werden.

Wenn wir nun auch eine Bohrschablone von Seite B haben wollen, müssen wir unseren Winkel vorher um 90 Grad gegen den Uhrzeigersinn um die Y-Achse drehen:

/* ... */

projection(cut = true)
rotate( [0, -90, 0] )
regalbodenwinkel(
  seite_a = [50, 6, 1],
  seite_b = [75, 4, 3],
  breite  = 35,
  dicke   = 4
);

Nun könnte man es dabei belassen und je nach Bedarf die Zeilen mit der Projektion und Rotation ein- bzw. auskommentieren. Alternativ können wir jedoch auch ein spezielles Modul definieren, dass auf Anfrage zwischen 3D Geometrie und Bohrschablonen hin- und herschalten kann:

/* ... */

module ausgabe(schablonen = false) {

	if (schablonen) {

		projection(cut = true)
		children(0);		

		translate( [-0.01, 0, 0] )
		projection(cut = true)
		rotate( [0, -90, 0] )
		children(0);		

	} else {

		children(0);

	}

}

ausgabe(schablonen = false)
regalbodenwinkel(
  seite_a = [50, 6, 1],
  seite_b = [75, 4, 3],
  breite  = 35,
  dicke   = 4
);

Das Modul ausgabe hat einen Parameter schablonen. Wenn dieser auf wahr (engl. true) gesetzt wird, dann werden die Bohrschablonen der Seiten A und B erzeugt (Abbildung 3.). Wird der Parameter auf falsch (engl. false) gesetzt, wird die normale 3D-Geometrie erzeugt. Innerhalb des Moduls ausgabe wird dies mit einer If-Fallunterscheidung beschrieben. Der Ausdruck if (schablonen) ist hierbei eine Abkürzung von if (schablonen == true). Die spannende Frage ist jedoch, wie denn unsere Regalbodenwinkel-Geometrie in das Modul ausgabe kommt. Dies geschieht durch das Schlüsselwort children. Hiermit erhalten wir Zugriff auf das unserem Modul nachfolgende Element! Der Parameter 0 gibt dabei an, dass wir das erste Element haben wollen. Würde hinter unserem Modul eine in geschweiften Klammern ({ ... }) eingefasste Geometriemenge stehen, könnten wir auch auf weitere Elemente zugreifen. Wieviele Elemente es gibt, würde uns in diesem Fall die “Spezialvariable” $children verraten. In unserem Fall wissen wir jedoch, dass es nur ein nachfolgendes Element (unseren Regalbodenwinkel) gibt. Daher benötigen wir $children an dieser Stelle nicht.

Abbildung 3.: Erzeugung von Bohrschablonen mittels projection

Grundsätzlich ermöglicht uns das Schlüsselwort children, Module zu definieren, die sich wie Transformationen verhalten. Aus Erfahrung kann man sagen, dass man diese Fähigkeit nicht allzu oft benötigt. Es gibt jedoch Situationen, wo man sehr elegante Lösungen für ansonsten aufwendige Geometriebeschreibungen damit erreichen kann.

Tipps für den 3D-Druck

Wenn wir unsere Geometriebeschreibung mit einem 3D-Drucker ausdrucken wollen, dann müssen wir unsere Geometrie zunächst berechnen (F6). Dies kann je nach Geometrie auch mal einige Minuten dauern. Haben Sie hier einfach etwas Geduld. Wenn die Berechnung fertig ist, können Sie die entstandene Geometrie exportieren (File -> Export -> Export as …). Ein typisches Format hierfür ist .stl. Die .stl-Datei können Sie dann in eine sogenannte Slicer-Software (to slice -> in Scheiben schneiden) einladen und für den 3D-Druck vorbereiten.

Innerhalb der Slicer-Software stellt sich die Frage, in welcher Ausrichtung man den Regalbodenwinkel drucken möchte. Da 3D-gedruckte Bauteile schichtweise entstehen, ist die Stabilität der Bauteile innerhalb einer Schicht deutlich größer als zwischen den Schichten. Insbesondere Scherkräfte, die auf die Schichten wirken, können ein Bauteil zerbrechen lassen. Für unseren Regalbodenwinkel wäre es daher am besten, ihn auf der Seite liegend zu drucken. Ein Nachteil dieser Ausrichtung ist, dass man dann vermutlich eine Stützstruktur innerhalb des Bauteils benötigt, die die oben liegende Wange beim Druck stabilisiert (Abbildung 3.).

Abbildung 3.: Regalbodenwinkel in einer Slicer-Software (hier: Cura). Auf der Seite liegend wird das Bauteil stabil, benötigt jedoch eine Stützstruktur (blau).

Eine alternative Ausrichtung, die ggf. ohne eine Stützstruktur auskommt und dennoch zu einem stabilen Bauteil führt, besteht darin, den Winkel auf der abgeschrägten Seite liegend zu drucken. Den passenden Winkel direkt in der Slicer-Software einzustellen, kann etwas aufwendig sein. Hier lohnt es sich, die Geometrie bereits passend gedreht aus OpenSCAD heraus zu exportieren. Hierzu können wir unser Modul regalbodenwinkel noch einmal erweitern:

module regalbodenwinkel( seite_a, seite_b, breite, dicke, drehen = false ) {

	module xloch_platte(groesse, l_dm, l_anz, randabstand) {
		/* ... */
	}

    diag   = sqrt( pow(seite_a[0], 2) + pow(seite_b[0], 2) );
    winkel = asin(seite_a[0] / diag);

    rotate( [0, drehen ? 90 + winkel : 0, 0] )
	difference() {

		union() {

			// Seite A
			xloch_platte(
				[seite_a[0], breite, dicke],
				seite_a[1],
				seite_a[2],
				dicke
			);

			// Seite B
		    translate([dicke,0,0])
		    rotate([0,-90,0])
			xloch_platte(
				[seite_b[0], breite, dicke],
				seite_b[1],
				seite_b[2],
				dicke
			);

			// Wangen
			cube( [seite_a[0], dicke, seite_b[0]] );

			translate( [0, breite-dicke, 0] )
			cube( [seite_a[0], dicke, seite_b[0]] );

		}

	  translate([seite_a[0],-1,0])
	  rotate([0,-(winkel),0])
	  cube([diag,breite+2,diag+2]);

	}

}

Wir geben unserem Modul einen weiteren Parameter drehen, dem wir den Standardwert false (dt. falsch) geben. Anschließend ziehen wir die Berechnung der Diagonale und des Winkels nach oben vor die Boolesche Differenz (difference) und drehen unser ganzes Objekt um die Y-Achse im Uhrzeigersinn um 90 + winkel Grad wenn der Parameter drehen wahr (engl. true) ist. Ansonsten drehen wir nicht (0 Grad).

Abbildung 3.: Regalbodenwinkel in einer Slicer-Software (hier: Cura). Auf der abgeschrägten Seite liegend wird das Bauteil ebenfalls stabil, benötigt aber keine Stützstruktur.

Exportieren wir nun unsere Geometrie erneut als .stl-Datei, dann liegt unser Bauteil direkt in der richtigen Ausrichtung in der Slicer-Software und kann ohne Stützstruktur und dennoch als stabiles Objekt gedruckt werden (Abbildung 3.).

Nachdem Sie den Regalbodenwinkel gedruckt haben, ist es ratsam, einmal alle Maße des gedruckten Objektes nachzumessen. Insbesondere bei den Löchern kann es dazu kommen, dass diese nicht maßgetreu gedruckt wurden. Wenn dies der Fall ist, können Sie jetzt von der Stärke der parametrischen Modellierung profitieren. Sie können einfach in den Parametern des Regalbodenwinkels die Durchmesser entsprechend anpassen und auf Knopfdruck eine angepassten Geometrie berechnen lassen.

Download der OpenSCAD-Datei dieses Projektes

Projekt 2: Spezialdübel

Wenn Sie in einem älteren Haus wohnen, kennen Sie vielleicht dieses Problem. Sie möchten etwas an Wand oder Decke aufhängen, beginnen ein Loch zu bohren und plötzlich fängt der Bohrer an zu wandern. Am Ende haben Sie ein ziemlich großes Loch in der Wand, dass für die Schrauben, die Sie verwenden wollen, viel zu groß ist. In diesem Projekt wollen wir einen Spezialdübel entwerfen, der Ihnen in diesem Fall helfen kann.

Abbildung 4.: Spezialdübel in eckig und rund

Was ist neu?

Wir werden unser erstes Extrusionsobjekt erstellen und dazu die 2D-Grundformen Quadrat (engl. square) und Kreis (engl. circle) sowie die lineare Extrusionstransformation linear_extrude verwenden. Darüber hinaus werden wir neue Varianten der Rotationstransformation rotate und der 3D-Grundform cylinder kennenlernen und unser Wissen über bereits bekannte Prinzipien und Funktionen vertiefen.

Los geht’s

Lassen Sie uns auch hier damit beginnen, erst einmal ein Modul und eine Testinstanz dieses Moduls zu definieren:

// Ein Spezialdübel mit vielen Einstellmöglichkeiten

module duebel (
    bohrloch_dm,   // Durchmesser des Bohrlochs in Millimeter
    schraube_dm,   // Durchmesser der Schraube in Millimeter
    laenge         // Länge des Dübels in Millimeter
){

}

duebel(8,5,50);

Da wir unserem Modul noch eine ganze Reihe an Parametern hinzufügen werden, arrangieren wir die Parameter dieses Mal als vertikale Liste. Dies erlaubt es uns zudem, die Parameter an Ort und Stelle mit einem erläuternden Kommentar zu versehen. Lassen Sie uns zunächst einen Dübel mit kantigem Grundkörper modellieren. Die runde Version des Dübels werden wir erst gegen Ende des Projektes der Modellbeschreibung als Option hinzufügen. Wenn der Dübel einen quadratischen Querschnitt hat und er in ein rundes Bohrloch passen soll, dann muss die Diagonale des Quadrats dem Durchmesser des Bohrlochs entsprechen. Um nun vom Durchmesser des Bohrlochs auf die Seitenlänge des Quadrats zu schließen, kann uns wieder der alte Pythagoras helfen:

/* ... */
module duebel (
    bohrloch_dm,   // Durchmesser des Bohrlochs in Millimeter
    schraube_dm,   // Durchmesser der Schraube in Millimeter
    laenge         // Länge des Dübels in Millimeter
){

    seitenlaenge = sqrt( pow(bohrloch_dm, 2) / 2 );

    linear_extrude(height = laenge)
    square( seitenlaenge, center = true );

}
/* ... */

Wir wissen, dass laut Pythagoras a^2 + b^2 = c^2 im rechtwinkligen Dreieck gilt. In diesem Fall ist die lange Seite des Dreieck c gegeben und wir suchen die Länge der kleinen Seiten. Da unser Dreieck in einem Quadrat liegt, wissen wir schonmal, dass die kleinen Seiten gleich lang sind. Wir können also unsere Formel umschreiben zu a^2 + a^2 = c^2 bzw. 2 x a^2 = c^2. Wenn wir jetzt noch die 2 auf die andere Seite ziehen, sind wir schon fast am Ziel: a^2 = c^2 / 2. Um a herauszubekommen, müssen wir nur noch die Wurzel ziehen: a = sqrt( c^2 / 2 ). Geschafft!

Den eckigen Grundkörper des Dübels beschreiben wir diesmal nicht durch die 3D-Grundform cube sondern über sein zweidimensionales Gegenstück square, welches wir anschließend mit einer linearen Extrusion (linear_extrude) in die Länge ziehen. Welchen Vorteil diese Vorgehensweise hat, sehen wir im nächsten Schritt.

Wie eingangs erwähnt, soll unser Dübel auch in “schwierigen” Bohrlöchern funktionieren, die durch ein verrutschen des Bohrers größer geworden sind als geplant. Daher verändern wir nun die Grundform so, dass sie keilförmig wird. Hierzu erstellen wir zwei neue Parameter, die uns erlauben, unseren Dübel nach Bedarf anzupassen. Dies wäre zum einen der Parameter uebergroesse, der dem Bohrdurchmesser hinzugefügt wird und zum anderen der Parameter aussen_vj, der einen Verjüngungsfaktor angibt, mit dem der Dübel zum Ende hin dünner wird. Man könnte an dieser Stelle sich auch überlegen, einen “Zieldurchmesser” anzugeben. Dies hätte jedoch zur Folge, dass man diesen Durchmesser immer mit angeben muss, da er ja passend zum Bohrlochdurchmesser gewählt werden muss. Die Version mit dem Verjüngungsfaktor erlaubt, dem Parameter einen Standardwert zu geben, der sich implizit auf den Bohrdurchmesser bezieht und damit nur geändert werden muss, wenn dieser Standardwert zu keinem guten Ergebnis führt. In der folgenden Geometriebeschreibung wird nun sichtbar, warum wir die Kombination square + linear_extrude anstelle von cube gewählt haben:

/* ... */
module duebel (
    bohrloch_dm,         // Durchmesser des Bohrlochs in Millimeter
    schraube_dm,         // Durchmesser der Schraube in Millimeter
    laenge,              // Länge des Dübels in Millimeter
    uebergroesse = 2,    // Übergröße des Außendurchmessers
    aussen_vj    = 0.75, // Verjüngungsfaktor des Außendurchmessers
){

    aussen_dm    = bohrloch_dm + uebergroesse;
    seitenlaenge = sqrt( pow(aussen_dm, 2) / 2 );

    linear_extrude(height = laenge, scale = aussen_vj)
    square( seitenlaenge, center = true );

}
/* ... */

Die Transformation linear_extrude bietet einen weiteren Parameter scale an, mit dem man die zu extrudierende 2D-Geometrie entlang der Extrusion verkleinern bzw. vergrößern kann. Mit Hilfe dieses Parameters können wir also sehr einfach die gewünschte, nach hinten zulaufende Form des Dübels beschreiben. Der Parameter uebergroesse kommt bei der Berechnung der seitenlaenge zum Einsatz.

Der Hohlraum innerhalb unseres Dübels, in dem wir die Schraube einschrauben, hat die Form eines Trichters. Zu Beginn hat das Loch im Dübel den Durchmesser der Schraube. Dann verjüngt es sich, damit die Schraube die Seiten des Dübels auseinanderdrücken und gegen die Innenseiten des Bohrlochs pressen kann. Wir können diese Trichterform aus zwei Zylindern herstellen und mittels einer Booleschen Differenzoperation von unserem Grundkörper abziehen:

/* ... */
module duebel (
    bohrloch_dm,           // Durchmesser des Bohrlochs in Millimeter
    schraube_dm,           // Durchmesser der Schraube in Millimeter
    laenge,                // Länge des Dübels in Millimeter
    uebergroesse   = 2,    // Übergröße des Außendurchmessers
    aussen_vj      = 0.75, // Verjüngungsfaktor des Außendurchmessers
    schraube_vj    = 0.2,  // Verjüngungsfaktor des Innendurchmessers
    schraube_vjend = 0.3,  // Relatives Ende der Verjüngung entlang der Tiefe    
){

	difference() {

    	aussen_dm    = bohrloch_dm + uebergroesse;
	    seitenlaenge = sqrt( pow(aussen_dm, 2) / 2 );

	    linear_extrude(height = laenge, scale = aussen_vj)
	    square( seitenlaenge, center = true );

	    // Loch in Trichterform
	    svj_ende = laenge * schraube_vjend;
	    vj_dm    = schraube_dm * schraube_vj;

	    translate([0,0,-0.01])
	    union() {
	        $fn = 24;

	        cylinder( d1 = schraube_dm, d2 = vj_dm, h = svj_ende + 0.01);
	        
	        translate( [0, 0, svj_ende] )
	        cylinder( d = vj_dm, h = laenge - svj_ende + 0.02 );
	    }

	}

}
/* ... */

Wir haben unserem Modul zwei weitere Parameter hinzugefügt. Der Parameter schraube_vj setzt den Verjüngungsfaktor des Innendurchmessers und bestimmt, wie dünn der Hals unserer Trichterform wird. Der Parameter schraube_vjend bestimmt die Position an der der Hals unserer Trichterform beginnt. Die Position wird relativ zur laenge des Dübels angegeben. Innerhalb der Modulbeschreibung haben wir die Definition unseres Dübel-Grundkörpers zum ersten Element einer Booleschen Differenzoperation (difference) gemacht. Als zweites Element, welches vom Grundkörper abgezogen werden soll, haben wir unsere Trichterform beschrieben. Für diese berechnen wir zunächst aus den relativen Parametern schraube_vj und schraube_vjend die zugehörigen Absolutwerte svj_ende und vj_dm. Im Anschluss beschreiben wir die Trichterform mittels zweier Zylinder (engl. cylinder). Beim ersten Zylinder kommt eine neue Form der Parametrisierung zum Einsatz. Anstelle des Durchmessers d geben wir zwei Durchmesser d1 und d2 an. Hierdurch wird der Zylinder als stumpfer Kegel erzeugt, der unten den Durchmesser d1 und oben den Durchmesser d2 hat. In unserem Fall beginnen wir mit dem Schraubendurchmesser schraube_dm und enden mit dem Durchmesser der Verjüngung vj_dm. Die Höhe entspricht dem ausgerechneten Ende der Verjüngung svj_ende plus einer minimalen Zugabe. Der zweite Zylinder wird wieder ganz regulär mit nur einem Durchmesser definiert und beschreibt den Hals unserer Trichterform mit Durchmesser vj_dm und der restlichen Länge des Dübels (laenge - svj_ende) plus ebenfalls einer Zugabe. Schließlich fassen wir beide Zylinder mit einer Booleschen Vereinigung (union) zusammen und verschieben die Trichterform minimal nach unten, damit die Differenzoperation mit dem Dübel-Grundkörper sauber abläuft. An dieser Stelle können wir noch auf ein weiteres Detail eingehen: innerhalb der Booleschen Vereinigung (union) steht an erster Stelle die Spezialvariable $fn, mit der wir die Feinheit unserer Geometrie steuern können. Da wir die Feinheit beider Zylinder erhöhen wollen, können wir die Variable $fn einfach innerhalb der lokalen Geometriemenge der Booleschen Operation setzen, anstatt sie für jeden Zylinder einzelnd als Parameter angeben zu müssen.

Abbildung 4.: Vorschau einzelner Geometrieteile durch das temporäre Voranstellen eines !-Zeichens

Da unsere Trichterform der subtraktive Teil einer Differenzoperation ist, ist es etwas schwierig, die Form “blind” zu modellieren. Wenn wir nun der Trichterform ein Ausrufezeichen voranstellen (!translate( ... ) union() ...) und eine Vorschau berechnen lassen, dann wird nur die Trichterform gezeichnet und keine andere Geometrie (Abbildung 4.). Diese selektive Vorschau einzelner Geometrieteile nutzt man erfahrungsgemäß sehr häufig und ist neben der transparenten Darstellung durch Voranstellen eines #-Zeichens (Abbildung 4.) eine der am häufigsten genutzten Modellierungshilfen.

Unserem Dübel fehlt jetzt noch die Möglichkeit, sich aufzuspreizen, wenn eine Schraube in den Dübel gedreht wird. Hierzu schlitzen wir den Dübel auf allen vier Seiten mittels geeigneter Quader (engl. cube) ein:

/* ... */
module duebel (
    bohrloch_dm,           // Durchmesser des Bohrlochs in Millimeter
    schraube_dm,           // Durchmesser der Schraube in Millimeter
    laenge,                // Länge des Dübels in Millimeter
    uebergroesse   = 2,    // Übergröße des Außendurchmessers
    aussen_vj      = 0.75, // Verjüngungsfaktor des Außendurchmessers
    schraube_vj    = 0.2,  // Verjüngungsfaktor des Innendurchmessers
    schraube_vjend = 0.3,  // Relatives Ende der Verjüngung entlang der Tiefe    
    kragen         = 0.1,  // Relative Länge des ungeschlitzten Teils
    schlitzstaerke = 0.5,  // Stärke der Schlitze in Millimeter
    abschluss      = 2,    // Stärke der Abschlusskappe in Millimenter
){

	difference() {

    	aussen_dm    = bohrloch_dm + uebergroesse;
	    seitenlaenge = sqrt( pow(aussen_dm, 2) / 2 );

	    /* ... */

        // Schlitze
        kragen_abs = laenge * kragen;
        
        for (i = [0:1])
        rotate( [0, 0, i * 90] )
        translate([
        	-(seitenlaenge + 2) / 2, 
        	-schlitzstaerke / 2, 
        	kragen_abs
        ]) 
        cube ([
        	seitenlaenge + 2, 
        	schlitzstaerke, 
        	laenge - abschluss - kragen_abs
        ]);

	}

}
/* ... */

Für die Modellierung der Schlitze haben wir drei weitere Parameter unserem Modell hinzugefügt. Der Parameter kragen definiert, wie lang der Abschnitt zu Beginn des Dübels sein soll, in dem es noch keine Schlitze gibt. Der Anteil wird relativ zur Gesamtlänge angegeben. Der Parameter schlitzstaerke bestimmt die Dicke der Schlitze in Millimeter. Der Parameter abschluss legt fest, wie viele Meter vor Ende des Dübels die Schlitze wieder enden sollen. Dies dient der Stabilisierung des Dübels, da anderenfalls die geschlitzten Seitenteile frühzeitig abbrechen könnten. Innerhalb der Modulbeschreibung werden die Schlitze über einen Quader (cube) modelliert. Dieser wird mittels einer Verschiebetransformation (translate) auf den Ursprung der X/Y-Ebene in Höhe der absoluten Kragengröße kragen_abs verschoben und anschließend um die Z-Achse rotiert (rotate). Die Rotation wird durch eine For-Schleife parametrisiert, die von 0 bis 1 läuft und somit 2 Instanzen des Quaders erzeugt: einmal um 0 Grad und einmal um 90 Grad gedreht. Zur Kontrolle können Sie die so entstehenden Quader temporär sichtbar machen, indem Sie der For-Schleife ein #-Zeichen voranstellen und eine Vorschau berechnen (Abbildung 4.).

Abbildung 4.: Hervorhebung einzelner Geometrieteile durch das temporäre Voranstellen eines #-Zeichens

Um sich in einem Bohrloch gut festkrallen zu können, wollen wir nun unserem Dübel noch ein paar Zähne verpassen:

/* ... */
module duebel (
    bohrloch_dm,           // Durchmesser des Bohrlochs in Millimeter
    schraube_dm,           // Durchmesser der Schraube in Millimeter
    laenge,                // Länge des Dübels in Millimeter
    uebergroesse   = 2,    // Übergröße des Außendurchmessers
    aussen_vj      = 0.75, // Verjüngungsfaktor des Außendurchmessers
    schraube_vj    = 0.2,  // Verjüngungsfaktor des Innendurchmessers
    schraube_vjend = 0.3,  // Relatives Ende der Verjüngung entlang der Tiefe    
    kragen         = 0.1,  // Relative Länge des ungeschlitzten Teils
    schlitzstaerke = 0.5,  // Stärke der Schlitze in Millimeter
    abschluss      = 2,    // Stärke der Abschlusskappe in Millimenter
    zahn_div       = 5,    // Divisor für die Anzahl der Zähne
    zahn_tiefe     = 1.5,  // Tiefe der Zähne in Millimeter
){

	difference() {

    	aussen_dm    = bohrloch_dm + uebergroesse;
	    seitenlaenge = sqrt( pow(aussen_dm, 2) / 2 );

	    /* ... */

        kragen_abs = laenge * kragen;

	    /* ... */

        // Zähne
        zahnanzahl  = floor( (laenge - kragen_abs) / zahn_div );
        zahnabstand = (laenge - kragen_abs) / (zahnanzahl + 1);
        
        gegenkathete = (aussen_dm - (aussen_dm * aussen_vj)) / 2;
        hypothenuse  = sqrt( pow(gegenkathete, 2) + pow(laenge, 2) );
        winkel = asin( gegenkathete / hypothenuse );
        
        diag_abst = sqrt( pow(aussen_dm/2 - zahn_tiefe, 2) / 2 );
                
        for ( j = [0:90:359])
        rotate( [0, 0, j] )
        rotate( -winkel, [1,1,0])
        for ( i = [1:zahnanzahl] )
        translate( [diag_abst, -diag_abst, kragen_abs + i * zahnabstand] )
        rotate([0,0,-45])
        translate( [0, -aussen_dm / 2, 0] )
        rotate([0,45,0])
        cube( aussen_dm );

	}

}
/* ... */

Für die Zähne definieren wir zwei weitere Parameter. Der Parameter zahn_div bestimmt die Anzahl der Zähne relativ zur Länge des Dübels. Der Parameter zahn_tiefe legt fest, wie tief die Zähne in den Grundkörper des Dübels eindringen sollen. Innerhalb der Modulbeschreibung generieren wir erst einmal aus dem Parameter zahn_div die absoluten Werte zahnanzahl und zahnabstand. Die bei der Zahnanzahl berücksichtigte Länge ist die Länge des Dübels abzüglich des Kragens. Die dort genutzte Funktion floor (dt. Fußboden) rundet den ihr übergebenen Wert ab, damit zahnanzahl ganzzahlig wird. Den Wert zahnabstand benötigen wir, um die Zähne gleichmäßig über den Dübel zu verteilen. Wir erhöhen hier die Zahnanzahl um 1, damit wir sowohl am Anfang als auch am Ende etwas Abstand zum Rand haben. Da unser Dübel-Grundkörper nach oben zuläuft, müssen wir noch den winkel der Seitenkante mit Hilfe des Arkussinus asin bestimmen. Und wir benötigen noch den Abstand diag_abst, der uns den Verschiebungswert in X- und Y-Richtung angibt, den man benötigt, um vom Ursprung an der äußeren Spitze der Dübelgrundfläche zu landen.

Nach diesen mathematischen Vorbereitungen können wir nun die Zahngeometrie beschreiben. Da unsere Geometriebeschreibung eine ganze Reihe von Schritten beinhaltet, ist es ratsam, sich jeden Schritt in der Vorschau anzuschauen, indem man an geeigneter Stelle ein !-Zeichen vor die Geometriebeschreibung setzt. Als Grundkörper wählen wir einen Würfel (cube) mit aussen_dm Kantenlänge. Setzen wir das !-Zeichen direkt vor cube und starten eine Vorschau (F5), dann wird nur dieser angezeigt. Im nächsten Schritt drehen wir den Würfel um 45 Grad entlang der Y-Achse (!rotate( [0, 45, 0] )). Dies erzeugt schon einmal die “Schneidfläche” entlang der Y-Achse, mit der wir später in den Grundkörper schneiden. Jetzt zentrieren wir den Würfel über der X-Achse indem wir ihn eine halbe Würfellänge entlang der Y-Achse verschieben (!translate( [0, -aussen_dm / 2, 0] )). Nun ist der Würfel in einer guten Position, um ihn um die senkrechte Z-Achse um -45 Grad zu drehen (!rotate([0,0,-45])). Jetzt steht der Würfel schon fast richtig. Wir müssen ihn nur noch auf die äußere Kante des Grundkörpers und etwas nach oben schieben (!translate( [diag_abst, -diag_abst, kragen_abs + i * zahnabstand] )). Bei dieser Verschiebung haben wir schon die Schleifenvariable i eingebaut. Wenn wir jetzt eine For-Schleife davor setzen, können wir uns einen Turm aus Würfeln bauen (!for ( i = [1:zahnanzahl] )).

Jetzt kommt der Punkt, wo wir uns um das Zulaufen unseres Dübel-Grundkörpers kümmern müssen. Unser Würfelturm soll ja der Seitenkante folgen, damit wir gleichmäßig unsere Zähne herausschneiden können. Hierzu kippen wir jetzt den Würfelturm um winkel. Da wir aber nicht um eine der Koordinatenachsen drehen wollen, sondern um eine Achse, die genau zwischen X- und Y-Achse liegt, benutzen wir an dieser Stelle eine besondere Form der Rotationstransformation rotate. Diese besondere Form bekommt als ersten Parameter den Winkel, um den man drehen möchte, und als zweiten Parameter die Achse, um die man drehen möchte (!rotate( -winkel, [1,1,0])).

Wir haben es fast geschafft! Jetzt müssen wir unseren gekippten Würfelturm nur noch dreimal kopieren und dabei jeweils um 90 Grad drehen. Dies erledigen wir mit einer zweiten For-Schleife und einer entsprechenden Rotationstransformation (!for ( j = [0:90:359]) rotate( [0, 0, j] )). Die For-Schleife endet bei 359 und nicht etwa bei 360, da wir ansonsten Würfeltürme bei 0 Grad und bei 360 Grad hätten, die übereinander lägen.

Als finalen “Touch” wollen wir unserem Spezialdübel noch die Option geben, anstelle eines viereckigen Grundkörpers auch einen runden Grundkörper zu haben:

module duebel (
    bohrloch_dm,           // Durchmesser des Bohrlochs in Millimeter
    schraube_dm,           // Durchmesser der Schraube in Millimeter
    laenge,                // Länge des Dübels in Millimeter
    uebergroesse   = 2,    // Übergröße des Außendurchmessers
    aussen_vj      = 0.75, // Verjüngungsfaktor des Außendurchmessers
    schraube_vj    = 0.2,  // Verjüngungsfaktor des Innendurchmessers
    schraube_vjend = 0.3,  // Relatives Ende der Verjüngung entlang der Tiefe    
    kragen         = 0.1,  // Relative Länge des ungeschlitzten Teils
    schlitzstaerke = 0.5,  // Stärke der Schlitze in Millimeter
    abschluss      = 2,    // Stärke der Abschlusskappe in Millimenter
    zahn_div       = 5,    // Divisor für die Anzahl der Zähne
    zahn_tiefe     = 1.5,  // Tiefe der Zähne in Millimeter
    rund           = false // Erstelle runden anstatt eckigen Dübel
){

	difference() {

        aussen_dm = bohrloch_dm + uebergroesse;
	    seitenlaenge = rund ? aussen_dm : sqrt( pow(aussen_dm, 2) / 2 );

	    linear_extrude(height = laenge, scale = aussen_vj)
	    if (rund)
	    	circle(d = seitenlaenge, $fn=36);
	    else
	    	square( seitenlaenge, center = true );

	    // Loch
	    svj_ende = laenge * schraube_vjend;
	    vj_dm    = schraube_dm * schraube_vj;

	    translate([0,0,-0.01])
	    union() {
	        $fn = 24;
            
	        cylinder( d1 = schraube_dm, d2 = vj_dm, h = svj_ende + 0.01);
	        
	        translate( [0, 0, svj_ende] )
	        cylinder( d = vj_dm, h = laenge - svj_ende + 0.02 );
	    }


        // Schlitze
        kragen_abs = laenge * kragen;
        
        for (i = [0:1])
        rotate( [0, 0, i * 90] )
        translate([
        	-(seitenlaenge + 2) / 2, 
        	-schlitzstaerke / 2, 
        	kragen_abs
        ]) 
        cube ([
        	seitenlaenge + 2, 
        	schlitzstaerke, 
        	laenge - abschluss - kragen_abs
        ]);
        
        // Zähne
        zahnanzahl  = floor( (laenge - kragen_abs) / zahn_div );
        zahnabstand = (laenge - kragen_abs) / (zahnanzahl + 1);
        
        gegenkathete = (aussen_dm - (aussen_dm * aussen_vj)) / 2;
        hypothenuse  = sqrt( pow(gegenkathete, 2) + pow(laenge, 2) );
        winkel = asin( gegenkathete / hypothenuse );
        
        diag_abst = sqrt( pow(aussen_dm/2 - zahn_tiefe, 2) / 2 );
                
        for ( j = [0:90:359] )
        rotate( [0, 0, j] )
        rotate( -winkel, [1,1,0] )
        for (i = [1:zahnanzahl] )
        translate( [diag_abst, -diag_abst, kragen_abs + i * zahnabstand] )
        rotate([0,0,-45])
        translate( [0, -aussen_dm / 2, 0] )
        rotate([0,45,0])
        cube( aussen_dm );        
	    
	}

}

duebel(8,5,50);

Die Option den Dübel rund oder eckig zu gestalten wird über den neu hinzugefügten Parameter rund gesteuert. Innerhalb der Geometriebeschreibung haben wir zwei Stellen mit einer Fallunterscheidung erweitert. Zum einen wird die Seitenlänge in Abhängigkeit des Parameters rund gesetzt. Zum anderen wird mittels einer If-Verzweigung zwischen runder (circle) und eckiger (square) Grundform unterschieden. Man beachte, das hier die If-Verzweigung selbst wieder wie eine Geometrie behandelt wird, die von linear_extrude extrudiert wird.

Unser fertiges Dübel-Modul hat eine ganze Menge an Parametern erhalten! Dadurch, dass wir den großen Teil der Parameter jedoch als relative Parameter gestaltet haben und den Parametern sinnvolle Standardwerte gegeben haben, bleibt das Modul angenehm benutzbar. Im Regelfall müssen nur die drei Parameter bohrloch_dm, schraube_dm und laenge angegeben werden. Alles andere passt sich automatisch den Gegebenheiten an.

Tipps für den 3D-Druck

Nachdem Sie die Geometrie berechnet (F6) und anschließend als .stl-Datei exportiert (F7) haben, können Sie die .stl-Datei in die Slicer-Software ihres 3D-Druckers einladen. Hier stellt sich die Frage, ob Sie den Dübel stehend oder liegend drucken wollen. Liegend dürfte der Dübel etwas stabiler werden und weniger Druckzeit benötigen, dafür jedoch eine Stützstruktur benötigen, die Sie nach dem Druck entfernen müssen. Wenn Sie den Dübel stehend drucken, fällt die Notwendigkeit einer Stützstruktur weg. Hier sollten Sie ggf. die Druckgeschwindigkeit etwas verringern, damit die einzelnen Schichten genug Zeit haben, ausreichend abzukühlen, bevor die nächste Schicht aufgebracht wird. Darüber hinaus könnte es von Vorteil sein, mit einem sogenannten Brim zu drucken, damit sich die Standfläche des Dübels auf dem Druckbett vergrößert und somit die Druckbetthaftung verbessert wird.

Download der OpenSCAD-Datei dieses Projektes

Projekt 3: Fensterstopper

Dieses Projekt hat die Konstruktion eines Fensterstoppers mit drehbarem Keil zum Ziel. Dreht man am Hebel des Stoppers, so verklemmt der Keil das Fenster mit der Fensterbank.

Abbildung 5.: Fensterstopper mit drehbarem Keil

Was ist neu?

Wir lernen eine neue Variante der For-Schleife (generative for) kennen, mit der wir formelbasiert Felder erzeugen können. In diesem Zusammenhang werden wir auch die Bedeutung des Schlüsselwortes let kennenlernen und sehen, wie wir die erzeugten Daten mit der 2D-Grundform polygon in eine Geometrie überführen können. Zu guter Letzt werden wir auch erfahren, was es mit der Hüllentransformation (hull) auf sich hat.

Los geht’s

Lassen Sie uns, wie inzwischen gewohnt, mit der Definition eines Moduls fensterstopper beginnen. Da wir auch bei diesem Projekt eine Menge Parameter erwarten, werden wir erneut ein senkrechtes Arrangement der Parameter wählen. Darüber hinaus wird unsere Geometriebeschreibung aus mehreren Teilen bestehen, die wir schonmal als Untermodule anlegen:

// Ein Fensterstopper mit drehbarem Keil

module fensterstopper(

	/* parameter */

){

    $fn = 36;

    module fenster() {
    }

    module halter() {
    }

    module stopper() {
    }
    
    fenster();    
    halter();    
    stopper();

}

fensterstopper(

	/* parameter */

);

Auch wenn wir jedes Untermodul nur einmal nutzen werden, wird uns die Unterteilung in mehrere Untermodule helfen, wenn wir später die einzelnen Geometrieteile z.B. für einen 3D-Druck generieren wollen. Das Modul fenster wird ein reines Hilfsmodul, in dem wir die Unterkante des Fensters als Orientierungshilfe modellieren werden. Im Modul halter werden wir das Gehäuse des Fensterstoppers beschreiben und im Modul stopper werden wir den drehbaren Keil sowie seinen Hebel definieren. Neben den Untermodulen und ihren drei Instanzen haben wir auch die Spezialvariable $fn auf Modulebene definiert. Auf diese Weise können wir die Feinheit der runden Geometrien unserer Geometriebeschreibung an einer zentralen Stelle einstellen.

Beginnen wir unsere Geometriebeschreibung mit dem Hilfsmodul fenster:

/* ... */
module fensterstopper(
    fenstertiefe,         // Stärke des Fensters (inkl. Spiel) in Millimeter
    fensterbank_abst,     // Abstand zwischen Fensterbank und Unterkante Fenster in Millimeter
){

    $fn = 36;

    module fenster() {
        color("LightSkyBlue")
        translate( [-20, -fenstertiefe / 2, fensterbank_abst] )
        cube( [100, fenstertiefe, 0.1] );
    }

	/* ... */

}
/* ... */

Wir haben die Parameter fenstertiefe und fensterbank_abst unserem Modul hinzugefügt. Sie beschreiben die Dicke des Fensters und den Abstand der Unterkante des Fensters zur Fensterbank. Im Untermodul fenster beschreiben wir nur diese Unterkante des Fensters mit einem 0.1 Millimeter dünnen Quader (cube). Die Breite des Quaders setzen wir hier pauschal auf 10cm, da dieses Maß für die Konstruktion des Stoppers nicht relevant ist. Mittels der Verschiebetransformation (translate) bringen wir den Quader auf die Höhe fensterbank_abst und platzieren ihn in der Tiefe mittig über der X-Achse (-fenstertiefe / 2). Um unser Hilfsobjekt optisch vom Rest der Geometrie abzusetzen, geben wir ihm auch noch einen schönen Blauton. Da wir das Untermodul nur einmal verwenden und nicht parametrisieren müssen, ist die Parameterliste leer. Auf die Parameter des übergeordneten Moduls fensterstopper können wir direkt zugreifen und müssen diese nicht mittels lokaler Parameter “weiterreichen”. Dies gilt übrigens auch für Variablen, die im übergeordneten Modul definiert wurden. Dies werden wir uns im nächsten Schritt zunutze machen.

Die Geometriebeschreibung des Halters ist etwas aufwendiger. Im Kern besteht der Halter jedoch nur aus einem einfachen Quader (cube), von dem wir eine Reihe passend gedrehter und verschobener Grundformen (cube und cylinder) mittels einer Booleschen Differenzoperation (difference) abziehen:

/* ... */
module fensterstopper(
    fenstertiefe,               // Stärke des Fensters (inkl. Spiel) in Millimeter
    fensterbank_abst,           // Abstand zwischen Fensterbank und Unterkante Fenster
    							// in Millimeter
    fensterbank_anp      = 0,   // Einfache Anpassung des Fensterbankabstands in Millimeter
    halterbreite         = 30,  // Breite des Halters in Millimeter
    materialstaerke      = 5,   // Materialstärke des Halters in Millimeter
    fenster_ueberlappung = 7,   // Überlappung von Halter und Fenster in Millimeter
    achse_dm             = 10,  // Durchmesser der Drehachse in Millimeter
    achse_spiel          = 0.5, // Spiel der Drehachse in Millimeter
){

    $fn = 36;
    achshoehe = materialstaerke + (fensterbank_abst - materialstaerke) / 2;

	/* ... */

    module halter() {
    	halter_gr = [
    		halterbreite,                              // Breite
    		fenstertiefe + 2 * materialstaerke,        // Tiefe
    		fensterbank_abst + fenster_ueberlappung    // Höhe
    	];
        
        translate( [0, -halter_gr.y / 2, 0] ) 
        difference() {
            cube( halter_gr );
            
            translate( [-1, materialstaerke, materialstaerke] )
            cube( [halter_gr.x + 2, fenstertiefe, halter_gr.z] );
            
            achse_erw = achse_dm + achse_spiel;
            
            translate( [halter_gr.x / 2, -1, achshoehe] )
            rotate( [-90, 0, 0] )
            cylinder( d = achse_erw, h = halter_gr.y + 2);
            
            translate( [(halter_gr.x - achse_erw) / 2, -1, achshoehe] )
            cube( [achse_erw, halter_gr.y + 2, halter_gr.z] );
        }
        
        translate( [0, -halter_gr.y / 2, -fensterbank_anp] )
        cube( [halter_gr.x, halter_gr.y, fensterbank_anp] );
    }

	/* ... */

}
/* ... */

Unsere Parameterliste hat reichlich Zuwachs bekommen. Der Parameter fensterbank_anp ermöglicht eine spätere Fein- bzw. Nachjustierung des Fensterstoppers. Die Parameter halterbreite und materialstaerke definieren die Breite des Halters und die Materialstärke der Seitenflächen und des Bodens des Halters. Mit dem Parameter fenster_ueberlappung legt man fest, wie weit der Halter seitlich mit dem Fenster überlappen soll. Die Parameter achse_dm und achse_spiel bestimmen den Durchmesser der Achse des drehbaren Keils sowie das Spaltmaß zwischen der Achse und ihrem Lager im Halter.

Innerhalb der Modulbeschreibung des Moduls fensterstopper definieren wir zunächst die Variable achshoehe, da wir diese nicht nur im Untermodul halter sondern auch später im Untermodul stopper benötigen werden. Wir positionieren die Achse auf halbem Weg zwischen Fensterbank und Unterkante des Fensters unter Berücksichtigung der Materialstärke des Halterbodens. Im Untermodul halter definieren wir eine weitere Variable halter_gr, die die Dimension unseres Halters in Abhängigkeit von Fenstermaßen, Materialstärke und Überlappung als dreidimensionalen Vektor beschreibt.

Die Geometrie des Halters beschreiben wir über eine Boolesche Differenzoperation (difference). Den Grundkörper, von dem wir die anderen Geometrien abziehen, bildet hier ein einfacher Quader, der die Außenmaße des Halters hat (cube( halter_gr );). Im Anschluss ziehen wir einen Quader ab, der eine zum Fenster korrespondierende Tiefe hat und etwas breiter ist, als unser Grundkörper (halter_gr.x + 2). Wir positionieren den Quader mittels Verschiebetransformation genau um die Materialstärke nach oben sowie um die Materialstärke nach innen. Hierdurch wird aus dem Grundkörper ein U-Profil, dass später entlang der Fensterkante verschoben werden kann. Beachten Sie auch die kleine Verschiebung in X-Richtung (-1), um eine saubere Differenzoperation zu gewährleisten.

Als nächstes müssen wir noch das Achslager von unserem Grundkörper abziehen. Da das Lager im Durchmesser etwas größer werden soll als die Achse selbst, definieren wir die Variable achse_erw als Summe von Achsdurchmesser und Achsspiel. Das Achslager besteht aus einem Zylinder (cylinder) und einem Quader (cube). Der Zylinder erhält den Durchmesser achse_erw und eine Höhe, die der Tiefe des Halters entspricht plus einer kleinen Zugabe. Da die Grundform cylinder senkrecht auf der X-Y-Ebene steht, müssen wir den Zylinder zunächst um die X-Achse auf die Seite drehen (rotate). Anschließend können wir den Zylinder mittels Verschiebetransformation (translate) auf die Mitte der Halterbreite (halter_gr.x / 2) und die eingangs berechnete achshoehe bewegen. Auch hier führen wir passend zur Zugabe in der Zylinderhöhe eine kleine Bewegung (-1) in Y-Richtung aus, um eine saubere Differenzoperation zu gewährleisten. Der zweite Teil des Achslagers besteht aus einem Quader, der genauso breit wie der Zylinder ist (achse_erw) und oberhalb der Achse aus der Haltergrundform geschnitten wird. Da die Grundform cube in diesem Fall ohne den Parameter center = true erstellt wurde, müssen wir bei der Verschiebung des Quaders in die Mitte des Halters seine eigene Breite berücksichtigen ((halter_gr.x - achse_erw) / 2). Davon abgesehen erfolgt die Positionierung des Quaders analog zu der des Zylinders zuvor. Auch hier achten wir wieder darauf, das die Differenzoperation sauber ablaufen kann (Zugabe in der Tiefe und korrespondierende Verschiebung).

Nachdem wir nun unseren kompletten Halter mit einer Booleschen Differenzoperation beschrieben haben, zentrieren wir den Halter noch über der X-Achse, indem wir der Differenzoperation (difference) eine entsprechende Verschiebung (translate) voranstellen.

Um den Abstand zwischen Fensterbank und Fenster auf eine zusätzliche, einfache Weise nachjustieren zu können, erstellen wir neben der Differenzoperation noch einen Sockel, der unterhalb des Halters bei Bedarf (Parameter fensterbank_anp) anwachsen kann. Dieser Sockel besteht aus einem einfachen Quader (cube), der die Grundfläche des Halters hat und fensterbank_anp hoch ist. Damit er “nach unten” anwächst, wird er mit einer Verschiebetransformation (translate) entsprechend um -fensterbank_anp entlang er Z-Achse verschoben. Mit der gleichen Transformation erledigen wir die Zentrierung des Sockels über der X-Achse (-halter_gr.y / 2);

Damit haben wir das Untermodul halter fertig und können uns nun endlich dem Untermodul stopper zuwenden. Da das Modul einige neue OpenSCAD-Funktionen verwendet, wollen wir hier schrittweise und etwas langsamer vorgehen:

/* ... */
module fensterstopper(
    fenstertiefe,               // Stärke des Fensters (inkl. Spiel) in Millimeter
    fensterbank_abst,           // Abstand zwischen Fensterbank und Unterkante Fenster
    							// in Millimeter
    fensterbank_anp      = 0,   // Einfache Anpassung des Fensterbankabstands in Millimeter
    halterbreite         = 30,  // Breite des Halters in Millimeter
    materialstaerke      = 5,   // Materialstärke des Halters in Millimeter
    fenster_ueberlappung = 7,   // Überlappung von Halter und Fenster in Millimeter
    achse_dm             = 10,  // Durchmesser der Drehachse in Millimeter
    achse_spiel          = 0.5, // Spiel der Drehachse in Millimeter
    keil_start_r         = 0,   // Startradius des Keil in Millimeter
    keil_end_r           = 11,  // Endradius des Keil in Millimeter
    keil_winkel          = 42,  // Startwinkel des Keil in Grad
    hebel_tiefe          = 15,  // Tiefe des Hebels in Millimeter
    hebel_laenge         = 40,  // Länge des Hebels in Millimeter
    hebel_winkel         = 15,  // Startwinkel des Hebels in Grad
){

    $fn = 36;
    achshoehe = materialstaerke + (fensterbank_abst - materialstaerke) / 2;

	/* ... */

    module stopper( winkel = 0 ) {
        achse_laenge = fenstertiefe + 2 * materialstaerke + 2;
        
        translate( [halterbreite / 2, -achse_laenge / 2, achshoehe] )
        rotate( [-90, 0, 0] )
        cylinder( d = achse_dm, h = achse_laenge, $fn = 36 );
        
		/* ... */
    }

	/* ... */

}
/* ... */

Für den Stopper haben wir einen weiteren Satz an Parametern definiert. Zum einen sind dies die drei Parameter keil_start_r, keil_end_r und keil_winkel, die wir später für die Beschreibung des Drehkeils benötigen. Zum anderen haben wir drei Parameter, die Eigenschaften des Stopperhebels definieren (hebel_tiefe, hebel_laenge, hebel_winkel). Wie schon im Modul halter werden wir auch in diesem Untermodul die Variable achshoehe nutzen. Darüber hinaus haben wir innerhalb des Untermoduls stopper die Variable achse_laenge definiert. Diese entspricht der Breite des Halters plus zwei Millimeter. Hierdurch wird die Achse jeweils einen Millimeter (vorne und hinten) aus dem Halter hervorstehen. Die Achse selbst wird über die Grundform cylinder modelliert und mittels Rotations- und Verschiebetransformation an die richtige Stelle bewegt. Das Untermodul stopper besitzt überdies einen Parameter winkel, den wir im weiteren Verlauf zum Testen der Funktion des Drehkeils verwenden werden.

Als nächstes werden wir den Drehkeil modellieren, der Teil der Achse wird. Für diesen Zweck werden wir zunächst mit einer speziellen Variante der For-Schleife ein Feld mit 2D-Punkten erzeugen, die den Drehkeil im Zweidimensionalen beschreiben. Anschließend übergeben wir diese Punktmenge an die 2D-Grundform Polygon (engl. ebenfalls polygon), die aus der Punktmenge eine nutzbaren 2D-Geometrie erzeugt. Diese können wir dann extrudieren und mit den gewohnten Rotations- und Verschiebetransformationen an den gewünschten Ort bewegen:

/* ... */
module fensterstopper(
	/* ... */
){

    $fn = 36;
    achshoehe = materialstaerke + (fensterbank_abst - materialstaerke) / 2;

	/* ... */

    module stopper( winkel = 0 ) {
        achse_laenge = fenstertiefe + 2 * materialstaerke + 2;
        
        translate( [halterbreite / 2, -achse_laenge / 2, achshoehe] )
        rotate( [-90, 0, 0] )
        cylinder( d = achse_dm, h = achse_laenge, $fn = 36 );
        
        points = [
            for (i = [0:5:360]) 
            let (radius = keil_start_r + (keil_end_r - keil_start_r) * i / 360 ) 
            [ cos( i ) * radius, sin( i ) * radius ] 
        ];

        keilbreite = fenstertiefe - 2;
        
        translate( [halterbreite / 2, -keilbreite / 2, achshoehe] )
        rotate( [-90, 0, 0] ) 
        rotate( [0, 0, keil_winkel + winkel] )
        linear_extrude( height = keilbreite )  
        polygon(points);
            
		/* ... */
    }

	/* ... */

}
/* ... */

Das zentrale Element in der obigen Geometriebeschreibung ist die Variable points, der wir ein Feld zuweisen (points = [ ... ];). Ziel ist es, dieses Feld der 2D-Grundform polygon als Parameter zu übergeben. Die 2D-Grundform polygon erwartet ein Feld von 2D-Punkten bzw. zweidimensionalen Vektoren:

	points = [ [x0,y0], [x1,y1], ..., [xN,yN] ];

	polygon( points );

Es ist leicht, bei all den eckigen Klammern etwas die Übersicht zu verlieren. Das “äußere” Feld besteht aus der eckigen Klammer ganz am Anfang und der ganz am Ende. Im Inneren befinden sich dann, durch Kommata getrennt, die einzelnen zweidimensionalen Vektoren mit ihren x- und y-Werten. Für einfache 2D-Formen könnte man also durchaus so eine Punktmenge per Hand definieren oder sie von einem externen Programm erzeugen lassen.

Für unseren Drehkeil gehen wir einen anderen Weg und nutzen eine generative For-Schleife (engl. generative for):

    points = [
        for (i = [0:5:360]) 
        let (radius = keil_start_r + (keil_end_r - keil_start_r) * i / 360 ) 
        [ cos( i ) * radius, sin( i ) * radius ] 
    ];

Die erste Zeile des Ausdrucks gleicht dem einer “normalen” For-Schleife. Wir definieren die Schleifenvariable i und weisen ihr eine Spanne zu, die von 0 in Schritten von 5 bis 360 läuft (for (i = [0:5:360])). Die nächste Zeile enthält etwas Neues. Der OpenSCAD Ausdruck let( ... ) erlaubt es, ein oder mehrere Variablen zu definieren, die in jedem Schleifendurchlauf neu gesetzt werden. Genau so, wie es auch mit der Schleifenvariable selbst geschieht. In unserem Fall definieren wir die Variable radius und geben ihr einen Wert, der sich im Laufe der Schleife stetig vergrößert. Auf diese Weise hat radius zu Beginn der Schleife den Wert keil_start_r und zum Ende der Schleife den Wert keil_end_r. In Worten lässt sich die Formel in etwa so beschreiben: wir starten mit keil_start_r und wollen am Ende bei keil_end_r landen. Der Unterschied bzw. der Abstand zwischen keil_end_r und keil_start_r ist (keil_end_r - keil_start_r). Um sich schrittweise dem Wert keil_end_r zu nähern, brauchen wir in jedem Schritt nur einen Bruchteil des Abstands (i / 360). Wenn i bei 360 angekommen ist, ist unser Bruchteil 360 / 360, also 1. Vorher ist i kleiner als 360 und ergibt damit einen Wert kleiner 1 für die jeweiligen Zwischenschritte.

Wir haben jetzt also eine Variable radius, die zunehmend größer wird. Wo kommen jetzt unsere Punkte her? Die finden wir in der dritten Zeile unserer generativen For-Schleife ([ cos( i ) * radius, sin( i ) * radius ]). Die dritte Zeile beschreibt die prototypische Form eines einzelnen Feldes. In unserem Fall ist dies erstmal ein zweidimensionaler Vektor ([ .. , .. ]). Der x-Wert des Vektors ist cos( i ) * radius und der y-Wert ist sin( i ) * radius. Hier verwenden wir also sowohl die Schleifenvariable i als auch die mit let definierte Variable radius. Für jeden Schritt der For-Schleife wird nun diese prototypische Form genommen, mit den jeweils aktuellen Werten ausgefüllt und dem Feld hinzugefügt. Wenn Sie sich das erzeugte Feld einmal anschauen wollen, dann können Sie (z.B.) unterhalb der Definition von points den OpenSCAD-Befehl echo( points ); einfügen und eine Vorschau (F5) berechnen lassen. Dann wird der Inhalt von points im Konsolenbereich von OpenSCAD ausgegeben.

Abbildung 5.: Die 2D-Grundform polygon ermöglicht die Erstellung formelbasierter Geometrien

Übergibt man nun die Variable points als Parameter an die 2D-Grundform polygon, definiert die übergebene Punktmenge eine zweidimensionale Geometrie (Abbildung 5.). Diese Geometrie kann man dann wie jede andere zweidimensionale Grundform nutzen. Um unseren Drehkeil zu beschreiben, extrudieren wir die Form mittels linear_extrude auf die Länge keilbreite, die wir zuvor in Relation zur fenstertiefe definiert haben. Im Anschluss führen wir zunächst eine Rotation um die Z-Achse durch (rotate( [0, 0, keil_winkel + winkel] )). Diese Rotation erfüllt zwei Zwecke. Wir müssen zum einen sicherstellen, dass unser Keil nicht mit dem Halter kollidiert. Hierfür müssen wir einen geeigneten Wert für keil_winkel finden (Abbildung 5.). Ist einmal ein passender Wert gefunden, brauchen wir diesen nicht mehr zu verändern. Zum anderen nutzen wir die Rotation über den Parameter winkel zum späteren Testen der Funktionsfähigkeit unseres Drehkeils.

Abbildung 5.: Die orthogonale Ansicht (rechter markierter Button) kann bei der Feineinstellung hilfreich sein, da es in dieser Ansicht keine perspektivische Verzerrung gibt. Hier eine Ansicht von rechts (linker markierter Button) auf den Drehkeil.

Anschließend führen wir eine zweite Rotation um die X-Achse aus, um den Drehkeil in die Waagrechte zu bekommen und ihn anschließend mittels einer Verschiebetransformation an die passende Stelle zu bewegen.

Nun können wir uns der Modellierung des Hebels als letzten Teil des Untermoduls stopper widmen. Hierbei werden wir die Hüllentransformation (engl. hull) kennenlernen:

/* ... */
module fensterstopper(
	/* ... */
){

    $fn = 36;
    achshoehe = materialstaerke + (fensterbank_abst - materialstaerke) / 2;

	/* ... */

    module stopper( winkel = 0 ) {
        achse_laenge = fenstertiefe + 2 * materialstaerke + 2;
        
        translate( [halterbreite / 2, -achse_laenge / 2, achshoehe] )
        rotate( [-90, 0, 0] )
        cylinder( d = achse_dm, h = achse_laenge, $fn = 36 );
        
        points = [
            for (i = [0:5:360]) 
            let (radius = keil_start_r + (keil_end_r - keil_start_r) * i / 360 ) 
            [ cos( i ) * radius, sin( i ) * radius ] 
        ];

        keilbreite = fenstertiefe - 2;
        
        translate( [halterbreite / 2, -keilbreite / 2, achshoehe] )
        rotate( [-90, 0, 0] ) 
        rotate( [0, 0, keil_winkel + winkel] )
        linear_extrude( height = keilbreite )  
        polygon(points);
            
        translate( [halterbreite / 2, -achse_laenge / 2 + 0.1, achshoehe] )
        rotate( [0, hebel_winkel + winkel, 0] )
        rotate( [90, 0, 0] )
        linear_extrude( height = hebel_tiefe )
        hull() {
            circle( d = achse_dm );
            translate( [hebel_laenge - achse_dm / 2 - achse_dm / 3, 0, 0] )
            circle( d = achse_dm * 0.66 );
        }        
    }

	/* ... */

}
/* ... */

Die Hüllentransformation (hull) wirkt ähnlich wie die Booleschen Operationen auf die jeweils nachfolgende Geometriemenge ({ ... }). Die Transformation erzeugt die gemeinsame konvexe Hülle der in dieser Menge enthaltenden Geometrien (Abbildung 5.). Die hull-Transformation kann sowohl auf 2D als auch auf 3D-Geometrien angewandt werden.

Abbildung 5.: Die Hüllentransformation hull erzeugt die konvexe Hülle (rechts) der ihr übergebenen Geometriemenge (links)

Für die Beschreibung des Hebels verwenden wir die 2D-Variante und bilden die konvexe Hülle über zwei Kreise (engl. circle). Die so entstehende Hebel-Grundform extrudieren wir mittels linear_extrude und drehen das Ganze um die X-Achse (rotate( [90, 0, 0] )). Stellt man der Rotationstransformation ein !-Zeichen voran und berechnet eine Vorschau (F5), so sieht man, dass die Y-Achse genau durch das Drehzentrum des Hebels verläuft. Wir können nun mit einer zweiten Rotationstransformation (rotate( [0, hebel_winkel + winkel, 0] )) den Hebel in seine Ausgangslage drehen, die durch den Parameter hebel_winkel bestimmt wird. Wie auch beim Drehkeil drehen wir zusätzlich um den Testparameter winkel. Auf diese Weise werden sich Drehkeil und Hebel gemeinsam drehen, wenn wir die Funktion des Stoppers über den Parameter winkel testen. Wir schließen die Geometriebeschreibung des Hebels ab, indem wir ihn mittels einer Verschiebetransformation (translate) an die passende Position bewegen. Man beachte die leichte Zugabe von 0.1 entlang der Y-Achse, um sicherzustellen, dass Hebel und Achse miteinander verbunden sind.

Wir haben es geschafft. Die Geometriebeschreibung für unseren Fensterstopper ist vollständig! An dieser Stelle sollten wir einmal testen, ob der Drehkeil sich auch tatsächlich unter das Fenster pressen wird. Dies können wir überprüfen, wenn wir Hebel und Drehkeil mit unserem Testparameter winkel des Stopper-Moduls einmal um 100 Grad gegen den Uhrzeigersinn drehen:

/* ... */
module fensterstopper(
	/* ... */
){

	/* ... */

    fenster();    
    halter();    
    stopper( -100 );

}
/* ... */

Wenn alles richtig gelaufen ist, sollte nun nach der Berechnung einer Vorschau (F5) der Drehkeil durch den blauen Quader der Fensterunterkante hindurchdringen.

Tipps für den 3D-Druck

Um unseren Fensterstopper zu drucken, müssen wir die Untermodule halter und stopper getrennt voneinander berechnen lassen. Der einfachste Weg hierfür ist das zeitweise Voranstellen eines !-Zeichens vor der jeweiligen Modulinstanz. Löst man dann eine Geometrieberechnung aus (F6), kann man anschließend die so erzeugte Einzelgeometrie als .stl-Datei exportieren (F7).

Alternativ kann man das Modul fensterstopper noch um einen Parameter print_version erweitern und mittels Fallunterscheidung eine Positionierung der Module halter und stopper erzeugen, die den gleichzeitigen Export erlaubt:

/* ... */
module fensterstopper(
	/* ... */
    print_version        = false  
){

	/* ... */

    if (print_version) {

        halter();
        
        translate([
            halterbreite, 
            0, 
            (fenstertiefe + 2 * materialstaerke + 2) / 2 + hebel_tiefe - 0.1
        ])
        rotate( [90, 0, 0] )
        stopper();

    } else {

        fenster();    
        halter();    
        stopper();

    }

}
/* ... */

In diesem Fall rotieren wir das Modul stopper in eine geeignete Druckausrichtung und verschieben es so, dass die Fußpunkte von halter und stopper in Z-Richtung gleich sind und sich insgesamt die Geometrien nicht überschneiden (Abbildung 5.).

Abbildung 5.: Druckversion der Fensterstoppergeometrie

Download der OpenSCAD-Datei dieses Projektes

Projekt 4: Uhrwerk

Bei der Konstruktion technischer Bauteile kommt es regelmäßig vor, dass bereits existierende Komponenten integriert werden müssen. Im Kontext des 3D-Drucks sind dies häufig z.B. Motoren, Kugellager, Schrauben oder Muttern. In solchen Fällen ist es hilfreich, diese Komponenten auszumessen, nachzumodellieren und in der Geometriebeschreibung als Platzhalter zu verwenden. So kann man bereits im Computermodell sehen, ob alle Komponenten auch wirklich passen und am richtigen Ort sind. Wenn man bestimmte Bauteile häufiger verwendet, lohnt es sich, diese in einer OpenSCAD-Bibliothek abzulegen und dann mittels include bzw. use in das jeweilige Projekt einzubinden.

In diesem Projekt wollen wir uns genauer anschauen, was alles zu beachten ist, wenn wir eine nützliche OpenSCAD-Bibliothek erzeugen wollen. Als Übungsobjekt werden wir ein Standard-Uhrwerk nachmodellieren und die Geometriebeschreibung so gestalten, dass sie komfortabel als Teil einer OpenSCAD-Bibliothek genutzt werden kann.

Abbildung 6.: Ein nachmodelliertes Uhrwerk als Platzhalter- und Kontrollobjekt

Was ist neu?

Wir werden die 2D-Grundform import (dt. importieren) sowie die 3D-Grundform surface (dt. Oberfläche) kennenlernen. Als neue Transformationen werden wir mirror (dt. spiegeln) und resize (dt. Größe anpassen) nutzen. Darüber hinaus werden wir die Funktion search verwenden und etwas darüber erfahren, wie man mit OpenSCAD einfache Animationen erstellen kann.

Los geht’s

Wir beginnen mit der Definition eines Moduls und beschreiben schonmal den quaderförmigen Grundkörper des Uhrwerks:

module uhrwerk() {

    breite = 56;
    tiefe  = 56;
    hoehe  = 20;

    // Grundkörper
    color("Gray")
    translate( [-breite / 2, -tiefe / 2, -hoehe] )
    cube( [breite, tiefe, hoehe] );

}

uhrwerk(); // Testinstanz

Da wir ein gegebenes, physisches Objekt nachmodellieren, können wir die gemessenen Größen als fest annehmen und brauchen sie daher nicht zu Parametern des Moduls zu machen. Anstelle dessen definieren wir sie als Variablen innerhalb des Moduls. Auch wenn unsere Geometriebeschreibung bislang wenig umfangreich ist, haben wir bereits eine wichtige Designentscheidung getroffen. Wir haben uns dazu entschlossen, den Grundkörper der Uhr so am Ursprung des Koordinatensystems auszurichten, dass die Oberfläche des Grundkörpers plan mit der X-Y-Ebene ist (Verschiebung entlang der Z-Achse um -hoehe) und dass später die Achse der Uhr mit der Z-Achse zusammenfällt (Verschiebung in X- und Y-Richtung um -breite / 2 und -tiefe / 2). Diese Ausrichtung wird nicht nur die folgenden Modellierungsschritte vereinfachen, sondern auch die Verwendung des Moduls uhrwerk als Teil einer OpenSCAD-Bibliothek.

Um die Achse des Uhrwerks ragt ein Ausrichtungsmuster aus dem Grundkörper hervor, dass aus einem abgeflachten, eingekerbten Zylinder besteht. Dieses Ausrichtungsmuster dient der Arretierung und Ausrichtung des Uhrwerks, wenn es in einer Uhr verbaut wird. Es ist also ein wichtiges Detail, das wir in unserer Geometriebeschreibung berücksichtigen sollten:

module uhrwerk() {

    /* ... */

    ausrichtung_dm       = 14; // Durchmesser des Ausrichtungsmusters
    ausrichtung_h        = 1;  // Höhe des Ausrichtungsmusters
    ausrichtung_abfl     = 1;  // Rücksprung der Abflachungen
    ausrichtung_kerbe_dm = 6;  // Durchmesser der Kerben
    ausrichtung_kerbe_tf = 2;  // Rücksprung der Kerben

    /* ... */

    // Ausrichtungsmuster
    difference(){
        color("DimGray")
        cylinder( d = ausrichtung_dm, h = ausrichtung_h, $fn = 36);
        
        // Abflachung und Kerbe
        color("Gray")
        for ( i = [0:1])
        mirror( [i, 0, 0] )
        union(){
            translate([
                ausrichtung_dm / 2 - ausrichtung_abfl,
                -(ausrichtung_dm + 2) / 2,
                -1
            ])
            cube([
                ausrichtung_abfl + 1, 
                ausrichtung_dm + 2, 
                ausrichtung_h + 2
            ]);
            
            translate([
                ausrichtung_dm / 2 - ausrichtung_kerbe_tf + 
                    ausrichtung_kerbe_dm / 2,
                0,
                -1
            ])
            cylinder(
                d = ausrichtung_kerbe_dm, 
                h = ausrichtung_h + 2, 
                $fn = 18
            );
        }
    }

}
/* ... */

Wir beschreiben das Ausrichtungsmuster über die Grundform Zylinder (cylinder), von der wir die Abflachung und Kerben mittels Boolescher Differenzoperation (difference) abziehen. Wir modellieren zunächst die Abflachung und Kerbe auf einer Seite des Hauptzylinders durch die Boolesche Vereinigung (union) eines Quaders (cube) und eines Zylinders (cylinder), die wir gemäß ihrer jeweiligen Rücksprünge passend zum Hauptzylinder positionieren. Da diese Objekte vom Hauptzylinder mittels difference abgezogen werden, achten wir bei der Dimensionierung (und Positionierung) auf ausreichende Zugaben, um eine saubere Differenzoperation zu gewährleisten.

Da unser Modell symmetrisch zum Ursprung ist, können wir Abflachung und Kerbe auf der anderen Seite des Hauptzylinders durch eine Kombination von Spiegeltransformation (mirror) und For-Schleife beschreiben. Der Parameter der Spiegeltransformation ist ein dreidimensionaler Vektor, der angibt, entlang welcher Achse wir unsere Geometrie spiegeln möchten. Ist der Vektor der Nullvektor ([0, 0, 0]), dann findet keine Spiegelung statt. Da unsere Schleifenvariable i genau einmal 0 und einmal 1 wird, erzeugen wir also sowohl unser ursprüngliches Abflachungs-Kerbe-Paar als auch das gespiegelte Paar auf der gegenüberliegenden Seite.

Wird das Uhrwerk in einer Uhr verbaut, wird es über eine zentrale Verschraubung befestigt. Potentielle Uhren müssen also an geeigneter Stelle ein passendes Loch aufweisen. Wir werden diese Verschraubungsachse nur als einfachen Zylinder modellieren und das feine Gewinde der Achse nicht in unser Modell aufnehmen:

module uhrwerk() {

    /* ... */

    verschraubung_dm = 7.8; // Durchmesser der Verschraubung
    verschraubung_h  = 5;   // Höhe der Verschraubung gemessen vom Uhrwerk Grundkörper aus

    /* ... */

    // Verschraubung
    color("Gold")
    cylinder( d = verschraubung_dm, h = verschraubung_h, $fn = 36);

}
/* ... */

Für die Geometrie des Uhrwerks fehlen nun nur noch die drei Achsen für die Stunden, Minuten und Sekundenzeiger:

module uhrwerk() {

    /* ... */

    stundenachse_dm = 5.1; // Durchmesser der Stundenachse
    stundenachse_h  = 8.3; // Höhe der Stundenachse
    
    minutenachse_dm = 3.2;  // Durchmesser der Minutenachse
    minutenachse_h  = 12.1; // Höhe der Minutenachse

    sekundenachse_dm = 1;    // Durchmesser der Sekundenachse
    sekundenachse_h  = 14.5; // Höhe der Sekundenachse

    /* ... */

    // Achsen
    color("Black") {
        cylinder( d = stundenachse_dm,  h = stundenachse_h,  $fn = 36);
        cylinder( d = minutenachse_dm,  h = minutenachse_h,  $fn = 36);
        cylinder( d = sekundenachse_dm, h = sekundenachse_h, $fn = 18);
    }

}
/* ... */

Wie auch die Verschraubung modellieren wir die Achsen als einfache Zylinder. Ein kleines Detail am Rande: die Verwendung der Farbtransformation (color("Black")) ist ein Beispiel dafür, dass Transformationen auch auf Geometriemengen ({ ... }) angewandt werden können.

Abbildung 6.: Das fertige Uhrwerkmodell

Unser nachmodelliertes Uhrwerk könnten wir schon jetzt als Platzhalter- und Kontrollobjekt bei der Konstruktion einer Uhr verwenden (Abbildung 6.). Für die Gestaltung einer Uhr wäre es jedoch deutlich angenehmer, wenn unser Modell auch einen Satz Zeiger hätte. Damit könnte man dann z.B. gleich sehen, ob das Ziffernblatt die richtigen Proportionen hat. Lassen Sie uns also unserem Uhrwerksmodell einen Satz Zeiger geben:

module uhrwerk( zeit = [12, 0, 0] ) {

    /* ... */

    // Zeiger
    rotate( [0, 0, -360 * zeit[0] / 12] )
    translate( [0, 0, stundenachse_h - 1] )
    stundenzeiger();

    rotate( [0, 0, -360 * zeit[1] / 60] )
    translate( [0, 0, minutenachse_h - 1] )
    minutenzeiger();

    rotate( [0, 0, -360 * zeit[2] / 60] )
    translate( [0, 0, sekundenachse_h ] )
    sekundenzeiger();

    module stundenzeiger( ) {
        
        h = 0.4;   // Höhe bzw. Materialstärke
        l = 72.35; // Länge über alles
        r = 67.6;  // Radius (Drehpunkt zu Außenkante)
        b = 5;     // Breite
        dm = 9.5;  // Durchmesser Flansch

        color("Black")
        union() {
            cylinder( d = dm, h = h, $fn = 36 );
            
            translate( [-b / 2, 0, 0] )
            cube( [b, r, h] );
        }
    }

    module minutenzeiger() {

        h  = 0.4;   // Höhe bzw. Materialstärke
        l  = 100.5; // Länge über alles
        r  = 96;    // Radius (Drehpunkt zu Außenkante)
        b  = 4;     // Breite
        dm = 9;     // Durchmesser Flansch
        
        color("Black")
        union() {
            cylinder( d = dm, h = h, $fn = 36 );
            
            translate( [-b / 2, 0, 0] )
            cube( [b, r, h] );
        }    
    }

    module sekundenzeiger() {

        b1  = 1.25; // Breite Vorderteil
        l1  = 95;   // Länge Vorderteil
        b2  = 1.25; // Breite "Steg"
        l2  = 15;   // Länge "Steg"
        b3  = 4.6;  // Breite Hinterteil
        l3  = 24;   // Länge Hinterteil
        dm  = 7.1;  // Durchmesser Flansch
        h   = 0.4;  // Höhe bzw. Materialstärke
        
        color("Red")
        union() {
            cylinder( d = dm, h = h, $fn = 36);
            
            translate( [-b1 / 2, 0, 0] )
            cube( [b1, l1, h] );
            
            translate( [-b2 / 2, -l2, 0] )
            cube( [b2, l2, h] );

            translate( [-b3 / 2, -(l2 + l3), 0] )
            cube( [b3, l3, h] );
        }
    }

}
/* ... */

Wir haben unserem Modul uhrwerk einen Parameter zeit gegeben, der eine Testzeit als dreidimensionalen Vektor erwartet mit [Stunden, Minuten, Sekunden]. Für jeden Zeiger haben wir ein eigenes Untermodul definiert, so dass wir die jeweiligen Zeigergeometrien einfach auf die zugehörige Höhe verschieben und gemäß der Zeit um die Z-Achse drehen können. Hierbei rechnen wir die jeweiligen Zeitangaben in Winkelangaben um.

Die einzelnen Zeiger wurden ausgemessen, die Messwerte dann in Variablen innerhalb der Untermodule festgehalten und dort durch eine Kombination von 3D-Grundformen modelliert. Obwohl die Zeiger eine relativ einfache Form haben (Abbildung 6.1), ist dieses Vorgehen relativ aufwendig.

Abbildung 6.: Zeiger als SVG gezeichnet können als Geometrie eingeladen werden

Da es durchaus üblich ist, dass einem Uhrwerk eine Auswahl verschiedener Zeigerstile beiliegt, ist die bisherige Vorgehensweise nicht besonders effektiv. Ein alternativer Weg besteht darin, die Zeiger in einem externen Zeichenprogramm zu zeichnen und als .svg-Datei abzuspeichern (Abbildung 6.). Mittels der import-Funktion von OpenSCAD können solche externen Zeichnungen dann als 2D-Grundform geladen werden:

module uhrwerk( zeit = [12, 0, 0], zeigerstil = "modern") {

    /* ... */
    
    module stundenzeiger() {
        
        h = 0.4;   // Höhe bzw. Materialstärke
        l = 72.35; // Länge über alles
        r = 67.6;  // Radius (Drehpunkt zu Außenkante)
        b = 5;     // Breite
        dm = 9.5;  // Durchmesser Flansch

        if (zeigerstil == "modern") {
            /* ... */
        }    

        if (zeigerstil == "deco") {
            color("Black")
            linear_extrude(height = h)
            import("stunden.svg");
        }
    }

    module minutenzeiger() {

        h  = 0.4;   // Höhe bzw. Materialstärke
        l  = 100.5; // Länge über alles
        r  = 96;    // Radius (Drehpunkt zu Außenkante)
        b  = 4;     // Breite
        dm = 9;     // Durchmesser Flansch

        if (zeigerstil == "modern") {
            /* ... */
        }
        
        if (zeigerstil == "deco") {
            color("Black")
            linear_extrude(height = h)
            import("minuten.svg");
        }
    }

    module sekundenzeiger() {

        b1  = 1.25; // Breite Vorderteil
        l1  = 95;   // Länge Vorderteil
        b2  = 1.25; // Breite "Steg"
        l2  = 15;   // Länge "Steg"
        b3  = 4.6;  // Breite Hinterteil
        l3  = 24;   // Länge Hinterteil
        dm  = 7.1;  // Durchmesser Flansch
        h   = 0.4;  // Höhe bzw. Materialstärke

        if (zeigerstil == "modern") {            
            /* ... */
        }
                
        if (zeigerstil == "deco") {
            color("Black")
            linear_extrude(height = h)
            import("sekunden.svg");
        }        
    }

}

Das Modul uhrwerk hat einen weiteren Parameter zeigerstil bekommen, mit dem die Art der zu verwendenden Zeiger bestimmt werden kann. Innerhalb der Zeiger-Untermodule wird anhand des Stils eine Fallunterscheidung getroffen. Der Stil “modern” fügt der Geometriebeschreibung die per Hand modellierten und zuvor beschriebenen Zeiger hinzu. Beim Stil “deco” werden hingegen die Zeigerformen aus .svg-Dateien geladen und anschließend mittels linear_extrude in eine 3D-Geometrie umgewandelt. Die import-Funktion verhält sich hierbei wie jede andere 2D-Grundform und kann entsprechend auf gleiche Weise genutzt, z.B. transformiert, werden.

Abbildung 6.: In bestimmten Fällen können leicht bearbeitete Fotos als Grundlage für Geometrien dienen

Die Nutzung des SVG-Imports für komplizierte 2D-Geometrien ist im Allgemeinen der beste Weg, derartige Geometrien in OpenSCAD zu nutzen. Es ist jedoch auch in hierfür spezialisierten Programmen wie z.B. Inkscape nicht ohne Aufwand, solche Geometrien zu erstellen. In unserem Anwendungsfall ist solch ein Aufwand nicht unbedingt gerechtfertigt, da wir die Zeiger in erster Linie als Anschauungsobjekt nutzen und bis auf ihre Gesamtmaße keinen Anspruch an Detailtreue haben. In solch einem Fall können wir Arbeitszeit durch Rechenzeit tauschen und uns die 2D-Geometrien direkt aus Fotos (Abbildung 6.) dieser Geometrien von OpenSCAD erstellen lassen. Die benötigte Rechenzeit und der benötigte Speicher sind dabei leider nicht zu unterschätzen und es kann passieren, dass der im Folgenden beschriebene Weg auf einem zu schwachen Computer nicht praktikabel ist. Zum Glück speichert OpenSCAD das Ergebnis dieser Rechnung zwischen, so dass sie nicht bei jeder Vorschau erneut durchlaufen werden muss!

Schauen wir uns an, wie wir aus .png-Bildern unsere Zeiger extrahieren können:

module uhrwerk( zeit = [12, 0, 0], zeigerstil = "modern") {

    /* ... */
    
    module stundenzeiger() {
        
        /* ... */

        if (zeigerstil == "modern") {
            /* ... */
        }    

        if (zeigerstil == "deco") {
            /* ... */
        }

        if (zeigerstil == "klassisch") {
            lk = 61;

            color("Black")
            png_zeiger( "stunden.png", lk, h, [-9.5, -6.75, 0]);
        }
    }

    module minutenzeiger() {

        /* ... */

        if (zeigerstil == "modern") {
            /* ... */
        }
        
        if (zeigerstil == "deco") {
            /* ... */
        }

        if (zeigerstil == "klassisch") {
            lk = 84;

            color("Black")
            png_zeiger("minuten.png", lk, h, [-9, -6.75, 0]);
        }
    }

    module sekundenzeiger() {

        /* ... */

        if (zeigerstil == "modern") {            
            /* ... */
        }
                
        if (zeigerstil == "deco") {
            /* ... */
        }        

        if (zeigerstil == "klassisch") {
            lk = 123;

            color("Black")
            png_zeiger("sekunden.png", lk, h, [-4,-36.8,0] );
        }
    }

    module png_zeiger(dateiname, laenge, hoehe, null_verschiebung) {
        translate( null_verschiebung )
        linear_extrude( height = hoehe )
        resize( [0, laenge], auto = true )
        projection( cut = true )
        translate( [0, 0, 75] )
        surface( dateiname, invert = true );        
    }

}

Das neue Untermodul png_zeiger erzeugt einen Zeiger aus einer .png-Bilddatei. Zunächst nutzen wir die surface (dt. Oberfläche) Geometrie, um aus einer .png-Bilddatei ein Höhenrelief zu erzeugen. Normalerweise werden hierbei helle Farben als “hoch” und dunkle Farben als “niedrig” interpretiert. Durch den Parameter invert (dt. umkehren) wird diese Interpretation umgekehrt. Das so entstandene Höhenrelief verschieben wir nun derart nach oben, dass es die X-Y-Ebene an geeigneter Stelle schneidet (Abbildung 6. links). Wenden wir jetzt die projection-Transformation auf das Höhenrelief an, so schneiden wir das Relief genau in der X-Y-Ebene und erzeugen aus dem Schnitt eine hinreichend saubere 2D-Geometrie (Abbildung 6. rechts). Die so entstandene 2D-Geometrie hat leider nicht die passende Größe. Daher nutzen wir die resize-Transformation (dt. Größe anpassen), um die Geometrie auf eine gewünschte Länge zu skalieren. Die Breite lassen wir hierbei automatisch bestimmen und haben sie deshalb auf 0 gesetzt. Im Anschluss können wir die nun korrekt skalierte 2D-Geometrie auf die gewünschte Höhe extrudieren und so verschieben, dass die Achse des Zeigers im Ursprung des Koordinatensystems liegt.

Abbildung 6.: Mittels surface-Geometrie können aus Bildern 3D-Strukturen entstehen (links), die anschließend mittels projection-Transformation in hinreichend saubere 2D-Geometrien überführt werden können (rechts)

Um nicht bei jeder Nutzung des Uhrwerk-Moduls diese hohe Rechenzeit aufzuwenden, kann es sich lohnen, die Berechnung einmal durchzuführen und nach der resize-Operation die entstandene 2D-Form mit OpenSCAD als .svg-Datei zu speichern und diese dann wie oben beschrieben mittels import zu laden.

Saubere Schnittstellen von Bibliotheken

Im Prinzip ist unser Uhrwerk-Modul nun fertig und könnte in anderen Geometriebeschreibungen mittels use eingebunden und benutzt werden. Es gibt jedoch einen “Schönheitsfehler” an unserer bisherigen Vorgehensweise. Wenn Sie das Modul in einer anderen Geometriebeschreibung nutzen möchten, dann benötigen Sie fast zwangsläufig Informationen über die Maße des Uhrwerks. Diese stehen der anderen Geometriebeschreibung jedoch nicht zur Verfügung. Eine ad-hoc Lösung würde darin bestehen, die Datei des Uhrwerk-Moduls zu öffnen und die Werte händisch herausfinden. Eine etwas weniger unelegante Lösung würde darin bestehen, die derzeit innerhalb des Moduls uhrwerk definierten Variablen außerhalb des Moduls zu definieren. Wenn Sie dann die .scad-Datei mittels include anstelle von use einbinden, können Sie auf diese Variablen zugreifen. Es gibt jedoch zwei große Probleme mit dieser Lösung. Zum einen kann es zu Namenskollisionen kommen. Wir haben für das Uhrwerk zum Beispiel die Variablen breite, tiefe und hoehe definiert. Da ist die Wahrscheinlichkeit schon nicht gering, dass Sie diese Namen gerne auch in einem anderen Projekt verwenden würden. Schwieriger wird es gar, wenn sie in einer Bibliotheksdatei mehrere Module vorhalten wollen, die alle ihre eigenen Variablen benötigen. Man kann dieses Problem mit einer strikten Namenskonvention lösen. Schön ist das eher nicht. Das zweite große Problem besteht darin, dass Sie keine Kontrolle darüber haben, welche Variablen bzw. Informationen “exportiert” werden. Die Wahl zwischen include und use ist eine Wahl zwischen “alles” oder “nichts”. Wenn Sie sich für “alles” entscheiden, dann verlieren Sie die Möglichkeit, “lokale” Variablen außerhalb von Modulen zu definieren, die nur innerhalb ihrer Bibliothek gelten.

Eine bessere Lösung dieser Problematik kann wie folgt aussehen:

// Uhrwerk Konstanten
uhrwerk_daten = [
    ["breite", 56], // Breite des Grundkörpers
    ["tiefe",  56], // Tiefe des Grundkörpers
    ["höhe",   20], // Höhe des Grundkörpers

    ["ausrichtung_dm",       14], // Durchmesser des Ausrichtungsmusters
    ["ausrichtung_h",         1], // Höhe des Ausrichtungsmusters 
    ["ausrichtung_abfl",      1], // Rücksprung der Abflachungen
    ["ausrichtung_kerbe_dm",  6], // Durchmesser der Kerben
    ["ausrichtung_kerbe_tf",  2], // Rücksprung der Kerben
    
    ["verschraubung_dm", 7.8], // Durchmesser der Verschraubung
    ["verschraubung_h",  5  ], // Höhe der Verschraubung 
                               // gemessen vom Uhrwerk Grundkörper aus
    
    ["stundenachse_dm", 5.1], // Durchmesser der Stundenachse
    ["stundenachse_h",  8.3], // Höhe der Stundenachse
    
    ["minutenachse_dm",  3.2], // Durchmesser der Minutenachse
    ["minutenachse_h",  12.1], // Höhe der Minutenachse

    ["sekundenachse_dm",  1  ], // Durchmesser der Sekundenachse
    ["sekundenachse_h",  14.5]  // Höhe der Sekundenachse
];

function uhrwerk_mass( name ) = 
    [ for (d = uhrwerk_daten) if (d[0] == name) d[1] ][0];

Wir legen ein Feld mit Daten an, die wir zu unserer Geometrie nach außen zur Verfügung stellen wollen. In unserem Fall finden sich alle wichtigen Maße unseres Uhrwerks in dem Feld uhrwerk_daten. Um auf diese Daten zugreifen zu können auch wenn die Bibliothek mit use eingebunden wurde, müssen wir eine Funktion (hier: uhrwerk_mass) für den Zugriff bereitstellen. Diese Funktion bekommt als Parameter den Namen des Datenfeldes, dessen Daten wir auslesen möchten. Eine Möglichkeit, solch eine Funktion zu realisieren, ist die Nutzung einer generative for-Schleife. Wir laufen mit der Schleifenvariablen d durch das Feld uhrwerk_daten und fügen unserem Ergebnis-Feld genau dann ein Element (d[1]) hinzu, wenn der übergebene name dem Namen des Feldeintrags entspricht (if (d[0] == name)). Ergebnis dieser generativen For-Schleife ist ein Feld, dass nur einen Eintrag enthält: genau die Daten, die wir gerne aus uhrwerk_daten auslesen möchten. Was jetzt nur noch bleibt, ist auf diesen einen Eintrag des Feldes zuzugreifen. Dies macht das nachstehende [0].

Für kompliziertere Suchen über Daten stellt OpenSCAD den Befehl search zur Verfügung. Wie dieser genutzt werden kann, können wir am folgenden Beispiel sehen:

// Zeiger Konstanten
zeiger_daten = [
    ["stil",      "modern", "klassisch", "deco"],

    ["stunden_l",    72.35,          61,   62.9], // Länge über alles
    ["stunden_r",     67.6,        56.6,  58.65], // Radius (Drehpunkt zu Außenkante)
    ["stunden_b",        5,         1.5,      2], // Breite an der Spitze
    ["stunden_h",      0.4,         0.4,    0.4], // Höhe bzw. Materialstärke

    ["minuten_l",    100.5,          84,  101.5], // Länge über alles
    ["minuten_r",       96,        79.5,  74.62], // Radius (Drehpunkt zu Außenkante)
    ["minuten_b",        4,           1,      1], // Breite an der Spitze
    ["minuten_h",      0.4,         0.4,    0.4], // Höhe bzw. Materialstärke

    ["sekunden_l",     134,         123,    120], // Länge über alles
    ["sekunden_r",      95,        88.4,  85.66], // Radius (Drehpunkt zu Außenkante)
    ["sekunden_b",    1.25,         0.6,    0.7], // Breite an der Spitze
    ["sekunden_h",     0.4,         0.4,    0.4]  // Höhe bzw. Materialstärke
];

function zeiger_mass( name, stil ) = 
    let ( 
        zeile  = search( [name], zeiger_daten)[0],
        spalte = search( [stil], zeiger_daten[0])[0]
    ) zeiger_daten[zeile][spalte];

Hier haben wir alle Daten der verschiedenen Zeigervarianten zusammengefasst. Die Zugriffsfunktion zeiger_mass hat nun zwei Parameter. Einmal den Namen des Werts und einmal den Namen des Stils. Wir nutzen den Ausdruck let, um mittels search die Indizes der Zeile und Spalte herauszufinden, in der die angefragten Daten liegen. Auch hier taucht wieder die nachgestellte [0] auf, da search als Ergebnis ein Feld mit Indizes zurückliefert und wir nur am ersten Element dieses Feldes in diesem Fall interessiert sind. Der erste Parameter an search ist eine Liste bzw. ein Feld mit Suchbegriffen. Da wir nur nach jeweils einer Zeichenkette (name oder stil) suchen wollen, ist der erste Parameter ein Feld mit nur einem Eintrag. Der zweite Parameter ist das Feld, das durchsucht werden soll. Bei der Suche nach der richtigen Zeile möchten wir durch das komplette Feld zeiger_daten suchen. Hierbei schaut search standardmäßig nur den ersten Eintrag der Feldelemente an. Also hier genau die Namen der Datensätze. Für die Suche nach der richtigen Spalte durchsuchen wir nur den ersten Eintrag von zeiger_daten, der ja wiederum auch ein Feld ist. Auf diese Weise erhalten wir schließlich zwei Indizes zeile und spalte, mit der wir dann das gesuchte Datum aus dem Feld zeiger_daten auslesen und als Funktionswert zurückliefern können (zeiger_daten[zeile][spalte]).

Natürlich ist es sinnvoll, die Funktionen uhrwerk_mass und zeiger_mass auch innerhalb des Moduls uhrwerk für die Initialisierung der Variablen zu nutzen:

module uhrwerk( zeit = [12, 0, 0], zeigerstil = "modern") {

    breite = uhrwerk_mass("breite");
    tiefe  = uhrwerk_mass("tiefe");
    hoehe  = uhrwerk_mass("höhe");
       
    ausrichtung_dm       = uhrwerk_mass("ausrichtung_dm");
    ausrichtung_h        = uhrwerk_mass("ausrichtung_h");
    ausrichtung_abfl     = uhrwerk_mass("ausrichtung_abfl");
    ausrichtung_kerbe_dm = uhrwerk_mass("ausrichtung_kerbe_dm");
    ausrichtung_kerbe_tf = uhrwerk_mass("ausrichtung_kerbe_tf");
    
    verschraubung_dm = uhrwerk_mass("verschraubung_dm");
    verschraubung_h  = uhrwerk_mass("verschraubung_h");
    
    stundenachse_dm = uhrwerk_mass("stundenachse_dm");
    stundenachse_h  = uhrwerk_mass("stundenachse_h");
    
    minutenachse_dm = uhrwerk_mass("minutenachse_dm");
    minutenachse_h  = uhrwerk_mass("minutenachse_h");

    sekundenachse_dm = uhrwerk_mass("sekundenachse_dm");
    sekundenachse_h  = uhrwerk_mass("sekundenachse_h");
                
    /* ... */

    module stundenzeiger() {
        
        h = zeiger_mass("stunden_h",zeigerstil);
        l = zeiger_mass("stunden_l",zeigerstil);
        r = zeiger_mass("stunden_r",zeigerstil);
        b = zeiger_mass("stunden_b",zeigerstil);

        /* ... */
    }

    module minutenzeiger() {

        h = zeiger_mass("minuten_h",zeigerstil);
        l = zeiger_mass("minuten_l",zeigerstil);
        r = zeiger_mass("minuten_r",zeigerstil);
        b = zeiger_mass("minuten_b",zeigerstil);

        /* ... */
    }

    module sekundenzeiger() {

        h = zeiger_mass("sekunden_h",zeigerstil);
        l = zeiger_mass("sekunden_l",zeigerstil);
        r = zeiger_mass("sekunden_r",zeigerstil);
        b = zeiger_mass("sekunden_b",zeigerstil);

        /* ... */
    }
    
    /* ... */
   
}

Das Uhrwerk animieren

OpenSCAD bietet eine einfache Möglichkeit an, unsere Geometrien zu animieren und ggf. die Animationssequenz als Bildfolge abzuspeichern. Um unser Uhrwerk zu animieren, können wir unsere Geometriebeschreibung außerhalb des Moduls uhrwerk wie folgt erweitern:

/* ... */

function get_zeit( tick ) = [(tick / 3600) % 12, (tick / 60) % 60, tick % 60];

uhrwerk( get_zeit($t * 43200) , "modern");

Wir definieren uns eine Funktion get_zeit, der wir als Parameter eine Sekundenzahl geben können und als Ergebnis einen dreidimensionalen Vektor mit Stunden, Minuten und Sekunden erhalten. Der %-Operator steht für den Modulo bzw. den Rest einer Division und sorgt dafür, dass die Minuten- und Sekundenwerte stets im Bereich 0 bis 59 liegen und dass der Stundenwert stets im Bereich 0 bis 11 liegt. Als Parameter übergeben wir der Funktion get_zeit nun den Ausdruck $t * 43200. Die Spezialvariable $t liefert die aktuelle Zeit innerhalb einer Animation zurück, wobei die Animationszeit als Fließkommazahl stets zwischen 0 und 1 verläuft. Wir skalieren also mit dem Ausdruck die Animationszeit auf den Bereich 0 bis 43200 (12 Stunden haben 43200 Sekunden).

Um nun die Animation zu starten, muss man das Animationsmenu sichtbar machen (View -> Animate). Dann erscheinen unter dem Anzeigefenster die drei Eingabefelder Time, FPS und Steps. Bei Steps tragen wir nun 43200 ein und bei FPS eine 1. Schon sollte sich unsere Uhr in Bewegung setzen und vor sich hin ticken. Man beendet die Animation, indem man das Animationsmenu wieder ausblendet (View -> Animate). Hakt man das Feld Dump Pictures an, dann wird jeder Animationsschritt zusätzlich als numerierte .png-Datei abgespeichert. Diese Dateien kann man im Anschluss nutzen, um sie z.B. in eine Video-Datei zu konvertieren. Unter Linux geht dies zum Beispiel mit dem Programm ffmpeg.

Download der OpenSCAD-Datei dieses Projektes

Download der Zeiger-Dateien dieses Projektes

Projekt 5: Stiftehalter

In diesem Projekt wollen wir einen Stiftehalter konstruieren, der viel Platz für unsere Schreibtischutensilien bereit hält und trotz überhängender Elemente ohne Stützstruktur 3D-gedruckt werden kann. Hierzu lassen wir uns von der gotischen Baukunst inspirieren.

Abbildung 7.: Stiftehalter mit gotisch anmutenden Elementen

Was ist neu?

Wir lernen die Rotationsextrusion rotate_extrude kennen und verfolgen diesmal einen explorativen Designansatz, der das finale Modul “von innen heraus” entwickelt.

Los geht’s

Kernelement des Stiftehalters sind die gotisch anmutenden Bögen an der Außenseite. Um diese Bögen zu erstellen, bietet sich die Rotationsextrusion rotate_extrude an. Ähnlich wie die lineare Extrusion linear_extrude wirkt rotate_extrude auf eine nachfolgende 2D-Geometrie und erzeugt aus dieser ein 3D-Objekt. Im Gegensatz zu linear_extrude wird die 2D-Geometrie jedoch nicht entlang einer Gerade sondern entlang einer (impliziten) Kurve extrudiert.

Die Transformation rotate_extrude ist insofern etwas gewöhnungsbedürftig, da einige ihrer Eigenschaften implizit durch die Position der 2D-Geometrie, die extrudiert wird, gegeben ist. Man kann sich die Funktionsweise von rotate_extrude in etwa so vorstellen: die gegebene 2D-Geometrie wird um die X-Achse in die Senkrechte gedreht. Anschließend wird die Geometrie um die Z-Achse im Uhrzeigersinn gedreht und dabei extrudiert. Das Ergebnis hängt also sehr davon ab, wo sich die 2D-Geometrie zuvor befunden hat (Abbildung 7.).

Abbildung 7.: Die Wirkung der Rotationsextrusion hängt davon ab, wo die zugrundeliegende 2D-Geometrie entlang der X-Achse positioniert wird.

Beginnen wir unseren Bogen mit einer 2D-Geometrie, die aus einem Quadrat und einem Kreis besteht und testen wir, ob wir ein entsprechendes Bogenstück erzeugt bekommen:

bogen_grundseite = 5;
bogen_winkel     = 70;
bogen_radius     = 20;

rotate( [-90, 0, 0])
rotate_extrude( angle = bogen_winkel, $fn = 100 )
translate( [-bogen_radius, 0] )
union(){
    translate( [0, -bogen_grundseite / 2] )
    square( bogen_grundseite );
    
    translate( [bogen_grundseite, 0] )
    circle( d = bogen_grundseite * 3 / 5, $fn = 18);
}

Wir erzeugen ein Quadrat (square) mit Seitenlänge bogen_grundseite und verschieben es mittig auf die X-Achse. Da wir uns im zweidimensionalen befinden, benötigt unsere Verschiebetransformation translate auch nur einen zweidimensionalen Vektor als Eingabe. Zusätzlich definieren wir noch einen Kreis (circle), der im Durchmesser 3/5 der Kantenlänge des Quadrats hat und positionieren den Kreis an die Außenkante des Quadrats. Wir verbinden Quadrat und Kreis zu einer gemeinsamen 2D-Geometrie mittels der Booleschen Vereinigung (union).

An dieser Stelle können wir nun den Radius unseres Bogens bestimmen, indem wir die 2D-Geometrie entlang der X-Achse vom Ursprung wegbewegen. Da wir den Kreis unserer Geometrie auf der Innenseite des Bogens haben wollen, schieben wir unsere Geometrie in negativer X-Richtung (translate( [-bogen_radius, 0] )). Erst jetzt wenden wir die rotate_extrude-Transformation an, um unser Bogenstück zu erzeugen. Der Parameter angle gibt hierbei an, wie weit die 2D-Geometrie gedreht werden soll. Ein Winkel von 360 würde einen Ring ergeben. In älteren Versionen von OpenSCAD konnte kein Winkel angegeben werden und es wurde immer ein vollständiger Ring erzeugt. Im Anschluß an die Rotationsextrusion liegt unser Bogenstück noch auf der Seite. Mit einer Rotation um die X-Achse können wir schließlich den Bogen aufrichten.

Wir können nun die gegenüberliegende Seite des Bogens auf einfache Weise mit einer Kombination von For-Schleife und Spiegeltransformation (mirror) erzeugen:

bogen_grundseite = 5;
bogen_winkel     = 70;
bogen_radius     = 20;

for (m = [0:1])
mirror( [m, 0, 0] )
rotate( [-90, 0, 0])
rotate_extrude( angle = bogen_winkel, $fn = 100 )
translate( [-bogen_radius, 0] )
union(){
    translate( [0, -bogen_grundseite / 2] )
    square( bogen_grundseite );
    
    translate( [bogen_grundseite, 0] )
    circle( d = bogen_grundseite * 3 / 5, $fn = 18);
}

Die Schleifenvariable m nimmt genau einmal den Wert 0 und einmal den Wert 1 an und erzeugt damit eine normale und eine gespiegelte Version unseres Bogenstücks. Nun klafft jedoch eine Lücke zwischen unseren Bögen, die wir noch schließen müssen. Das Problem ist, dass unser ursprüngliches Bogenstück mit seiner Oberkante am Ende des Bogens nicht direkt die Z-Achse berührt. Dadurch entsteht beim Spiegeln eine Lücke. Wir müssen also unser Bogenstück vor dem Spiegeln passend an die Z-Achse heranschieben:

bogen_grundseite = 5;
bogen_winkel     = 70;
bogen_radius     = 20;

for (m = [0:1])
mirror( [m, 0, 0] )
translate( [cos(bogen_winkel) * bogen_radius, 0, 0] )
rotate( [-90, 0, 0])
rotate_extrude( angle = bogen_winkel, $fn = 100 )
translate( [-bogen_radius, 0] )
union(){
    translate( [0, -bogen_grundseite / 2] )
    square( bogen_grundseite );
    
    translate( [bogen_grundseite, 0] )
    circle( d = bogen_grundseite * 3 / 5, $fn = 18);
}

Es zeigt sich, dass der Abstand zur Z-Achse durch cos(bogen_winkel) * bogen_radius beschrieben werden kann. Dies ist erstmal nicht offensichtlich. Es hilft, sich die Situation einmal zu skizzieren, das “passende” rechtwinklige Dreieck zu identifizieren und dann bei Wikipedia nachzuschlagen, wie das denn nochmal mit dem Cosinus war… (cos(a) = Ankathete / Hypothenuse).

Wenn wir jetzt ein wenig mit den Parametern bogen_radius und bogen_winkel spielen, dann sehen wir, dass die beiden Bogenhälften immer sauber aneinanderliegen. Für die weitere Verwendung unseres Bogens ist die bisherige Parametrisierung ein wenig unkomfortabel. Idealerweise würden wir lieber die Breite des kompletten Bogens von Außenkante zu Außenkante festlegen und den Radius passend dazu berechnen. Dies geht auf folgende Weise:

bogen_breite     = 50;
bogen_grundseite = 5;
bogen_winkel     = 70;
bogen_radius     = bogen_breite / (2 - 2 * cos(bogen_winkel) );

for (m = [0:1])
mirror( [m, 0, 0] )
translate( [cos(bogen_winkel) * bogen_radius, 0, 0] )
rotate( [-90, 0, 0])
rotate_extrude( angle = bogen_winkel, $fn = 100 )
translate( [-bogen_radius, 0] )
union(){
    translate( [0, -bogen_grundseite / 2] )
    square( bogen_grundseite );
    
    translate( [bogen_grundseite, 0] )
    circle( d = bogen_grundseite * 3 / 5, $fn = 18);
}

Auch in diesem Fall ist es hilfreich, sich die ganze Sache zu skizzieren und eine Gleichung aufzustellen, die einen Bezug zwischen bogen_winkel, bogen_radius und bogen_breite herstellt. Diese Gleichung kann man dann so umformen, dass bogen_radius nur noch alleine auf einer Seite des Gleichheitszeichens steht.

Was uns jetzt noch fehlt sind zwei senkrechte Säulen unterhalb unseres Bogens. Diese können wir mit der gleichen Grundform und einer linearen Extrusion beschreiben. Jetzt ist auch ein guter Zeitpunkt, das ganze schonmal in ein Modul zu verpacken:

module bogen(
    bogen_breite,
    bogen_grundseite,
    bogen_winkel,
    saeulen_hoehe
) {
    
    bogen_radius = bogen_breite / (2 - 2 * cos(bogen_winkel) );

    module grundform() {
        union(){
            translate( [0, -bogen_grundseite / 2] )
            square( bogen_grundseite );
            
            translate( [bogen_grundseite, 0] )
            circle( d = bogen_grundseite * 3 / 5, $fn = 18);
        }
    }

    translate( [0, 0, saeulen_hoehe] )
    for (m = [0:1])
    mirror( [m, 0, 0] )
    translate( [cos(bogen_winkel) * bogen_radius, 0, 0] )
    union(){
        rotate( [-90, 0, 0])
        rotate_extrude( angle = bogen_winkel, $fn = 100 )
        translate( [-bogen_radius, 0] )
        grundform();

        translate( [0, 0, -saeulen_hoehe] )
        linear_extrude( height = saeulen_hoehe )
        translate( [-bogen_radius, 0] )
        grundform();
    }    
}

bogen(
    bogen_breite     = 50,
    bogen_grundseite = 5,
    bogen_winkel     = 75,
    saeulen_hoehe    = 50
);

Wir haben aus den Variablen bogen_breite, bogen_grundseite und bogen_winkel Parameter des neuen Moduls bogen gemacht. Neu hinzugekommen ist der Parameter saeulen_hoehe. Innerhalb des Moduls haben wir unsere 2D-Grundform in ein Untermodul gekapselt, da wir die Grundform an zwei Stellen brauchen - für den Bogen und für die Säulen. An der Stelle, wo wir unser ursprüngliches Bogenstück mittels Drehung um die X-Achse aufgerichtet haben, haben wir die Geometriebeschreibung des Bogens aufgetrennt und eine Boolesche Vereinigung (union) eingefügt. Auf diese Weise können wir unterhalb des Bogenstücks eine Säule anfügen, die dann im weiteren Verlauf mitverschoben und gespiegelt wird.

Es gibt noch einen Schönheitsfehler in unserer Geometriebeschreibung. Wenn wir kleine Winkel nutzen, z.B. 30 Grad, dann “durchbrechen” sich die Spitzen der gespiegelten Bogenteile. Um dies zu verhindern, müssen wir die Spitzen noch sauber abschneiden. Dies können wir mit einer Booleschen Differenzoperation erreichen, die wir direkt vor der Spiegel-Transformation platzieren:

/* ... */
    translate( [0, 0, saeulen_hoehe] )
    for (m = [0:1])
    mirror( [m, 0, 0] )
    difference(){
        translate( [cos(bogen_winkel) * bogen_radius, 0, 0] )
        union(){
            rotate( [-90, 0, 0])
            rotate_extrude( angle = bogen_winkel, $fn = 100 )
            translate( [-bogen_radius, 0] )
            grundform();

            translate( [0, 0, -saeulen_hoehe] )
            linear_extrude( height = saeulen_hoehe )
            translate( [-bogen_radius, 0] )
            grundform();
        }   
       
        translate([
            0, 
            -bogen_grundseite, 
            sin(bogen_winkel) * bogen_radius - 5 * bogen_grundseite
        ])
        cube( 5 * bogen_grundseite);
    }

/* ... */

Nun können wir unser Modul bogen benutzen, um aus ihm das Seitenteil unseres Stiftehalters zu definieren. Hierzu instanziieren wir das Modul mehrfach mit jeweils angepassten Parametern und verschieben die einzelnen Teile auf geeignete Weise zueinander:

bogen_breite     = 60;
bogen_grundseite = 5;
bogen_winkel     = 50;
saeulen_hoehe    = 75;

// Hauptbogen
bogen(
    bogen_breite,
    bogen_grundseite,
    bogen_winkel,
    saeulen_hoehe
);

faktor_breite     = 2 / 3;
faktor_grundseite = 3 / 5;

// Innere Hauptbögen
verschiebung_x1 = 
    (bogen_breite -
     bogen_breite * faktor_breite - 
     bogen_grundseite / 2) / 2;

for(v = [-1:2:1])
translate( [verschiebung_x1*v, 0, 0] )
bogen(
    bogen_breite * faktor_breite,
    bogen_grundseite * faktor_grundseite,
    bogen_winkel * 0.82,
    saeulen_hoehe
);

// Innere Seitenbögen
verschiebung_x2 = 
    (bogen_breite - 
     bogen_breite * (1 - faktor_breite) - 
     bogen_grundseite / 2) / 2;

for(v = [-1:2:1])
translate( [verschiebung_x2*v, 0, 0] )
bogen(
    bogen_breite * (1 - faktor_breite),
    bogen_grundseite * faktor_grundseite,
    bogen_winkel * 0.58,
    saeulen_hoehe
);

Die resultierende Geometriebeschreibung ist im Grunde nicht sonderlich kompliziert, jedoch ein wenig unübersichtlich. Ist man mit einer solchen Beschreibung konfrontiert und möchte sich eine Übersicht darüber verschaffen, welcher Teil der Geometriebeschreibung welcher Komponente im Ausgabefenster entspricht, ist die Hervorhebung einzelner Teile mittels vorangestelltem #-Zeichen sehr hilfreich. Wie schon bei der Beschreibung eines einzelnen Bogens benutzen wir auch hier eine For-Schleife, um jeweils eine Geometriekopie zu definieren. In diesem Fall wird die nachfolgende Geometriebeschreibung nicht gespiegelt, sondern einmal negativ und einmal positiv entlang der X-Achse verschoben. Dies wird erreicht, indem die Schleifenvariable einmal den Wert -1 und einmal den Wert 1 annimmt. Damit die Schleifenvariable auf dem Weg von -1 nach 1 nicht auch den Wert 0 annimmt, ist die Schrittweite auf 2 gesetzt.

Nun können wir auch das Seitenteil in ein Modul auslagern und das Modul bogen zum Untermodul dieses Moduls werden lassen:

module seitenteil(
    bogen_breite,
    bogen_grundseite,
    bogen_winkel,
    saeulen_hoehe
) {

    module bogen(
        bogen_breite,
        bogen_grundseite,
        bogen_winkel,
        saeulen_hoehe
    ) {
            
	    /* ... */

    }

    // Hauptbogen
    
    /* ... */

    // Innere Hauptbögen
    
    /* ... */
    
    // Innere Seitenbögen
    
    /* ... */
}

bogen_breite     = 60;
bogen_grundseite = 5;
bogen_winkel     = 50;
saeulen_hoehe    = 75;

seitenteil(
    bogen_breite,
    bogen_grundseite,
    bogen_winkel,
    saeulen_hoehe
);

Obwohl das Modul seitenteil und sein Untermodul bogen nun gleichnamige Parameter besitzen, kommt es nicht zu Verwechselungen (zumindest nicht auf Seiten von OpenSCAD). Die jeweils “inneren” Variablen überdecken die “äußeren”. Im Modul bogen ist also der Parameter bogen_breite des Moduls seitenteil nicht mehr sichtbar. Es kann nur noch auf den Parameter bogen_breite des Moduls bogen zugegriffen werden.

Nun können wir das Modul seitenteil nutzen, um unseren Stiftehalter zu beschreiben. Beginnen wir mit einer hexagonalen Anordnung der Seitenteile:

bogen_breite     = 60;
bogen_grundseite = 5;
bogen_winkel     = 50;
saeulen_hoehe    = 75;

hexagonal_height = bogen_breite / ( 2 * tan(30) );

for (r = [0:60:359])
rotate( [0, 0, r] )
translate( [0, hexagonal_height - bogen_grundseite / 2, 0] )
seitenteil(
    bogen_breite,
    bogen_grundseite,
    bogen_winkel,
    saeulen_hoehe
);

Um ein Hexagon zu erzeugen, müssen wir ein Seitenteil passend entlang der Y-Achse verschieben und es dann 5 mal um 60 Grad um die Z-Achse drehen. Für die Verschiebung entlang der Y-Achse müssen wir den Abstand der Seitenkanten zum Zentrum des Hexagons ausrechnen. Auch hier hilft es, sich das ganze einmal aufzuzeichnen. Die senkrechte Linie vom Zentrum des Hexagons auf die Mitte eines Seitenteils hat zum Radius des Hexagons einen Winkel von 30 Grad. Der Radius ist in dem entstehenden Dreieck die Hypothenuse. Die Mittelsenkrechte ist die Ankathete und die Hälfte des Seitenteils ist die Gegenkathete. Da der Tangens des Winkels gleich Ankathete durch Gegenkathete ist (und wir an der Ankathete interessiert sind), liefert Gegenkathete / tan(30) die gesuchte Länge der Mittelsenkrechten. Da die Gegenkathete die Hälfte des Seitenteils mit Länge bogen_breite ist, landen wir schließlich bei (bogen_breite / 2) / tan(30), was man wiederum auch als bogen_breite / (2 * tan(30) ) schreiben kann.

Da wir die Seitenteile mittig auf der X-Achse stehend konstruiert haben, müssen wir sie um bogen_grundseite / 2 weniger verschieben als gerade ausgerechnet. Als nächstes beschreiben wir den Boden des Stiftehalters und einen Seitenrand:

bogen_breite     = 60;
bogen_grundseite = 5;
bogen_winkel     = 50;
saeulen_hoehe    = 75;

boden_dicke = 1;
rand_hoehe  = 5;
rand_dicke  = 3;

hexagonal_height = bogen_breite / ( 2 * tan(30) );

for (r = [0:60:359])
rotate( [0, 0, r] )
translate( [0, hexagonal_height - bogen_grundseite / 2, 0] )
union() {
    seitenteil(
        bogen_breite,
        bogen_grundseite,
        bogen_winkel,
        saeulen_hoehe
    );
    
    // bodenplatte
    translate( [-bogen_breite / 2, -hexagonal_height, 0] )
    cube( [bogen_breite, hexagonal_height, boden_dicke] );

    // rand
    translate( [-bogen_breite / 2, -rand_dicke / 2, 0] )
    cube( [bogen_breite, rand_dicke, rand_hoehe] );    
}

Wir nutzen an dieser Stelle einfach die Symmetrie des Hexagons aus und beschreiben jeweils nur einen Teil der Bodenplatte und einen Teil des Randes und nutzen eine Boolesche Vereinigung (union), um diese Teile zusammen mit dem Seitenteil auszurichten und zu drehen.

Zum Schluss kümmern wir uns noch um das Innenleben des Stiftehalters. Dies besteht aus einem hohlen Zylinder und sechs Innenwänden:

/* ... */

zylinder_dm      = 50;
zylinder_hoehe   = 100;
zylinder_wandung = 2;

hexagonal_height = bogen_breite / ( 2 * tan(30) );
hexagonal_radius = bogen_breite / ( 2 * sin(30) );

/* ... */

// Zylinder in der Mitte
difference() {
    cylinder( d = zylinder_dm, h = zylinder_hoehe ,$fn = 50);
    
    translate( [0, 0, boden_dicke] )
    cylinder( 
        d = zylinder_dm - 2 * zylinder_wandung, 
        h = zylinder_hoehe ,
        $fn = 50
    );
}

// Innenwände
for (r = [0:60:359])
rotate( [0, 0, r] )
translate( [zylinder_dm/2 - 0.1, -zylinder_wandung / 2, 0] )
cube([
    hexagonal_radius - zylinder_dm/2 - bogen_grundseite / 2, 
    zylinder_wandung,
    saeulen_hoehe
]);

Wir erzeugen den hohlen Zylinder in der Mitte mittels einer Booleschen Differenzoperation, bei der wir einfach einen zweiten, kleineren und nach oben verschobenen Zylinder abziehen. Für die Innenwände benötigen wir nun den Radius des Hexagons. Man kann den Radius mit den gleichen Überlegungen wie zuvor herleiten. Nur dass man hier nicht den Tangens sondern den Sinus verwendet. Die Innenwände bestehen aus einem Rechteck, das zunächst passend verschoben und dann fünf mal um 60 Grad gedreht und dabei kopiert wird.

Tipps für den 3D-Druck

Die Überhänge in unserem Stiftehalter sind so gering gehalten, dass man den Halter ohne Stützstruktur drucken können sollte. Der Druck wird desto anspruchsvoller, je größer wir bogen_winkel wählen. Sollten Sie Probleme mit dem Drucken der Überhänge haben, versuchen Sie die Schichthöhe (engl. layer height) in ihrem Slicer-Programm zu verringern. Alternativ können Sie auch versuchen, die Linienbreite (engl. line width) zu vergrößern. Mit einer typischen 0.4 Millimeter Düse sollten sie ohne weiteres Linienbreiten von 0.5 bis 0.6 Millimetern drucken können. Zu guter Letzt kann man auch versuchen, die Druckgeschwindigkeit zu reduzieren. Dann hat das Gebläse an der Druckdüse mehr Zeit, das frisch gedruckte Plastik herunterzukühlen.

Download der OpenSCAD-Datei dieses Projektes

Projekt 6: Stempel

In diesem Projekt werden wir einen Stempel modellieren, dessen Geometrie sich automatisch an die Größe des Textes anpasst, mit dem der Stempel parametrisiert wird.

Abbildung 8.: Ein parametrisierbarer Stempel

Was ist neu?

Wir werden uns mit der 2D-Grundform text befassen und sehen, dass uns bei der Verwendung von text eine bestimmte Limitation von OpenSCAD besonders zu schaffen macht. Wir werden diese Limitation trickreich umschiffen. Dabei werden uns die Boolesche Operation intersection sowie die bereits bekannte Funktion projection helfen. Darüber hinaus lernen wir die offset, minkowski- und scale-Transformationen kennen und testen aus, wie man OpenSCAD aus der Kommandozeile heraus bedienen kann.

Los geht’s

Beginnen wir mit der Erstellung eines Grundgerüsts für unsere Modellbeschreibung. Wir definieren ein Modul stempel und darin zwei Untermodule relief und korpus. Die Idee, den Stempel in zwei Untermodule aufzuteilen, kommt daher, dass wir ggf. das Stempelrelief und den Korpus des Stempels getrennt voneinander 3D-drucken wollen, um z.B. unterschiedliche Materialien verwenden zu können:

module stempel (
    txt,                       // Stempeltext
    schriftgroesse,            
    schriftart      = "Liberation Sans:style=Bold Italic", 

    stempeltiefe    = 2,       // Tiefe des Stempelreliefs
){

    module relief() {
        linear_extrude( height = stempeltiefe )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        
    }
    
    module korpus() {        

    }
    
    color("Peru")
    relief();
    
    korpus();
    
}

stempel("OpenSCAD",10);

Unser Modul stempel bekommt vorerst vier Parameter. Der Parameter txt bestimmt den Text, den der Stempel erhalten soll, der Parameter schriftgroesse bestimmt die (maximale) Höhe eines regulären Großbuchstabens in Millimetern und der Parameter schriftart bestimmt die zu verwendene Schriftart. Eine Übersicht über die in OpenSCAD verfügbaren Schriftarten findet sich im Menü unter Help -> Font List. Der vierte Parameter stempeltiefe definiert die Tiefe des Stempelreliefs.

Im Untermodul relief verwenden wir die 2D-Grundform text, um eine zweidimensionale Geometrie des Stempeltextes zu generieren. Wir setzen die vertikale und horizontale Ausrichtung der Schrift (valign und halign) auf center (dt. zentriert) und übergeben txt, schriftgroesse und schriftart als weitere Parameter. Wie auch bei Kreisen, Kugeln und Zylindern können wir die Feinheit der Geometrie mit der Spezialvariable $fn bestimmen. Die resultierende Textgeometrie können wir wie jede andere 2D-Grundform verwenden. In diesem Fall extrudieren (linear_extrude) wir den Text auf die gewünschte Tiefe des Stempelreliefs.

Um nun in unserer Modellbeschreibung fortzufahren, wäre es nützlich, wenn wir Breite und Höhe des Textes herausbekommen könnten. Die Höhe wird ähnlich der Schriftgröße sein. Die Breite hängt vom jeweiligen Stempeltext txt ab. Auch wenn es verwundern mag: es gibt in OpenSCAD keine Möglichkeit, diese Maße herauszufinden! Diese Limitation von OpenSCAD macht es sehr schwer, Geometrien zu definieren, die sich automatisch auf einen gegebenen Text anpassen. Wir wollen es an dieser Stelle dennoch probieren und müssen dafür tief in die Trickkiste greifen. Wir beginnen damit, dass wir ein weiteres Untermodul textflaeche definieren und dort noch einmal den Text als 2D-Grundform erstellen und auf 3 Millimeter extrudieren:

module stempel (
	/* ... */
){

    module textflaeche() {
        linear_extrude( height = 3 )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        
    }
    
    !textflaeche(); // debug

	/* ... */
    
}
/* ... */

Es empfiehlt sich, das Untermodul textflaeche einmal unter der Moduldefinition zu instanziieren und der Instanz ein !-Zeichen voranzustellen. Auf diese Weise kann man die schrittweise Konstruktion der textflaeche besser verfolgen. Nun wird es trickreich. Wir richten unseren Text auf, indem wir ihn um 90 Grad um die X-Achse drehen. Dann nutzen wir die projection-Transformation aus, um den stehenden Text auf die X-Y-Ebene zu projizieren. Anschließend verbinden wir die projizierten “Schatten” der einzelnen Buchstaben mit der hull-Transformation:

module stempel (
	/* ... */
){

    module textflaeche() {
    	hull()
        projection()
        rotate([90,0,0])
        linear_extrude( height = 3 )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        
    }
    
    !textflaeche(); // debug

	/* ... */
    
}
/* ... */

Es empfiehlt sich, die beschriebenen Schritte einzelnd mittels Vorschau (F5) bzw. Berechnung (F6) nachzuvollziehen (Abbildung 8.). Nutzt man die Berechnung anstatt der Vorschau werden 2D-Formen besser als solche erkennbar.

Abbildung 8.: Durch Ausnutzen von projection und hull bekommen wir ein Rechteck, dass die Breite des Textes hat

Wir sind bei der Ermittlung der Textfläche schon ein gutes Stück weiter gekommen. Wir haben jetzt eine 2D-Form, die zumindest schonmal die Breite unseres Textes hat. Wenden wir den Trick einfach noch einmal an, nur drehen wir jetzt nicht um die X-Achse, sondern um die Y-Achse:

module stempel (
	/* ... */
){

    module textflaeche() {
    	hull()
        projection()
        rotate([90,0,0])
        linear_extrude( height = 3 )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        

        hull()
        projection()    
        rotate( [0, 90, 0] )
        linear_extrude( height = 1 )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        
    }
    
    !textflaeche(); // debug

	/* ... */
    
}
/* ... */

Jetzt haben wir eine weitere 2D-Form, die dieses Mal die Höhe unseres Textes hat. Man beachte, dass wir diesmal nur um einen Millimeter extrudiert haben. Was uns jetzt noch fehlt ist ein Weg, diese beiden 2D-Formen zusammenzuführen. Wenn wir die Formen jeweils um eine geeignete Länge extrudieren und dann übereinanderlegen, dann sollte der Schnitt dieser beiden Formen genau die gesuchte Geometrie haben. Für die “geeignete Länge” der ersten Form müssen wir Abschätzen, wie hoch der Text maximal sein wird. Hier können wir einfach 3 x schriftgroesse annehmen. Bei der Länge können wir die Anzahl der Zeichen im Text nehmen, diese mit der Schriftgröße multiplizieren und dann noch einen Sicherheitsfaktor von zwei mit hinzuziehen. Mit diesen Werten können wir nun die 2D-Formen extrudieren und durch geeignete Drehung und Verschiebung überlappen lassen:

module stempel (
	/* ... */
){

    module textflaeche() {
        translate( [0,0,-1] )
        rotate( [-90,0,0] )
        translate( [0, 0, -(3 * schriftgroesse) / 2] )
        linear_extrude( height = 3 * schriftgroesse )
        hull()
        projection()
        rotate([90,0,0])
        linear_extrude( height = 3 )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );
        
        geschaetzte_laenge = len(txt) * schriftgroesse * 2;        
        
        rotate( [0, -90, 0] )
        translate( [0, 0, -geschaetzte_laenge / 2] )
        linear_extrude( height = geschaetzte_laenge )
        hull()
        projection()    
        rotate( [0, 90, 0] )
        linear_extrude( height = 1 )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        
    }
    
    !textflaeche(); // debug

	/* ... */
    
}
/* ... */

Man beachte die Verwendung der Funktion len, um die Anzahl der Zeichen des Stempeltextes zu ermitteln. In gleicher Weise funktioniert die Funktion len auch, um die Anzahl der Elemente in einem Feld zu ermitteln. Nun sind wir fast am Ziel! Mit der Booleschen Operation intersection können wir nun den Schnitt unserer beiden 3D-Formen erzeugen und anschließend mit einer weiteren Anwendung von projection zu einer 2D-Form umwandeln:

module stempel (
	/* ... */
){

    module textflaeche() {
        projection()
        intersection(){
            translate( [0,0,-1] )
            rotate([-90,0,0])
            translate([0,0,-(3 * schriftgroesse) / 2])
            linear_extrude( height = 3 * schriftgroesse )
            hull()
            projection()
            rotate([90,0,0])
            linear_extrude( height = 3 )
            text(
                text = txt, 
                size = schriftgroesse, 
                font = schriftart, 
                valign = "center", 
                halign = "center",
                $fn = 50
            );
            
            geschaetzte_laenge = len(txt) * schriftgroesse * 2;        
            
            rotate( [0, -90, 0] )
            translate( [0, 0, -geschaetzte_laenge/2] )
            linear_extrude( height = geschaetzte_laenge )
            hull()
            projection()    
            rotate( [0, 90, 0] )
            linear_extrude( height = 1 )
            text(
                text = txt, 
                size = schriftgroesse, 
                font = schriftart, 
                valign = "center", 
                halign = "center",
                $fn = 50
            );        
        }
    }
    
    !textflaeche(); // debug

	/* ... */
    
}
/* ... */
Abbildung 8.: Geschafft! Eine 2D-Fläche, die sich automatisch an die Dimension des Textes anpasst

Wenn man nun das !-Zeichen wieder entfernt und eine Vorschau auslöst (F5) kann man sehen, dass wir unser Ziel erreicht haben (Abbildung 8.). Wir haben eine 2D-Fläche erzeugt, die sich automatisch an die Größe des Stempeltextes anpassen kann! Mit Hilfe dieser speziellen Fläche werden wir nun den Rest des Stempels konstruieren. Bevor wir fortfahren, sollten wir sicherstellen, dass wir unsere “debug”-Instanz der textflaeche jetzt wieder entfernen.

Nun widmen wir uns wieder dem Untermodul relief und nutzen unser Modul textflaeche, um den Reliefuntergrund zu beschreiben:

module stempel (
    txt,                       // Stempeltext
    schriftgroesse,            
    schriftart      = "Liberation Sans:style=Bold Italic", 

    stempeltiefe = 2,          // Tiefe des Stempelreliefs
    rand         = 5,          // Abstand zum Text
    kissentiefe  = 2,          // Tiefe des Reliefuntergrunds
){

	/* ... */

    module relief() {
        linear_extrude( height = stempeltiefe )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        

        // reliefuntergrund
        translate( [0, 0, stempeltiefe] )
        linear_extrude( height = kissentiefe)
        offset( delta = rand )
        textflaeche();
    }
    
	/* ... */    
}
/* ... */

Wir erweitern unser Modul stempel um die Parameter rand und kissentiefe, die einen Abstand zum Text und die Tiefe des Reliefuntergrunds festlegen. Den Reliefuntergrund modellieren wir auf Basis der textflaeche und erweitern diese mit der Transformation offset. Diese Transformation dehnt die nachfolgende 2D-Geometrie um den Parameter delta aus bzw. schrumpft die Geometrie falls delta negativ ist. Anschließend extrudieren wir unsere gedehnte textflaeche auf kissentiefe und positionieren sie mittels Verschiebetransformation oberhalb des Stempeltextes (translate( [0, 0, stempeltiefe] )).

Es wäre schön, wenn wir unserem Stempeltext noch wahlweise eine Umrandung geben könnten. Auch diese können wir auf Basis unserer textflaeche konstruieren:

module stempel (
    txt,                       // Stempeltext
    schriftgroesse,            
    schriftart      = "Liberation Sans:style=Bold Italic", 

    stempeltiefe = 2,          // Tiefe des Stempelreliefs
    rand         = 5,          // Abstand zum Text
    kissentiefe  = 2,          // Tiefe des Reliefuntergrunds

    umrandung        = true,   // Umrandung oder nicht
    umrandung_dicke  = 2,      // Wandungsstaerke der Umrandung
    umrandung_radius = 2,      // Eckenradius der Umrandung
){

	/* ... */

    module relief() {
        linear_extrude( height = stempeltiefe )
        text(
            text = txt, 
            size = schriftgroesse, 
            font = schriftart, 
            valign = "center", 
            halign = "center",
            $fn = 50
        );        

        // reliefuntergrund
        translate( [0, 0, stempeltiefe] )
        linear_extrude( height = kissentiefe)
        offset(delta = rand + (umrandung ? umrandung_dicke : 0))
        textflaeche();

        // umrandung
        if (umrandung) {
            
            linear_extrude( height = stempeltiefe )
            difference() {
                minkowski() {
                    offset( delta = rand - umrandung_radius )
                    textflaeche();
                    
                    circle( r = umrandung_radius + umrandung_dicke, $fn = 36);
                }
                
                minkowski() {
                    offset( delta = rand - umrandung_radius )
                    textflaeche();
                    
                    circle( r = umrandung_radius, $fn = 36);
                }
            }

        } // if umrandung
    } // module relief
    
	/* ... */    
}
/* ... */

Für die optionale Umrandung erweitern wir das Modul stempel um drei weitere Parameter umrandung, umrandung_dicke und umrandung_radius, die die Umrandung charakterisieren. Zunächst passen wir unseren Reliefhintergrund an. Wenn es eine Umrandung gibt, dann fügen wir dem Parameter delta der offset-Transformation die Dicke der Umrandung hinzu.

Die Umrandung selbst modellieren wir innerhalb einer if-Fallunterscheidung, so dass die Umrandung nur dann zu einem Teil der Geometriebeschreibung wird, wenn der Parameter umrandung auf true gesetzt ist. Wir konstruieren die Umrandung als Boolesche Differenz zweier 2D-Geometrien, die wir erst dann mittels linear_extrude zu einer 3D-Geometrie umwandeln. Die beiden 2D-Geometrien sind zwei Minkowski-Summen. Die Minkowski-Summe zweier Punktmengen ist die Menge der paarweise addierten Punkte aus beiden Mengen. Im Falle der Minkowski-Summe eines Rechtecks und eines Kreises kann man sich einfach vorstellen, dass der Kreis an alle vier Ecken des Rechtecks kopiert und anschließend die konvexe Hülle des Ganzen gebildet wird (Abbildung 8.).

Abbildung 8.: Minkowski-Summe (rechts) eines Rechteck und eines Kreises (links)

Für die Beschreibung der Umrandung nutzen wir die minkowski-Transformation, um der textflaeche abgerundete Ecken zu geben. Wir benötigen zwei Flächen. Eine große, die die Außenkante der Umrandung beschreibt und eine kleine, die die Innenkante der Umrandung beschreibt. Die kleinere Fläche wird anschließend von der größeren abgezogen. Da bei der minkowski-Transformation der Radius des Kreises zur Erweiterung der textflaeche mittels offset hinzukommt, müssen wir den Radius innerhalb der offset-Transformation berücksichtigen und vom rand abziehen.

Da die minkowski-Transformation nicht damit klarkommt, wenn der Radius der Kreise 0 wird, müssen wir noch eine Fallunterscheidung vornehmen:

module stempel (
	/* ... */
){
	/* ... */

    module relief() {
		/* ... */

        // umrandung
        if (umrandung) {
            
            linear_extrude( height = stempeltiefe )
            difference() {
                minkowski() {
                    offset(
                        delta = rand - 
                                umrandung_radius + 
                                ((umrandung_radius > 0) ? 0 : umrandung_dicke)
                    )
                    textflaeche();
                    
                    if (umrandung_radius > 0)
                    circle( r = umrandung_radius + umrandung_dicke, $fn = 36);
                }
                
                minkowski() {
                    offset( delta = rand - umrandung_radius )
                    textflaeche();
                    
                    if (umrandung_radius > 0)
                    circle( r = umrandung_radius, $fn = 36);
                }
            }

        } // if umrandung
    } // module relief
    
	/* ... */    
}
/* ... */

Nun werden die Kreise innerhalb der Minkowski-Summen nur in die Geometriebeschreibung übernommen, wenn der Radius der Kreise größer 0 ist. Gleichzeitig wird die offset-Transformation der größeren Fläche so angepasst, dass im Falle eines Radius von null die Dicke der Umrandung nicht über den Kreis sondern über die offset-Transformation hergestellt wird.

Damit haben wir die Beschreibung des Stempelreliefs abgeschlossen und können uns jetzt der Geometriebeschreibung des Stempelkorpus zuwenden:

module stempel (
	/* ... */
    stempelrelief      = true, // erzeugt das Stempelrelief     
    stempelkorpus      = true, // erzeugt den Stempelkorpus

    korpus_hoehe       = 10,   // Höhe des Hauptkörpers
    korpus_basis       = 2,    // Höhe der Hauptkörperbasis
    korpus_rand        = 1.6,  // Wandungsstärke der Reliefaufnahme
    korpus_zugabe      = 0.5,  // Vergrößerung der Reliefaufnahme für den
                               // zweiteiligen Druck
    korpus_innentiefe  = 1,    // Tiefe der Reliefaufnahme
    korpus_radius      = 1,    // Radius der Außenkanten des Hautpkörpers
    korpus_ruecksprung = 3,    // Rücksprung der oberen Fläche des Hauptkörpers
    korpus_griff_dm    = 25,   // Durchmesser der Griffkugel
    korpus_griff_hoehe = 40,   // Höhe des Griffkugelzentrums über Hauptkörper
    korpus_griff_rand  = 3,    // unterer Steg des Griffes
    korpus_griff_vdm   = 5     // Durchmesser der "Vorne"-Markierung
){
	/* ... */

    module korpus() {        

        korpus_null = stempeltiefe + kissentiefe - korpus_innentiefe;

        module basis( ruecksprung = 0) {
            minkowski() {
                offset(
                    delta = rand + 
                            (umrandung ? umrandung_dicke : 0) + 
                            korpus_rand +                     
                            (stempelrelief ? 0 : korpus_zugabe) -
                            korpus_radius - 
                            ruecksprung
                )        
                textflaeche();
                
                if (korpus_radius > 0)
                circle( r = korpus_radius, $fn = 36);
            }        
        }            

    }
    
	/* ... */    
}
/* ... */

Wir beginnen auch hier damit, unser Modul stempel um eine ganze Reihe von Parametern zu erweitern. Hier sind insbesondere die Parameter stempelrelief und stempelkorpus hervorzuheben. Sie steuern, ob eine Geometriebeschreibung für das Relief bzw. den Korpus erzeugt wird. Wir werden diese Informationen nicht nur später bei der Instanziierung der Untermodule nutzen, sondern auch innerhalb des Untermoduls korpus. Wir verzichten an dieser Stelle auf eine ausführliche Beschreibung der zahlreichen weiteren Parameter, da sich ihre Bedeutung aus den Kommentaren und ihrer Verwendung in der Geometriebeschreibung ergeben wird.

Im Untermodul korpus definieren wir zunächst die Variable korpus_null, die die Höhe der Unterkante des Korpus beschreibt. Da der Korpus auf der Unterseite eine Aussparung haben soll, die das Stempelrelief aufnimmt, liegt die Unterkante nicht parallel zu stempeltiefe + kissentiefe sondern um korpus_innentiefe niedriger. Als nächstes definieren wir ein Hilfsmodul basis, das eine zweidimensionale Beschreibung der Korpusgrundform liefert. Wie auch das Stempelrelief baut diese Grundform auf der textflaeche auf, die mittels offset-Transformation ausgedehnt und mittels minkowski-Transformation abgerundet wird. Die Ausdehnung der Basisfläche ist abhängig von einigen Parametern und berücksichtigt z.B. ob es eine Umrandung gibt. Über den Parameter ruecksprung kann die Basisfläche im weiteren Verlauf der Modellbeschreibung angepasst werden, ohne eine weitere offset-Transformation nutzen zu müssen. Die Prüfung, ob gleichzeitig auch ein stempelrelief erzeugt wird, hat folgenden Hintergrund: wenn das Stempelrelief nicht gleichzeitig erzeugt wird, gehen wir davon aus, dass das Stempelkorpus und Stempelrelief getrennt hergestellt und nachträglich zusammengefügt werden. Damit man einen Einfluss auf die Passgenauigkeit hat, gibt es den Parameter korpus_zugabe. Man kann damit die Aussparung auf der Unterseite des Korpus etwas größer machen. Entsprechend muss dies auch in der Ausdehnung der Basisfläche berücksichtigt werden. Werden hingegen Stempelrelief und Stempelkorpus gleichzeitig erzeugt, gehen wir davon aus, dass sie auch am Stück hergestellt werden und der Bedarf nach einer Anpassung entfällt.

Der Hautpkörper des Stempelkorpus wird mittels einer hull-Transformation erzeugt, von dem anschließend die Reliefaussparung mittels Boolescher Differenz abgezogen wird:

module stempel (
	/* ... */
){
	/* ... */

    module korpus() {        

		/* ... */

        // hauptkörper
        difference() {
            hull(){
                translate([
                    0,
                    0,
                    korpus_null
                ])
                linear_extrude( height = korpus_basis )
                basis();

                translate([
                    0,
                    0,
                    korpus_null + korpus_hoehe
                ])
                linear_extrude( height = 0.01 )
                basis( korpus_ruecksprung );
            }
            
            translate( [0, 0, stempeltiefe] )
            linear_extrude( height = kissentiefe)
            offset(
            	delta = rand + 
            			(umrandung ? umrandung_dicke : 0) + 
            			(stempelrelief ? 0 : korpus_zugabe)
           	)
            textflaeche();
        }

    }
    
	/* ... */    
}
/* ... */

Der untere Teil des Hauptkörpers besteht aus einer basis mit Höhe korpus_basis. Hierdurch entsteht eine kleine Kante. Der obere Teil besteht aus einer mit korpus_ruecksprung verkleinerten Basis, die nur ein hundertstel Millimeter dick ist. Dieser Teil dient nur als “Deckel”, während die Seitenteile durch die hull-Transformation erzeugt werden. Die Aussparung für das Stempelrelief wird direkt auf Basis der textflaeche erzeugt, mit offset auf die richtige Größe gebracht, analog zur Basis des Stempelreliefs positioniert und schließlich mittels Boolescher Differenzoperation vom Hauptkörper abgezogen.

Was nun noch fehlt ist der Stempelgriff:

module stempel (
	/* ... */
){
	/* ... */

    module korpus() {        

		/* ... */

        // griff kugel
        translate([
            0, 
            0, 
            korpus_null + korpus_hoehe + korpus_griff_hoehe
        ])
        sphere(d = korpus_griff_dm, $fn = 50);
        
        // griff vorne knubbel
        color("Silver")
        translate([
            0, 
            korpus_griff_dm / 2, 
            korpus_null + korpus_hoehe + korpus_griff_hoehe
        ])
        scale( [1, 0.5, 1] )
        sphere(d = korpus_griff_vdm, $fn = 25);
        
        // griff hals
        translate( [0, 0, korpus_null + korpus_hoehe])
        rotate_extrude( $fn = 50)
        difference() {
            square([
                korpus_griff_dm / 2 - korpus_griff_rand, 
                korpus_griff_hoehe
            ]);  
            
            translate([
                korpus_griff_dm / 2, 
                korpus_griff_hoehe / 2 
            ])
            scale( [0.4, 1] )
            circle( d = korpus_griff_hoehe, $fn = 50 );
        }

    }
    
	/* ... */    
}
/* ... */

Der Stempelgriff besteht aus einer Kugel, einem darunter liegenden Hals und einem taktilen Indikator, der die Vorderseite des Stempels anzeigt. Die Kugel besteht lediglich aus einer passend verschobenen sphere-Grundform. Der Vorne-Indikator besteht aus einer kleinen Kugel, die wir zusätzlich mittels der scale-Transformation (dt. skalieren) in Y-Richtung auf 50% gestaucht haben. Den Hals beschreiben wir über eine Rotationsextrusion (Abbildung 8.). Als 2D-Grundform nehmen wir hierfür ein Rechteck und schneiden auf einer Seite des Rechtecks einen gestauchten Kreis mittels Boolescher Differenz aus. Durch die Rotationsextrusion wird dann aus dieser 2D-Geometrie ein konkaver, dreidimensionaler Hals.

Abbildung 8.: Erzeugung des Stempelhalses mittels Rotationsextrusion

Unser Stempelmodul ist nun fast fertig. Wir müssen nur noch die Parameter stempelrelief und stempelkorpus bei der Instanziierung der Untermodule berücksichtigen:

module stempel (
	/* ... */
){
	/* ... */

    if (stempelrelief) {
        color("Peru")
        rotate([0, stempelkorpus ? 0 : 180, 0])            
        relief();
    }
    
    if (stempelkorpus) {        
        korpus();
    }

}
/* ... */

Im Falle des Stempelreliefs haben wir noch eine Rotationstransformation hinzugefügt, die dafür sorgt, dass das Relief umgedreht wird, sollte es alleine berechnet werden. Dies erleichtert den anschließenden 3D-Druck des Reliefs.

Ein kleiner Tipp zum Schluss: Wenn wir in unserem Text spezielle Unicode-Zeichen verwenden wollen, können wir diese mittels der chr-Funktion von OpenSCAD kodieren. Die “Gegenrichtung” bildet die Funktion ord. Sie liefert für ein gegebenes Zeichen den zugehörigen Unicode. Da viele Schriftarten auch grafische Symbole als Zeichen enthalten, kann dies ein Weg sein, diese Symbole innerhalb von OpenSCAD als 2D-Grundform zu nutzen. So liefert text( chr( 9786 ) ); zum Beispiel ein lachendes Gesicht als 2D-Geometrie zurück :).

OpenSCAD in der Kommandozeile

Wir haben es geschafft, die Stempelgeometrie so zu beschreiben, dass sie sich automatisch auf die Dimension des jeweiligen Textes anpasst. Hierdurch eignet sich das Modul stempel recht gut, um es als Beispiel für eine automatisierte Geometrieerstellung über die Kommandozeile zu nutzen. Ein solches Vorgehen könnte man dazu nutzen, die Erstellung von Stempeln über eine alternative Schnittstelle anzubieten.

Um automatisiert Stempel aus der Kommandozeile zu erzeugen, muss unsere Stempelinstanz innerhalb der .scad-Datei mittels Variablen parametrisiert werden:

/* ... */

st  = "OpenSCAD";
stg = 10;

stempel(st,stg);

Die Variablen müssen hierbei schon mit Beispielwerten initialisiert werden und dürfen nicht undefiniert bleiben. Jetzt können wir die Variablen aus der Kommandozeile heraus setzen:

$ OpenSCAD -o stempel.stl -D 'st="3D-Druck!"' -D 'stg=15' stempel.scad 

Die Option -o stempel.stl gibt die Ausgabedatei an. Die Option -D 'st="3D-Druck!"' setzt die Variable st auf den Wert “3D-Druck” und die Option -D 'stg=15 setzt den Parameter stg auf 15. Das letzte Argument stempel.scad ist der Name der .scad-Datei, in der sich unser Modul stempel und die mit den Variablen st und stg parametrisierte Stempelinstanz befindet.

Download der OpenSCAD-Datei dieses Projektes

Projekt 7: Flammenskulptur

Im überwiegenden Teil der Fälle wird OpenSCAD für die Konstruktion und die geometrische Beschreibung von technischen Systemen bzw. Bauteilen genutzt. Dies bedeutet jedoch nicht, dass sich nicht auch organische Formen mit OpenSCAD erzeugen ließen. In diesem Projekt werden wir eine Flammenskulptur erzeugen, die wir mit Hilfe einer Reihe mathematischer Funktionen modellieren werden.

Abbildung 9.: Eine mathematisch modellierte Flammenskulptur

Was ist neu?

Wir werden in diesem Projekt sowohl neue mathematische Funktionen von OpenSCAD kennenlernen (exp, norm, cross) als auch unsere eigenen Funktionen definieren. In diesem Zusammenhang werden wir das erste Mal eine rekursive Funktion verwenden. Desweiteren werden wir sehen, wie wir den children-Aufruf über mehrere Modulebenen kaskadieren können und was es mit dem Befehl concat auf sich hat.

Los geht’s

Fangen wir mit der Modulbeschreibung und Testinstanz unseres Moduls an. Im Gegensatz zu den bisherigen Projekten werden wir gleich alle Parameter am Anfang definieren und nicht erst nach und nach einführen:

// Eine Flammenskulptur

module flammen(
    hoehe, 
    skalierung_start  = [1.0, 1.0, 1.0],
    skalierung_end    = [0.1, 0.1, 0.1],
    x_radius          = 15,
    y_radius          = 10,
    steilheit         = 0.2,
    uebergang         = 0.35,
    hoehen_verzerrung = 0.7,
    rotationen        = 1,
    ebenen            = 30
) {

	        
}

flammen( 180 ) {
    circle( d = 60 , $fn = 30);
    sphere( d = 60 );
}

Betrachtet man die Testinstanz unseres Moduls flammen unterhalb der Modulbeschreibung, so sieht man, dass das Modul auf einer Geometriemenge operieren wird. Hierdurch kann man nachträglich die zugrunde liegenden Geometrien und somit den Charakter der Skulptur ändern.

Die Grundidee für unser Modell besteht darin, zunächst eine Menge an Positionen zu berechnen, die einen geschwungenen Pfad beschreiben und dann anschließend eine 2D-Grundform entlang dieses Pfades zu bewegen und mittels der hull-Transformation paarweise in eine 3D-Geometrie zu überführen. Zusätzlich soll die 2D-Grundform entlang des Pfades skaliert werden. Lassen Sie uns mit der Berechnung des geschwungenen Pfades beginnen:

module flammen(
    hoehe, 
    skalierung_start  = [1.0, 1.0, 1.0],
    skalierung_end    = [0.1, 0.1, 0.1],
    x_radius          = 15,
    y_radius          = 10,
    steilheit         = 0.2,
    uebergang         = 0.35,
    hoehen_verzerrung = 0.7,
    rotationen        = 1,
    ebenen            = 30
) {

    // Positionen entlang des Pfades
    schrittweite = hoehe / ebenen;

    rot_sw = 360 * rotationen / ebenen;

    positionen = [ for (i = [0:ebenen]) [
        cos( i * rot_sw) * x_radius, 
        sin( i * rot_sw) * y_radius, 
        pow( i / ebenen, hoehen_verzerrung) * hoehe
    ]];
	        
}
/* ... */

Der Parameter hoehe gibt die Höhe des Pfades an, den wir erstellen wollen. Der Parameter ebenen gibt die Anzahl der Schritte an, die wir entlang es Pfades berechnen wollen. Hieraus können wir eine schrittweite ermitteln, die wir im weiteren Verlauf als interne Variable benötigen. Analog können wir eine Rotationsschrittweite rot_sw bilden. Die Positionen entlang unseres Pfades speichern wir als dreidimensionale Vektoren innerhalb eines Feldes. Wir berechnen die Positionen mit einer generativen For-Schleife. Die X- und Y-Koordinaten bewegen sich hierbei auf einer elliptischen Kreisbahn mit den Radien x_radius und y_radius, die sich schrittweise nach oben bewegt. Anstatt die Z-Koordinate linear anwachsen zu lassen, verwenden wir eine Potenzfunktion, um die Verteilung der Punkte entlang der Z-Achse mit dem Parameter hoehen_verzerrung beeinflussen zu können. Abbildung 9. gibt einen Eindruck davon, wie sich die Potenzfunktion für verschiedene Werte des Parameters hoehen_verzerrung verhält.

Abbildung 9.: Höhenverzerrung durch Potenzfunktion

Neben der Positionierung möchten wir unsere Grundform entlang des Pfades auch skalieren. Dies soll nicht linear, sondern mit einer Sigmoid-Funktion geschehen. Da OpenSCAD keine “fertige” Sigmoid-Funktion anbietet, müssen wir uns diese selbst erstellen:

module flammen(
    hoehe, 
    skalierung_start  = [1.0, 1.0, 1.0],
    skalierung_end    = [0.1, 0.1, 0.1],
    x_radius          = 15,
    y_radius          = 10,
    steilheit         = 0.2,
    uebergang         = 0.35,
    hoehen_verzerrung = 0.7,
    rotationen        = 1,
    ebenen            = 30
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
    function sigmoid(x, steilheit = 0.5, uebergang = 0.5) =
        let (
            schrittweite = 1.0 - pow( steilheit, 0.1),
            startpunkt   = -uebergang / schrittweite        
        ) 1 / ( 1 + exp( -( x / schrittweite + startpunkt) ) );
    
    sk_faktor = [ for (i = [0:ebenen]) 
        1.0 - sigmoid( i / ebenen, steilheit, uebergang ) 
    ];

	        
}
/* ... */

Wir definieren unsere Sigmoid-Funktion mittels der Exponentialfunktion exp und dem bekannten Schema 1 / (1 + exp(-x) ). Zusätzlich haben wir unsere Funktion so parametrisiert, dass der sigmoide Übergang im Intervall 0 bis 1 für den Parameter x liegt und wir die relative Position und Steilheit des sigmoiden Übergangs mittels der Parameter steilheit und uebergang einstellen können. Auch diese beiden Parameter erwarten Werte zwischen 0 und 1. Abbildung 9. gibt einen Eindruck davon, welche Wirkung verschiedene Werte für steilheit und uebergang auf die Ausgabe der Funktion haben.

Abbildung 9.: Normalisierte Sigmoidfunktion

Wie auch die Positionen berechnen wir die Skalierungsfaktoren über eine generative For-Schleife und speichern sie im Feld sk_faktor ab. Bevor wir fortfahren, sollten wir schonmal eine provisorische Version einer einzelnen Flamme definieren, um zu sehen, ob wir auf dem richtigen Weg sind:

module flammen(
	/* ... */
    skalierung_start  = [1.0, 1.0, 1.0],
    skalierung_end    = [0.1, 0.1, 0.1],
	/* ... */
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
	/* ... */


    // Modul für eine einzelne Flamme
    module flamme() {
                               
        for (i = [0:ebenen])
        translate( positionen[i] )
        linear_extrude( height = 2 )
        scale( 
            skalierung_start * sk_faktor[i] +  
            skalierung_end * (1.0 - sk_faktor[i])
        )
        children(0);

    }

	flamme() children(0); // debug
	        
}

flammen( 180 ) {
    circle( d = 60 , $fn = 30);
    sphere( d = 60 );
}

Wir definieren ein Untermodul flamme und gehen innerhalb dieses Moduls mit einer For-Schleife parallel durch das positionen-Feld und das sk_faktor-Feld und nutzen ihre Werte, um eine 2D-Geometrie zu skalieren (scale) und zu verschieben (translate). Den Skalierungsvektor erzeugen wir als lineare Interpolation zwischen dem Vektor skalierung_start und dem Vektor skalierung_end, wobei der Interpolationswert aus dem Feld sk_faktor entnommen wird.

Zwischen Skalierung und Verschiebung führen wir eine lineare Extrusion aus, um die 2D-Geometrie in eine 3D-Geometrie zu überführen. Als 2D-Geometrie verwenden wir children(0), also das einer Instanz des Moduls flamme nachfolgende Element. Unterhalb unserer Moduldefinition des Moduls flamme haben wir testweise eine Instanz des Moduls instanziiert. Als nachfolgendes Element haben wir hier wieder children(0) verwendet! Dieses children(0) bezieht sich nun wiederum auf das nachfolgende Element des Moduls flammen (Plural!). Schauen wir bei dessen Instanz nach, so sehen wir, dass hier das erste nachfolgende Element ein Kreis (circle) mit Durchmesser 60 ist. Mit dieser Kette haben wir dieses äußere Element bis tief in ein Untermodul weitergeleitet.

Abbildung 9.: Verschiebung einer 2D-Grundform entlang eines Pfades (links) und korrespondierende orthogonale Seitenansicht (rechts)

Abbildung 9. zeigt unseren bisherigen Zwischenstand. Die Kreisgeometrie folgt erkennbar einem geschwungenen Pfad und wird dabei skaliert. Rein theoretisch könnten wir jetzt die Kreise mittels der hull-Transformation paarweise verbinden und würden damit eine bereits recht passable 3D-Geometrie erhalten. Schöner wäre es jedoch, wenn sich die Kreise gemäß des Pfades neigen würden. Hierzu müssen wir herausbekommen, in welche Richtung der Pfad an der jeweiligen Stelle zeigt und dann den Kreis passend rotieren. Hierfür brauchen wir ein wenig Mathematik.

Den Cosinus des Winkels zwischen zwei Vektoren bekommen wir, wenn wir das Skalarprodukt der zwei Vektoren durch das Produkt der Längen der beiden Vektoren teilen. Für die Berechnung der Länge eines Vektors bietet OpenSCAD die Funktion norm an. Die Berechnung des Skalarprodukts müssen wir selbst definieren. Das Skalarprodukt zweier Vektoren v1 und v2 berechnet man, indem man die einzelnen Komponenten der Vektoren miteinander multipliziert und diese Produkte dann aufsummiert (v1[0] * v2[0] + v1[1] * v2[1] + ... + v1[n] * v2[n]). In den meisten Programmiersprachen würden wir hierfür elementweise durch die Vektoren gehen und nach und nach unsere Summe bilden. So eine “laufende Summe” können wir in OpenSCAD nicht bilden, da Variablen ja nur einmal ein Wert zugewiesen werden kann. Ausweg bietet hier die Verwendung von Rekursion:

module flammen(
	/* ... */
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
	/* ... */


    // Winkel entlang des Pfades
    function skalar_p(v1, v2, idx) = (
    	v1[idx] * v2[idx] + 
    	(
    		idx > 0 ? 
    		skalar_p(v1, v2, idx - 1) : 
    		0
    	)
    );

    function skalarprodukt(v1, v2) = skalar_p(v1, v2, len(v1) - 1);

    // Modul für eine einzelne Flamme
	/* ... */
	        
}
/* ... */
}

Die Funktion skalar_p berechnet das Skalarprodukt zweier Vektoren v1 und v2 auf rekursive Weise. Hierfür hat die Funktion noch einen dritten Parameter idx. Die zweite Funktion skalarprodukt startet die Rekursion. Sie ruft skalar_p auf und setzt bei diesem initialen Aufruf den Wert des Parameters idx auf den Index des letzten Elementes (len(v1) - 1) der Vektoren. Die Funktion skalar_p berechnet daraufhin das Produkt der letzten Elemente von v1 und v2 und addiert abhängig vom Wert von idx entweder eine 0, oder den Wert, den der rekursive Aufruf der Funktion skalar_p ergibt. Der Trick - wie bei allen rekursiven Funktionen - besteht nun darin, dafür zu sorgen, dass die Funktion sich nicht unendlich oft selbst aufruft. Wir erreichen das an dieser Stelle dadurch, dass bei dem rekursiven Aufruf von skalar_p der Parameter idx den Wert idx - 1 bekommt. Der Wert von idx wird also mit jedem rekursiven Aufruf kleiner. Kommt er irgendwann bei 0 an, findet kein weiterer rekursiver Aufruf statt und die Funktion wird beendet. Auf dem Weg dahin haben wir die Produkte aller Elemente berechnet und aufsummiert. Als Endergebnis erhalten wir das gesuchte Skalarprodukt von v1 und v2. Falls Ihnen jetzt ein wenig der Kopf brummt: das ist eine ganz normale Nebenwirkung, wenn man sich mit rekursiven Algorithmen beschäftigt!

Nun können wir unser Skalarprodukt nutzen, um die Winkel entlang des Pfades zu berechnen:

module flammen(
	/* ... */
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
	/* ... */


    // Winkel entlang des Pfades
	/* ... */

    function winkel(v1, v2) = acos( skalarprodukt(v1, v2) / ( norm(v1) * norm(v2) )  );

    rel_pos = concat(
        [ positionen[0] ],
        [ for (i = [1:ebenen]) positionen[i] - positionen[i-1] ]
    );
    
    pos_winkel = concat(
        [0],
        [ for (i = [1:ebenen-1]) winkel( [0,0,1], rel_pos[i]) ],
        [0]
    );


    // Modul für eine einzelne Flamme
	/* ... */
	        
}
/* ... */
}

Wir definieren zunächst die Funktion winkel, die uns den Winkel zwischen zwei Vektoren v1 und v2 liefert. Wie zuvor beschrieben, teilen wir hierfür das Skalarprodukt durch das Produkt der Längen und ermitteln den Winkel über die Arkuscosinus-Funktion. Um die Winkel entlang des Pfades zu berechnen, müssen wir aus unseren absoluten positionen erst einmal die relativen Richtungen der einzelnen Pfadabschnitte bestimmen. Hierzu erzeugen wir ein Feld rel_pos das wir aus zwei Feldern mittels der Funktion concat zusammensetzen (engl. concatenate -> dt. konkatenieren bzw. aneinanderhängen). Das erste Feld enthält nur ein Element: die erste Position unseres Pfades. Das zweite Feld enthält für jede folgende Position des Pfades die Differenz zwischen dieser Position (positionen[i]) und der vorherigen Position (positionen[i-1]). Dementsprechend läuft die Schleifenvariable i auch nicht bei 0 sondern erst bei 1 los (i = [1:ebenen]). Insgesamt erhalten wir so ein Feld rel_pos dessen Einträge von 0 bis ebenen gehen. Es ist also genauso lang wie das Feld positionen.

Nun haben wir alle Vorbereitungen abgeschlossen, um endlich das Feld pos_winkel definieren zu können, welches die Winkel entlang des Pfades enthalten wird. Da wir für einen sauberen Abschluss die erste und die letzte 2D-Geometrie nicht drehen wollen, setzen wir das Feld pos_winkel aus drei einzelnen Feldern zusammen. Der erste und der letzte Eintrag wird hierbei einfach auf 0 gesetzt. Die Eintrage dazwischen erzeugen wir nun über eine generative For-Schleife, die unsere Funktion winkel nutzt, um den Winkel zwischen dem senkrechten Vektor [0, 0, 1] und dem jeweiligen relativen Pfadabschnitt rel_pos[i] zu berechnen. Auch hier haben wir darauf geachtet, dass die Einträge des Feldes pos_winkel von 0 bis ebenen gehen. Hierdurch wird die spätere Benutzung dieser Felder eleganter, da wir uns Fallunterscheidungen für den Anfang und das Ende des Pfades sparen können.

Jetzt können wir die Winkelinformation in unserem provisorischen Untermodul flamme zum Einsatz bringen:

module flammen(
	/* ... */
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
	/* ... */


    // Winkel entlang des Pfades
	/* ... */

    // Modul für eine einzelne Flamme
    module flamme() {
                               
        for (i = [0:ebenen])
        translate( positionen[i] )
        rotate( pos_winkel[i], cross([0,0,1], rel_pos[i]) )
        linear_extrude( height = 2 )
        scale( 
            skalierung_start * sk_faktor[i] +  
            skalierung_end * (1.0 - sk_faktor[i])
        )
        children(0);

    }

	flamme() children(0); // debug
	        
}
/* ... */
}

Wir fügen eine Rotationstransformation (rotate) vor der Verschiebung (translate) der Grundform ein. Wir nutzen hier die spezielle Variante der rotate-Transformation, der wir separat einen Winkel und einen Vektor übergeben können, um den die Drehung stattfinden soll. Als Winkel übergeben wir den zuvor ermittelten Winkel aus dem Feld pos_winkel. Die Drehachse ist der Vektor, der senkrecht auf dem Vektor unserer relativen Richtung rel_pos und der senkrechten im Ursprung des Koordinatensystems ([0, 0, 1]) steht. Dieser Vektor ergibt sich aus dem Kreuzprodukt der beiden anderen Vektoren. Glücklicherweise müssen wir das Kreuzprodukt nicht selber definieren, sondern können stattdessen die OpenSCAD-Funktion cross nutzen. Abbildung 9. zeigt, wie unser derzeitiger Zwischenstand aussieht.

Abbildung 9.: Korrekte Rotation der 2D-Grundform entlang des Pfades

Im nächsten Schritt müssen wir das Untermodul flamme etwas umbauen. Unser Ziel ist es ja, die einzelnen Geometrien entlang des Pfades paarweise mit der hull-Transformation zu verbinden. Daher lösen wir die Geometriebeschreibung aus der For-Schleife heraus und verlagern sie in ein weiteres Untermodul ebene, dass wir mit der Schleifenvariable i indizieren können:

module flammen(
	/* ... */
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
	/* ... */


    // Winkel entlang des Pfades
	/* ... */

    // Modul für eine einzelne Flamme
    module flamme() {
                
        module ebene( i ) {
            translate( positionen[i] )
            rotate( pos_winkel[i], cross([0,0,1], rel_pos[i]) )
            linear_extrude( height = 0.01 )
            scale( 
                skalierung_start * sk_faktor[i] +  
                skalierung_end * (1.0 - sk_faktor[i])
            )
            children(0);
        }
        
        for (i = [1:ebenen])
        hull() {
            ebene(i-1) children(0);
            ebene(i) children(0);
        } 
    }

	flamme() children(0); // debug
	        
}
/* ... */
}

Das Untermodul ebene kapselt nun eine einzelne Grundform an der i-ten Position entlang des Pfades. Wir haben die zu Testzwecken auf 2 Millimeter gesetzte Extrusion nun auf 0.01 Millimeter reduziert. Damit arbeiten wir effektiv wieder mit 2D-Geometrien. Die For-Schleife im übergeordneten Modul flamme verbindet diese Ebenen paarweise. Man beachte, dass die Schleifenvariable i nun von 1 anstatt von 0 startet, damit wir in der Geometriemenge, die der hull-Transformation zugeführt wird, den Aufruf ebene(i-1) children(0); machen dürfen. Wie schon zuvor, müssen wir auch hier die von außen gesetzte Geometrie mittels children(0) in das Untermodul ebene herabreichen.

Abbildung 9.: Paarweise Verbindung der Grundformen entlang des Pfades mittels hull-Transformation

Damit ist die Geometriebeschreibung einer einzelnen Flamme vollständig (Abbildung 9.). Was jetzt noch bleibt, ist die Anordnung unserer einzelnen Flamme als Flammentripel. Vorher sollten wir nun die Testinstanz des Moduls flamme entfernen (// debug):

module flammen(
	/* ... */
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
	/* ... */


    // Winkel entlang des Pfades
	/* ... */


    // Modul für eine einzelne Flamme
	/* ... */


    // Flammentripel
    translate([-x_radius,0,0])
    union(){
        flamme()
        children(0);

        translate([x_radius,0,0])
        rotate([0,0,120])
        translate([-x_radius,0,0])
        flamme()
        children(0);

        translate([x_radius,0,0])
        rotate([0,0,240])
        translate([-x_radius,0,0])
        flamme()
        children(0);
    }
	        
}
/* ... */
}

Unser Flammentripel besteht im Wesentlichen aus einer Booleschen Vereinigung und drei Flammen, die jeweils um 120 Grad zueinander gedreht sind. Da die Grundformen innerhalb des Moduls flamme ihre geschwungene Kreisbahn immer bei [x_radius, 0] beginnen, müssen wir die Flammen vor der Rotation zeitweise in den Ursprung verschieben und nach der Rotation wieder zurück.

In seiner jetzigen Form endet das Flammentripel an seiner Spitze stumpf. Um die Situation zu verbessern, positionieren wir noch eine passend skalierte Kappe an die Spitze des Tripels. Die Geometrie hierfür lassen wir uns als zweites Element (children(1);) der externen Geometriemenge liefern:

module flammen(
	/* ... */
) {

    // Positionen entlang des Pfades
	/* ... */


    // Skalierung entlang des Pfades
	/* ... */


    // Winkel entlang des Pfades
	/* ... */


    // Modul für eine einzelne Flamme
	/* ... */


    // Flammentripel
	/* ... */


    // Kappe
    if ($children > 1) {
        translate([-x_radius,0,0])
        translate(positionen[ebenen])
        scale( 
            skalierung_start * sk_faktor[ebenen] +  
            skalierung_end * (1.0 - sk_faktor[ebenen])
        )
        children(1);
    }	        
}
/* ... */
}

Unsere Geometriebeschreibung ist nun vollständig! Durch den Ansatz, sowohl die 2D-Grundform als auch die Geometrie der abschließenden Kappe über eine externe Geometriemenge zu definieren, lässt sich unsere Flammenskulptur nun leicht anpassen. Darüber hinaus bieten die regulären Parameter des Moduls eine Reihe weiterer Einstellmöglichkeiten zur Feinabstimmung.

Variationen der Flammenskulptur

Abbildung 9. zeigt eine Reihe von Varianten, die nur durch Änderung der Ausgangsgeometrien und Parameter entstanden sind.

Abbildung 9.: Variationen der Flammenskulptur

Hier sind die zugehörigen Instanziierungen der Varianten in Abbildung 9.:

// Nummer 1
flammen( 180 ) {
    circle( d = 60 , $fn = 30);
    sphere( d = 60 );
}

// Nummer 2
translate( [60, -60, 0] )
flammen( 
    180,
    steilheit         = 0.1,
    uebergang         = 0.3,
    hoehen_verzerrung = 1.2,
    ebenen            = 30
) {
    scale( [1, 0.33] )
    circle( d = 60 , $fn = 30);
    sphere( d = 120 );
}

// Nummer 3
union(){
    translate( [125, -125, 0] )
    flammen( 
        180,
        skalierung_start  = [1.0, 1.0, 1.0],
        skalierung_end    = [0.2, 0.2, 0.2],
        steilheit         = 0.3,
        uebergang         = 0.35,
        hoehen_verzerrung = 0.5,
        ebenen            = 30
    ) {
        square( 40, center = true);
        cylinder( d1 = 50, d2 = 0, h = 30);
    }

    translate( [125, -125, 0] )
    flammen( 
        180,
        skalierung_start  = [1.0, 1.0, 1.0],
        skalierung_end    = [0.2, 0.2, 0.2],
        steilheit         = 0.3,
        uebergang         = 0.35,
        hoehen_verzerrung = 0.5,
        ebenen            = 30
    ) {
        rotate( [0, 0, 45] )
        square( 40, center = true);
        cylinder( d1 = 50, d2 = 0, h = 30);
    }
}

// Nummer 4
translate( [200, -200, 0] )
flammen( 
    180,
    skalierung_start  = [0.6, 0.6, 1.0],
    skalierung_end    = [0.4, 1.0, 1.0],
    x_radius          = 25,
    y_radius          = 15,
    steilheit         = 0.8,
    uebergang         = 0.6,
    hoehen_verzerrung = 0.8,
    rotationen = 1.3,
    ebenen            = 50
) {
    square( [5, 45], center = true);
}

Download der OpenSCAD-Datei dieses Projektes

Projekt 8: Rekursiver Baum

Natürliche Strukturen, wie etwa die Form von Pflanzen, entstehen vielfach aus Wachstumsprozessen, denen relativ einfache Regeln zugrunde liegen. Mittels rekursiver Geometriebeschreibungen können wir solche Wachstumsprozesse in vereinfachter Form nachbilden und so komplexe Geometrien erzeugen. In diesem Projekt wollen wir die äußere Form eines Baumes mit genau so einer rekursiven Geometriebeschreibung nachbilden.

Abbildung 10.: Ein Baum erzeugt aus einer rekursiven Geometriebeschreibung

Was ist neu?

Wir haben bereits gesehen, dass man mittels rekursiver Funktionen iterative Algorithmen in OpenSCAD definieren kann. Hier werden wir die Möglichkeit kennenlernen, Rekursion auch in der Geometriebeschreibung selbst zu verwenden. Darüber hinaus werden wir lernen, wie wir Zufallszahlen mit der Methode rands erzeugen können.

Los geht’s

Gleich vorab muss eine Warnung ausgesprochen werden: rekursive Geometrien können in ihrer Komplexität sehr schnell anwachsen und bringen praktisch jedes Computersystem sehr schnell an seine jeweiligen Grenzen. Insbesondere die sogenannte Rekursionstiefe, die ein Parameter jeder rekursiven Geometriebeschreibung ist, ist an dieser Stelle ausschlaggebend. Beim nachfolgenden Design sollten sie diesen Parameter nur äußerst zurückhaltend ändern.

Die Grundidee für unseren rekursiven Baum besteht darin, wie bei einem Wachstumsprozess den Baum schrittweise in die Höhe wachsen zu lassen. Jeder Schritt wird dabei rekursiv erfolgen. Starten wir mit einer minimalen Geometriebeschreibung:

module r_baum(
    h_schrittweite,
    haupt_tiefe
){
    linear_extrude( height = h_schrittweite )
    children(0);
    
    if (haupt_tiefe > 0) {
        
        translate( [0, 0, h_schrittweite] )
        r_baum( 
        	h_schrittweite,
        	haupt_tiefe - 1 
        ){
            children(0);
        }
        
    }
}

r_baum(
    h_schrittweite = 10,
    haupt_tiefe    = 20
) {
    circle( d = 10, $fn=8 );
}

Das Modul r_baum hat zunächst zwei Parameter h_schrittweite und haupt_tiefe. Die h_schrittweite gibt die Höhe an, um die wir in jedem Schritt wachsen wollen. Die haupt_tiefe gibt an, wie viele Schritte wir für den aktuellen Abschnitt des Baumes noch gehen können. Innerhalb des Moduls erzeugen wir zunächst ein Stück des Baumes mittels linearer Extrusion (linear_extrude) der dem Modul als erstes Element übergebenen Geometrie. Anschließend überprüfen wir, ob wir noch weitere Wachstumsschritte zur Verfügung haben (if (haupt_tiefe > 0) ...). Wenn dem so ist, dann definieren wir als weitere, um h_schrittweite nach oben verschobene Geometrie das Modul r_baum erneut. Diese rekursive Definition kopiert im Prinzip die Modulbeschreibung des Moduls r_baum erneut an diese Stelle. Dies ist ein wenig so, wie der Effekt, wenn man in zwei gegenüberliegende Spiegel schaut. Es entsteht eine endlose Folge von Kopien.

Damit diese Folge von Kopien in unserer Modulbeschreibung eben nicht endlos ist, ist der Parameter haupt_tiefe so wichtig. Bei der rekursiven Definition von r_baum reduzieren wir die haupt_tiefe um 1. Auf diese Weise wird haupt_tiefe bei jedem Kopierschritt etwas geringer, bis schließlich haupt_tiefe den Wert 0 annimmt und das Kopieren stoppt. Zusätzlich reichen wir mit children(0) die dem Modul ursprünglich übergebene Geometrie an die rekursive Kopie weiter.

Als nächstes wollen wir in unserer Modulbeschreibung die Möglichkeit zufälliger Variationen anlegen. Dies können wir durch die Funktion rands erreichen:

module r_baum(
    h_schrittweite,
    haupt_tiefe,
    z_init
){
    zufall = rands( 0, 1, 10, z_init );
    
    linear_extrude( height = h_schrittweite )
    children(0);
    
    if (haupt_tiefe > 0) {
        
        translate( [0, 0, h_schrittweite] )
        r_baum( 
        	h_schrittweite,
            haupt_tiefe - 1 - zufall[0] * 2,
            zufall[9] * 100
        ) {
            children(0);
        }
        
    }
}

r_baum(
    h_schrittweite = 10,
    haupt_tiefe    = 20,
    z_init         = 42
) {
    circle( d = 10, $fn=8);    
}

Die Funktion rands (kurz für engl. random values -> dt. Zufallszahlen) erzeugt ein Feld von Zufallszahlen. Die ersten beiden Parameter geben die untere und obere Schranke dieser Zahlen an. In unserem Fall erzeugen wir also zufällige Kommazahlen zwischen 0 und 1. Der dritte Parameter gibt an, wie viele Zufallszahlen das Feld enthalten soll. Wir lassen uns erstmal 10 Zahlen erzeugen, da wir an verschiedenen Stellen innerhalb unseres Moduls “Zufall” einstreuen werden. Der letzte Parameter ist ein Initialisierungswert, den wir von außen über den Modulparameter z_init definieren. Die Zufallszahlen, die die Funktion rands erzeugt, sind nicht wirklich zufällig. Sie sehen nur so aus. In Wirklichkeit sind es nur Pseudozufallszahlen, die eine feste Reihenfolge haben. Der Initialisierungswert (eine beliebige Zahl), legt diese Reihenfolge fest. Dies hat den Vorteil, dass wir mit verschiedenen Initialisierungswerten unterschiedliche Folgen von Zufallszahlen bekommen, gleichzeitig jedoch auch die Kontrolle über die Zufallsfolgen behalten. Wenn wir den Initialisierungswert nicht ändern, erhalten wir immer die selbe Folge an Zahlen. Ganz konkret bedeutet das für unseren Baum, dass wir den Parameter z_init unseres Moduls ändern können, um eine neue Baumvariante zu generieren. Wenn uns die Variante gefällt, können wir uns den Wert von z_init merken und so die jeweilige Variante gezielt erzeugen. Wäre dies nicht der Fall und die Zufallszahlen wirklich zufällig, würden wir bei jeder Berechnung der Geometrie einen anderen Baum erhalten.

In unserem Modul r_baum nutzen wir die Zufallszahlen an zwei Stellen. Zum einen reduzieren wir die haupt_tiefe bei der rekursiven Definition von r_baum nicht nur um eins, sondern zusätzlich auch noch um einen zufälligen Wert zwischen 0 und 2 (zufall[0] * 2). Wir nutzen hierzu die erste unserer zehn Zufallszahlen. Zum anderen übergeben wir die letzte unserer zehn Zufallszahlen als neuen z_init-Wert an die rekursive Instanz weiter. Wir skalieren den Wert auf einen Bereich zwischen 0 und 100, da die Dokumentation von rands an dieser Stelle etwas unklar ist, ob sie ganze Zahlen oder Kommazahlen als Initialisierungswert benötigt. Mit der Skalierung gehen wir hier einfach auf Nummer sicher. Wenn Sie nun in der Testinstanz des Moduls r_baum den Wert von z_init ändern und eine Vorschau berechnen (F5), dann werden sie sehen, wie sich die Höhe unseres “Baumes” mit jedem Wert von z_init zufällig ändert.

Stamm und Äste von Bäumen sind überwiegend grade, weisen jedoch durchaus Variationen in ihrer Dicke auf und werden in Wachstumsrichtung dünner. Dies können wir über einen Skalierungsparameter modellieren:

module r_baum(
    h_schrittweite,
    haupt_tiefe,
    z_init,
    skalierung,
    s_varianz
){
    zufall = rands( 0, 1, 10, z_init );
    
    function z(von, bis, idx) = zufall[idx] * (bis - von) + von;
    
    
    sf = skalierung + z(-1, 1 ,1) * s_varianz;
    
    
    linear_extrude( height = h_schrittweite, scale = sf )
    children(0);
    
    if (haupt_tiefe > 0) {
        
        translate( [0, 0, h_schrittweite] )
        r_baum( 
        	h_schrittweite,
            haupt_tiefe - 1 - z(0,2,0),
            z(0,100,9),
            skalierung,
            s_varianz
        ) {
            scale( [sf, sf] )
            children(0);
        }
        
    }
}

r_baum(
    h_schrittweite = 10,
    haupt_tiefe    = 20,
    z_init         = 42,
    skalierung     = 0.97,
    s_varianz      = 0.1
) {
    circle( d = 10, $fn=8);    
}

Wir haben unserem Modul r_baum zwei neue Parameter hinzugefügt. Der Parameter skalierung gibt an, auf wieviel Prozent die übergeben Grundform in jedem Schritt schrumpfen soll. Der Parameter s_varianz definiert die zufällige Schwankungsbreite der skalierung. Innerhalb des Moduls haben wir zunächst ein wenig aufgeräumt und eine Funktion z definiert, die uns einen Zufallswert aus dem Feld zufall liefert und diesen auf ein Intervall [von,bis] skaliert. Hierdurch wird die weitere Verwendung von Zufallszahlen in unserem Modul etwas angenehmer. Im Anschluss bestimmen wir einen Skalierungsfaktor sf aus den Parametern skalierung und s_varianz, der für diesen Schritt der Rekursion gelten soll. Wir übergeben sf der linearen Extrusion (linear_extrude) als Parameter scale. Damit der nächste Baumabschnitt nun sauber an die skalierte Grundform anschließt, müssen wir diese bei der Weitergabe an die rekursive Definition von r_baum entsprechend skalieren (scale( [sf, sf] )).

Unser Baum ist derzeit nichts mehr als ein einzelner Ast. Um dies zu ändern, müssen wir während unseres simulierten Wachstumsprozesses irgendwann (mindestens) einen weiteren Ast abzweigen. Damit wir dies nicht unendlich oft machen, müssen wir eine weitere Rekursionstiefe abzeig_tiefe für diesen neuen Rekursionspfad einführen:

module r_baum(
    h_schrittweite,
    ht_init,
    haupt_tiefe,
    z_init,
    skalierung,
    s_varianz,
    at_init,
    abzweig_tiefe,
    abzweig_winkel
){
	/* ... */

    linear_extrude( height = h_schrittweite, scale = sf )
    children(0);
    
    // Sollen wir abzweigen?
    if (
        (abzweig_tiefe > 0) && 
        (z(0, 0.8 ,2) < ((ht_init - haupt_tiefe) / ht_init) )
    ){
        r_winkel = z(0, 720, 3);
        a_winkel = abzweig_winkel / 2 + z(0, 0.5, 4) * abzweig_winkel;

        translate( [0, 0, h_schrittweite] )
        rotate( [0, 0, r_winkel] ) 
        rotate( [a_winkel, 0, 0] )
        r_baum( 
		    h_schrittweite,
		    ht_init,
		    haupt_tiefe,
		    z(0,100, 5),
		    skalierung,
		    s_varianz,
		    at_init,
		    abzweig_tiefe - 1,
		    abzweig_winkel
        ) {
            scale( [sf, sf] )
            children(0);
        }
        
    }    
    
    if (haupt_tiefe > 0) {
        
        translate( [0, 0, h_schrittweite] )
        r_baum(
            h_schrittweite,
            ht_init,
            haupt_tiefe - 1 - z(0,2,0),
            z(0,100, 9),
            skalierung,
            s_varianz,
            at_init,
            abzweig_tiefe,
            abzweig_winkel
        ) {
            scale( [sf, sf] )
            children(0);
        }
        
    }
}

r_baum(
    h_schrittweite = 10,
    ht_init        = 20,
    haupt_tiefe    = 20,
    z_init         = 42,
    skalierung     = 0.97,
    s_varianz      = 0.1,
    at_init        = 1,
    abzweig_tiefe  = 1,
    abzweig_winkel = 30
) {
    circle( d = 10, $fn=8);    
}

Wir haben unser Modul r_baum um einige Parameter erweitert. Die beiden Init-Parameter ht_init und at_init enthalten die initialen Werte der Parameter haupt_tiefe und abzweig_tiefe. Wir benötigen diese initialen Werte, um bei einer Abzweigung mit einem neuen Ast “frisch” starten zu können. Darüber hinaus haben wir den Parameter abzweig_winkel eingeführt, mit dem wir den (maximalen) Winkel der abzweigenden Äste definieren.

Nach der Geometrie des Astabschnitts (linear_extrude ...) entscheiden wir uns, ob an dieser Stelle ein neuer Ast abzweigen soll. Wir tun dies nur, wenn zwei Bedingungen erfüllt sind: die abzweig_tiefe muss größer 0 sein (sicheres Rekursionsende!) und (&&) ein Zufallswert zwischen 0 und 0.8 muss kleiner sein als ein bestimmtes Verhältnis zwischen aktueller Haupttiefe und initialer Haupttiefe ((ht_init - haupt_tiefe) / ht_init). Warum das? Setzen wir mal ein paar Zahlen ein, um zu sehen, wie sich das Verhältnis mit der Zeit ändert. Am Anfang ist die aktuelle haupt_tiefe gleich der initialen Haupttiefe ht_init. Damit wird das Verhältnis 0. Das ein Zufallswert aus dem Intervall 0 bis 0.8 kleiner als 0 ist, ist ausgeschlossen. Am Ende ist die aktuelle haupt_tiefe gleich 0. Dann wird das Verhältnis 1. In diesem Fall ist es garantiert, dass ein Zufallswert aus dem Intervall 0 bis 0.8 kleiner ist. Anders gesagt: je weiter unser Ast wächst, desto wahrscheinlicher ist es, dass ein neuer Ast abgezweigt wird.

Innerhalb der If-Fallunterscheidung definieren wir zunächst die zwei Winkel r_winkel und a_winkel. Der Rotationswinkel r_winkel ist ein zufälliger Winkel, der bestimmt, in welche Richtung der Zweig abzweigen soll. Der Abzweigewinkel a_winkel bestimmt den konkreten Abzweigewinkel des Astes als eine Mischung aus dem Parameter abzweig_winkel und einem zufälligen Anteil. Anschließend wird der neue Ast als rekursive Definition des Moduls r_baum beschrieben, dass jedoch nun passend gedreht wird, bevor es um h_schrittweite verschoben wird. Die haupt_tiefe dieses neuen Astes wird auf ht_init zurückgesetzt. Die abzweig_tiefe wird hingegen um 1 verringert. Abbildung 10. zeigt die Wirkung dieses zweiten Rekursionspfades für Rekursionstiefen von 1, 2, und 3.

Abbildung 10.: Wirkung des Parameters abzweige_tiefe für Werte von 1, 2 und 3

Unsere Zweige wirken derzeit noch etwas unrealistisch lang und sie häufen sich je nach Zufall doch irgendwie unnatürlich. Lassen Sie uns das erste Problem zuerst angehen, indem wir davon ausgehen, dass sich die Schrittweite in der Länge mit zunehmender Rekursionstiefe verringern sollte. Dies können wir über einen weiteren Parameter r_schrittweite für die Schrittweitenreduktion ermöglichen:

module r_baum(
    h_schrittweite,
    r_schrittweite,
    ht_init,
    haupt_tiefe,
    z_init,
    skalierung,
    s_varianz,
    at_init,
    abzweig_tiefe,
    abzweig_winkel
){   
	/* ... */
       
    // Sollen wir abzweigen?
    if (
        (abzweig_tiefe > 0) && 
        (z(0, 0.8 ,2) < ((ht_init - haupt_tiefe) / ht_init) )
    ){
        r_winkel = z(0, 720, 3);
        a_winkel = abzweig_winkel / 2 + z(0, 0.5, 4) * abzweig_winkel;
        
        neue_schrittweite = 
            h_schrittweite -
            h_schrittweite / 2.5 * pow(r_schrittweite, abzweig_tiefe);        
        
        translate( [0, 0, h_schrittweite] )
        rotate( [0, 0, r_winkel] ) 
        rotate( [a_winkel, 0, 0] )
        r_baum( 
            neue_schrittweite,
            r_schrittweite,
            ht_init,
            ht_init,
            z(0,100, 5),
            skalierung,
            s_varianz,
            at_init,
            abzweig_tiefe - 1,
            abzweig_winkel
        ) {
            scale( [sf, sf] )
            children(0);
        }                
        
    }    
    
    if (haupt_tiefe > 0) {
        
        translate( [0, 0, h_schrittweite] )
        r_baum(
            h_schrittweite,
            r_schrittweite,
            ht_init,
            haupt_tiefe - 1 - z(0,2,0),
            z(0,100,9),
            skalierung,
            s_varianz,
            at_init,
            abzweig_tiefe,
            abzweig_winkel
        ) {
            scale( [sf, sf] )
            children(0);
        }
        
    }
}

r_baum(
    h_schrittweite = 10,
    r_schrittweite = 0.7,
	/* ... */
) {
    circle( d = 10, $fn=8);    
}

Wir nutzen für die Abnahme der Schrittweite eine Potenzfunktion. Da r_schrittweite kleiner 1 ist, machen höhere Potenzen den resultierenden Wert kleiner. Anders ausgedrückt: mit zunehmender Rekursionstiefe nimmt die Reduktion der Schrittweite zu.

Achten Sie darauf, dass sie an allen Stellen, wo sie das Modul r_baum verwenden, die Parameterlisten aktualisieren. Aufgrund eines Fehlers in OpenSCAD können wir an dieser Stelle leider keine Standardwerte für die Parameter setzen und müssen diese jedesmal vollständig angeben. Versucht man Standardparameter zu nutzen, stürzt OpenSCAD ab einer gewissen Anzahl an Parametern mit einem Speicherfehler ab.

Nun können wir uns um das zweite Problem kümmern. Die abzweigenden Äste starten mit der gleichen Größe wie der Ast, aus dem sie hervorgehen. Dies ist nicht sonderlich realistisch. Die Größe des abzweigenden Astes sollte zudem von seinem Winkel abhängen. Ist der Winkel groß, sollte der Durchmesser eher klein sein. Darüber hinaus sollte auch der Hauptast etwas von seinem Durchmesser verlieren und je nach Winkel des Abzweigenden Astes, diesem etwas ausweichen. Lassen Sie uns diese Gedanken in eine Geometriebeschreibung umsetzen:

module r_baum(
    h_schrittweite,
    r_schrittweite,
    ht_init,
    haupt_tiefe,
    z_init,
    skalierung,
    s_varianz,
    at_init,
    abzweig_tiefe,
    abzweig_winkel,
    abzweig_min_gr,
    abzweig_max_gr
){   
	/* ... */

    // Sollen wir abzweigen?
    if (
        (abzweig_tiefe > 0) && 
        (z(0, 0.8 ,2) < ((ht_init - haupt_tiefe) / ht_init) )
    ){
		/* ... */
 
        // wird 0, wenn a_winkel groß ist
        // wird 1, wenn a_winkel klein ist
        abzweig_verhaeltnis = (abzweig_winkel / a_winkel) - 1.0;
        
        abzweig_skalierung = 
            abzweig_min_gr * abzweig_verhaeltnis +
            abzweig_max_gr * (1.0 - abzweig_verhaeltnis);
        
        haupt_skalierung =
                        sf * abzweig_verhaeltnis +
            abzweig_max_gr * (1.0 - abzweig_verhaeltnis);
        

        translate( [0, 0, h_schrittweite] )
        rotate( [0, 0, r_winkel] ) 
        rotate( [a_winkel, 0, 0] )
        r_baum( 
            neue_schrittweite,
            r_schrittweite,
            ht_init,
            ht_init,
            z(0,100, 5),
            skalierung,
            s_varianz,
            at_init,
            abzweig_tiefe - 1,
            abzweig_winkel,
            abzweig_min_gr,
            abzweig_max_gr
        ) {
            scale( [abzweig_skalierung, abzweig_skalierung] )
            children(0);
        }
        
        // in gegenrichtung gedrehter Hauptast
        if (haupt_tiefe > 0) {            
            translate( [0, 0, h_schrittweite] )
            rotate( [0, 0, r_winkel] ) 
            rotate( [-(abzweig_winkel - a_winkel), 0, 0] )
            r_baum(
                neue_schrittweite,
                r_schrittweite,
                ht_init,
                haupt_tiefe - 1 - z(0,2, 6),
                z(0,100, 7),
                skalierung,
                s_varianz,
                at_init,
                abzweig_tiefe,
                abzweig_winkel,
                abzweig_min_gr,
                abzweig_max_gr
            ) {
                scale( [haupt_skalierung, haupt_skalierung] )
                children(0);
            }        
        }        
        
    } else { 
    
        if (haupt_tiefe > 0) {            
            translate( [0, 0, h_schrittweite] )
            r_baum(
                h_schrittweite,
                r_schrittweite,
                ht_init,
                haupt_tiefe - 1 - z(0,2,0),
                z(0,100,9),
                skalierung,
                s_varianz,
                at_init,
                abzweig_tiefe,
                abzweig_winkel,
                abzweig_min_gr,
                abzweig_max_gr
            ) {
                scale( [sf, sf] )
                children(0);
            }        
        }
        
    }
}

r_baum(
	/* ... */
    abzweig_min_gr = 0.3,
    abzweig_max_gr = 0.8
) {
    circle( d = 10, $fn=8);    
}

Die Geometriebeschreibung ist um einiges umfangreicher geworden! Wir haben zwei neue Parameter abzweig_min_gr und abzweig_max_gr hinzugefügt. Diese bestimmen, in welchem Größenbereich sich abzweigende Äste im Verhältnis zur Größe des Hauptastes bewegen. Innerhalb des Moduls haben wir den Fall, in dem wir nicht abzweigen in einen else-Block gesetzt. Dies bedeutet, dass wir im Falle eines Abzweigs mit dem Hauptast nicht einfach geradeaus weiterwachsen sondern das normale Wachstum stoppen.

Innerhalb der Beschreibung der Abzweigung bestimmen wir zunächst ein abzweig_verhältnis und nutzen dies, neue Skalierungsfaktoren sowohl für den abzweigenden Ast als auch für den Hauptast (abzweig_skalierung und haupt_skalierung) zu definieren. In der nachfolgenden Definition des abzweigenden Astes wird abzweig_skalierung dann genutzt, um die weitergereichte children(0)-Geometrie zu skalieren. Nach der Definition des abzweigenden Astes folgt nun eine neue Definition des Hauptastes. Auch hier wird erst einmal geprüft, ob der Hauptast überhaupt noch fortgesetzt werden soll (if (haupt_tiefe > 0)). Im Gegensatz zum normalen Hauptast-Wachstum wird nun der Hauptast vom abzweigenden Ast weggedreht (rotate( [-(abzweig_winkel - a_winkel), 0, 0] )) und die weitergereichte Geometrie wird mit haupt_skalierung skaliert. Darüber hinaus übernimmt der Hauptast nun auch die neue_schrittweite des abzweigenden Astes. Setzt man testweise at_init und abzweig_tiefe auf 3 oder sogar 4, dann sieht man, dass wir auf dem richtigen Weg sind.

Abbildung 10.: Lücken und harte Kanten zwischen den Baumscheiben bei Abzweigen

Wenn man genau hinschaut, dann sieht man, dass die Verbindungsstellen an den Astabzweigungen noch sehr grob aussehen (Abbildung 10.). Dies können wir mit Hilfe einer hull-Transformation an geeigneter Stelle lösen:

module r_baum(
	/* ... */
){   
    /* ... */
    
    // Sollen wir abzweigen?
    if (
        (abzweig_tiefe > 0) && 
        (z(0, 0.8 ,2) < ((ht_init - haupt_tiefe) / ht_init) )
    ){
		/* ... */

        translate( [0, 0, h_schrittweite] )
        rotate( [0, 0, r_winkel] ) 
        rotate( [a_winkel, 0, 0] )
        r_baum( 
			/* ... */
        ) {
            scale( [abzweig_skalierung, abzweig_skalierung] )
            children(0);
        }
        
        // Verbindung zum abzweigenden Ast glätten
        hull() {
            translate( [0, 0, h_schrittweite] )
            linear_extrude( height = 0.01 )
            scale([sf,sf])
            children(0);
    
            translate( [0, 0, h_schrittweite] )
            rotate( [0, 0, r_winkel] )
            rotate( [a_winkel, 0, 0] )
            linear_extrude(
                height = neue_schrittweite / 2, 
                scale = pow(skalierung,1/3)
            )
            scale( [abzweig_skalierung, abzweig_skalierung] )
            children(0);            
        }
        
        // in gegenrichtung gedrehter Hauptast
        if (haupt_tiefe > 0) {            
            translate( [0, 0, h_schrittweite] )
            rotate( [0, 0, r_winkel] ) 
            rotate( [-(abzweig_winkel - a_winkel), 0, 0] )
            r_baum(
				/* ... */
            ) {
                scale( [haupt_skalierung, haupt_skalierung] )
                children(0);
            }    
            
            // Verbindung zum Hauptast glätten
            hull() {
                translate( [0, 0, h_schrittweite] )
                linear_extrude( height = 0.01 )
                scale([sf,sf])
                children(0);
        
                translate( [0, 0, h_schrittweite] )
                rotate( [0, 0, r_winkel] )
                rotate( [-(abzweig_winkel - a_winkel), 0, 0] )
                linear_extrude(
                    height = neue_schrittweite / 2, 
                    scale = pow(skalierung,1/3)
                )
                scale( [haupt_skalierung, haupt_skalierung] )
                children(0);            
            }

        }
        
        
    } else { 
    
        if (haupt_tiefe > 0) {            
            translate( [0, 0, h_schrittweite] )
            r_baum(
                h_schrittweite,
                r_schrittweite,
                ht_init,
                haupt_tiefe - 1 - z(0,2,0),
                z(0,100,9),
                skalierung,
                s_varianz,
                at_init,
                abzweig_tiefe,
                abzweig_winkel,
                abzweig_min_gr,
                abzweig_max_gr
            ) {
                scale( [sf, sf] )
                children(0);
            }        
        }
        
    }
}

r_baum(
    h_schrittweite = 10,
    r_schrittweite = 0.7,
    ht_init        = 20,
    haupt_tiefe    = 20,
    z_init         = 42,
    skalierung     = 0.97,
    s_varianz      = 0.1,
    at_init        = 1,
    abzweig_tiefe  = 1,
    abzweig_winkel = 30,
    abzweig_min_gr = 0.3,
    abzweig_max_gr = 0.8
) {
    circle( d = 10, $fn=8);    
}

Für die Glättung instanziieren wir die übergebene Geometrie (children(0)) jeweils zwei Mal. Eine Instanz positionieren wir am Ende des aktuellen Wachstumsabschnitts und die andere Instanz positionieren wir an den Anfang der Abzweigung bzw. an den Anfang des gedrehten Hauptastes. Letzterer Instanz geben wir die Hälfte der Höhe des nächsten Wachstumsabschnitts und skalieren sie entsprechend. Anschließend nutzen wir die hull-Transformation, um beide “Baumscheiben” miteinander zu verbinden und so die vorhandenen Lücken zu überbrücken (Abbildung 10.).

Abbildung 10.: Die Lücken zwischen den Baumscheiben bei Abzweigen sind nun geschlossen (Abzweige rot, Hauptäste blau)

Was uns jetzt noch fehlt sind die Blätter des Baumes. Hier können wir die Abzweigtiefe nutzen, um im letzten Rekursionsschritt Blätter zu zeichnen anstelle eines Astes:

module r_baum(
	/* ... */
){   
    
    zufall = rands( 0, 1, 10, z_init );        

    function z(von, bis, idx) = zufall[idx] * (bis - von) + von;        

    if ((abzweig_tiefe < 1) && ($children > 1)) {

        color("green")
        rotate([(floor(z_init) % 2 == 0) ? 45 : -45,0,0])
        linear_extrude(height = 0.1)
        children(1);
        
    } else {
    
		/* ... */
        
        // Sollen wir abzweigen?
        if (
            (abzweig_tiefe > 0) && 
            (z(0, 0.8 ,2) < ((ht_init - haupt_tiefe) / ht_init) )
        ){
			/* ... */
            
            translate( [0, 0, h_schrittweite] )
            rotate( [0, 0, r_winkel] ) 
            rotate( [a_winkel, 0, 0] )
            r_baum( 
				/* ... */
            ) {
                scale( [abzweig_skalierung, abzweig_skalierung] )
                children(0);
                if ($children > 1) children(1);
            }
            
            // Verbindung zum abzweigenden Ast glätten
			/* ... */
            
            // in gegenrichtung gedrehter Hauptast
            if (haupt_tiefe > 0) {            
                translate( [0, 0, h_schrittweite] )
                rotate( [0, 0, r_winkel] ) 
                rotate( [-(abzweig_winkel - a_winkel), 0, 0] )
                r_baum(
					/* ... */
                ) {
                    scale( [haupt_skalierung, haupt_skalierung] )
                    children(0);
                    if ($children > 1) children(1);
                }    
                
                // Verbindung zum Haupt glätten
				/* ... */

            }
            
            
        } else { 
        
            if (haupt_tiefe > 0) {            
                translate( [0, 0, h_schrittweite] )
                r_baum(
					/* ... */
                ) {
                    scale( [sf, sf] )
                    children(0);
                    if ($children > 1) children(1);
                }        
            }

            if ((abzweig_tiefe < 2) && ($children > 1)) {

                color("green")
                rotate([(floor(z_init) % 2 == 0) ? 45 : -45,0,0])
                linear_extrude(height = 0.1)
                children(1);
                
            }            
            
        }
    }
}

r_baum(
	/* ... */
    at_init        = 4,
    abzweig_tiefe  = 4,
	/* ... */
) {
    circle( d = 10, $fn=8);    
    square([2,2]);
}

Wir nutzen als Blätter eine weitere, von außen an unser Modul übergebene Geometrie (hier: square([2,2]);). Dementsprechend müssen wir alle rekursiven Verwendungen von r_baum innerhalb unseres Moduls aktualisieren und auch die Geometrie children(1) weiterreichen. Dies sollten wir jedoch nur tun, wenn auch wirklich eine zweite Geometrie übergeben wurde. Daher prüfen wir $children an den entsprechenden Stellen. Da die Ausbeute an Blättern etwas gering ausfällt, wenn man die Blätter nur am Ende der Rekursion zeichnet (if (abzweig_tiefe < 1)) haben wir die Blattgeometrie noch ein weiteres Mal unterhalb des regulären Wachstumspfades definiert und an dieser Stelle schon auf die vorletzte Rekursionsebene geprüft (if (abzweig_tiefe < 2)). Da man nun erst ab einer insgesamten Rekursionstiefe von etwa 4 ein schönes Blätterdach bekommt, haben wir die Parameter der Testinstanz unseres Moduls r_baum ebenfalls angepasst. Der Ausdruck (floor(z_init) % 2 == 0) ? 45 : -45 sorgt dafür, dass die Blätter mal in die eine und mal in die andere Richtung gedreht werden in Abhängigkeit davon, ob der abgerundete (floor) z_init-Wert gerade oder ungerade ist. Wir haben an dieser Stelle den Wert z_init genommen, da sich dieser beständig ändert. Wir hätten genauso gut auch einen Wert aus dem Zufallsfeld nehmen können.

Damit sind wir mit unserer Geometriebeschreibung fertig. Wenn sie automatisch ein paar zufällige Bäume erstellen wollen, dann können Sie die Testinstanz wie folgt parametrisieren:

srnd = rands(0,1000,1)[0];

echo(srnd);

r_baum(
    h_schrittweite = 10,
    r_schrittweite = 0.7,
    ht_init        = 20,
    haupt_tiefe    = 20,
    z_init         = srnd,
    skalierung     = 0.97,
    s_varianz      = 0.1,
    at_init        = 4,
    abzweig_tiefe  = 4,
    abzweig_winkel = 50,
    abzweig_min_gr = 0.3,
    abzweig_max_gr = 0.8
) {
    circle( d = 10, $fn=8);    
    square([2,2]);
}

Die Variable srnd enthält nun eine einzelne zufällige Zahl, die sie für den Parameter z_init des Moduls r_baum nutzen. Da wir hier die rands-Funktion ohne Init-Parameter verwenden, bekommen wir bei jeder Berechnung (F5 bzw. F6) eine neue zufällige Zahl. Der echo-Befehl gibt die Zahl zudem in deŕ OpenSCAD-Konsole aus. Hierdurch kann man sich “gute” Init-Werte merken, aus denen schöne Bäume hervorgegangen sind. Es lohnt sich auch, den Parameter abzweig_winkel zu variieren. Gleiches gilt auch für at_init und abzweig_tiefe sofern es ihre Geduld und die verfügbare Rechenleistung ihres Computers zulassen. Abbildungen 10. und 10.6 zeigen zwei Bäume mit 40 bzw. 60 Grad Abzweigwinkel.

Abbildung 10.: Ein Baum mit Abzweigwinkel von 40 Grad
Abbildung 10.: Ein Baum mit Abzweigwinkel von 60 Grad

Download der OpenSCAD-Datei dieses Projektes

Projekt 9: Parabolspiegel

Parabolspiegel sind faszinierend. Sie bündeln einfallendes paralleles Licht bzw. elektromagnetische Strahlung in einem Fokuspunkt. Satellitenantennen oder Solarkocher machen sich diese Eigenschaft zu nutze. In die andere Richtung, wenn eine Lichtquelle im Fokuspunkt sitzt, kann man mit einem Parabolspiegel einen parallelen Lichtkegel erzeugen.

In diesem Projekt werden wir die Geometriebeschreibung für einen Parabolspiegel erstellen.

Abbildung 11.: Eine Parabolform

Was ist neu?

Wir werden die 3D-Grundform polyhedron kennenlernen. Sie ist im Prinzip das dreidimensionale Gegenstück zur 2D-Grundform polygon. Ein polyhedron zu erstellen ist in der Theorie einfach, in der Praxis jedoch durchaus anspruchsvoll. Daher werden wir Schritt für Schritt vorgehen und uns in diesem Projekt rein auf diese eine Neuheit konzentrieren.

Los geht’s

Eine Parabel ist dadurch charakterisiert, dass alle Punkte auf der Parabeloberfläche den gleichen Abstand zu einer Referenz- bzw. Basisfläche und einem Fokuspunkt haben. Abbildung 11. zeigt dies exemplarisch für einen einzelnen Punkt p der Parabeloberfläche und den zugehörigen Punkten b auf der Basisfläche und f, dem Fokuspunkt. Man erkennt, dass die Punkte p, b und f ein gleichschenkliges Dreieck bilden und daher der Abstand zwischen p und f und der Abstand zwischen p und b gleich ist.

Abbildung 11.: Verhältnis von Fokuspunkt f, Basispunkt b und Parabelpunkt p

Wenn wir nun die Geometrie einer Parabel beschreiben wollen, müssen wir die Punkte p der Parabeloberfläche für einen gegebenen Fokuspunkt und eine gegebenen Basisebene herausbekommen. Wenn wir uns noch einmal Abbildung 11. anschauen, dann sehen wir, dass der Punkt p senkrecht über Punkt b steht. Denkt man sich Punkt b als Teil einer Basisfläche in der X-Y-Ebene, dann können wir die X- und Y- Koordinaten von Punkt p einfach direkt aus Punkt b übernehmen. Es bleibt also nur die Z-Koordinate von p übrig, die wir herausbekommen müssen. Hierbei können uns zwei Dreiecke helfen, die sich in Abbildung 11. verstecken. Beide Dreiecke teilen sich den Winkel α. Das größere von beiden hat seine Hypothenuse von f nach b und die Ankathete f.z - b.z. Das kleinere Dreieck hat als Hypothenuse die gesuchte Länge p.z und als Ankathete genau die Hälfte der Strecke von f nach b (weil es Teil des gleichschenkliges Dreiecks ist).

Wenn wir uns daran erinnern, dass cos(α) = Ankathete / Hypothenuse ist, ergibt sich daraus für das große Dreieck:

// Abstand zwischen f und b
abst_fb = norm(f - b)			

cos(α) = (f.z - b.z) / abst_fb

Für das kleine Dreieck gilt:

cos(α) = (abst_fb / 2) / p.z

Da es sich in beiden Fällen um das gleiche α handelt, können wir die beiden Seitenverhältnisse gleich setzen und nach dem gesuchten p.z umformen:

(f.z - b.z) / abst_fb = (abst_fb / 2) / p.z


// beide Seiten mal p.z

p.z * (f.z - b.z) / abst_fb = abst_fb / 2


// beide Seiten durch ((f.z - b.z) / abst_fb)

p.z = (abst_fb / 2) / ((f.z - b.z) / abst_fb)


// äußere Division ersetzen durch Multiplikation mit Kehrwert

p.z = (abst_fb / 2) * (abst_fb / (f.z - b.z))


// Ausmultiplizieren: Zähler mal Zähler, Nenner mal Nenner

p.z = (abst_fb * abst_fb) / (2 * (f.z - b.z))

Damit wären wir fertig! Wir haben jetzt eine kompakte Formel, um einen Punkt p auf der Parabeloberfläche in Bezug zu einem Fokuspunkt und einem Basispunkt zu berechnen. Lassen Sie uns dieses Ergebnis in einer OpenSCAD-Funktion parabelpunkt festhalten:

function parabelpunkt( fokuspunkt, basispunkt ) =
    let ( abst_fb = norm(fokuspunkt - basispunkt) )
    [
        basispunkt.x,
        basispunkt.y,
        ( abst_fb * abst_fb ) / ( 2 * (fokuspunkt.z - basispunkt.z) )
    ];

Nach diesen Vorbereitungen können wir mit der Definition eines Moduls parabel beginnen und mit Hilfe unserer Funktion parabelpunkt die Punkte auf der Oberfläche der Parabel berechnen:

/*
    Modul parabel
    
    - fokuspunkt, Position des Fokuspunktes als 3D Vektor
    - basis,      Dimension der Basisfläche als 2D Vektor
    - aufloesung, Anzahl Gitterpunkte in X- und Y-Richtung als 2D Vektor
*/
module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
    function parabelpunkt( fokuspunkt, basispunkt ) =
        let ( abst_fb = norm(fokuspunkt - basispunkt) )
        [
            basispunkt.x,
            basispunkt.y,
            ( abst_fb * abst_fb ) / ( 2 * (fokuspunkt.z - basispunkt.z) )
        ];

    parabelpunkte = [
        for ( 
            y = [0 : basis.y / aufloesung.y : basis.y + 0.1], 
            x = [0 : basis.x / aufloesung.x : basis.x + 0.1] 
        )
        parabelpunkt( fokuspunkt, [x,y,0] )
    ];
    
}

parabel( 
    fokuspunkt = [75,75,100],
    basis      = [150,150] 
);

Unser Modul parabel hat drei Parameter. Der Parameter fokuspunkt bestimmt die Position des Fokuspunktes der Parabel als dreidimensionalen Vektor. Der Parameter basis legt die Dimension der Basisfläche in Form eines zweidimensionalen Vektors fest. Wir gehen also davon aus, dass unsere Basisfläche mit einer Ecke im Ursprung liegt und sich auf der X-Y-Ebene befindet (Z = 0). Der dritte Parameter aufloesung bestimmt die Anzahl der Einzelflächen in X- und Y-Richtung, aus der die Parabeloberfläche bestehen soll.

Innerhalb des Moduls berechnen wir die parabelpunkte als Feld von Vektoren über eine generative, zweidimensionale For-Schleife. Die Schrittweite der Schleifenvariablen ergibt sich aus der jeweiligen Anzahl der Teilflächen (aufloesung) und der Dimension der Basisfläche (basis). Die Zugabe von einem zehntel Millimeter bei den jeweiligen Obergrenzen der Spannen dient der Kompensation von Rundungsfehlern. Es bedeutet nicht, dass die Parabel einen Zehntel größer würde. Innerhalb der generativen For-Schleife nutzen wir schließlich unsere Funktion parabelpunkt um jeden Punkt der Parabeloberfläche auszurechnen.

Da wir einen dreidimensionalen Körper erstellen wollen, benötigen wir noch die Punkte der Basisfläche selbst. Diese können wir ebenfalls mit einer generativen For-Schleife erzeugen. Anstelle eine Funktion wie parabelpunkt zu nutzen, können wir in diesem Fall den dreidimensionalen Vektor der Punkte direkt angeben. Er besteht lediglich aus den X- und Y-Koordinaten sowie einer konstanten Z-Koordinate, die wir auf 0 setzen:

module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
	/* ... */

    parabelpunkte = [
        for ( 
            y = [0 : basis.y / aufloesung.y : basis.y + 0.1], 
            x = [0 : basis.x / aufloesung.x : basis.x + 0.1] 
        )
        parabelpunkt( fokuspunkt, [x,y,0] )
    ];

    basispunkte = [
        for ( 
            y = [0 : basis.y / aufloesung.y : basis.y + 0.1], 
            x = [0 : basis.x / aufloesung.x : basis.x + 0.1] 
        )
        [x, y, 0]
    ];
    
}
/* ... */

Jetzt kommen wir zum etwas kniffligen Teil. Wir müssen nun die einzelnen Flächen unserer Geometrie explizit angeben. Jede Einzelfläche benötigt vier Punkte, die reihum angegeben werden müssen. Dies ist im Grunde so, als würde man die Außenkante der Fläche mit einem Stift durchgehend und ohne neu anzusetzen nachzeichnen wollen. Sie erinnern sich vielleicht noch an diese Rätselbilder in ihrer Kindheit. Man bekam ein Blatt mit nummerierten Punkten und musste dann die Punkte nacheinander verbinden, um das Motiv zum Vorschein zu bringen. Wir machen jetzt sowas ähnliches.

Die Punkte in unseren Feldern parabelpunkte und basispunkte liegen ja nacheinander wie in einer langen Liste vor. Jede dieser Listen enthält (aufloesung.x + 1) * (aufloesung.y + 1) Punkte. Damit kann man jeden Punkt mit einer Nummer identifizieren. So wie die Hausnummern entlang einer Straße. Mit diesen “Hausnummern” der Punkte können wir jetzt eine Fläche beschreiben, indem wir jeweils vier “Hausnummern” in einem vierdimensionalen Vektor zusammenfassen:

module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
	/* ... */

    anzahl_x = aufloesung.x + 1;
                
    parabel_flaechen = [
        for ( 
            y = [0 : aufloesung.y - 1], 
            x = [0 : aufloesung.x - 1] 
        )
        [ 
            y    * anzahl_x + x, 
            y    * anzahl_x + x + 1,
           (y+1) * anzahl_x + x + 1,
           (y+1) * anzahl_x + x
        ]
    ];
    
}
/* ... */

Da wir unsere Parabelpunkte zeilenweise definiert haben, liegen die Punkte im eindimensionalen Feld parabelpunkte entsprechend zeilenweise hintereinander. Die Nummern benachbarter Punkte in X-Richtung unterscheiden sich daher immer nur um 1. Die Nummern benachbarter Punkte in Y-Richtung unterscheiden sich hingegen um anzahl_x = aufloesung.x + 1. Anders ausgedrückt: alle anzahl_x Punkte im Feld parabelpunkte startet eine neue Zeile. Abbildung 11. zeigt diesen Zusammenhang für eine Auflösung von 5 x 5 Flächen schematisch auf. In diesem Beispiel wäre also aufloesung.x = 5 und dementsprechend anzahl_x = 6. Die Zeilen fangen also bei 0, 6, 12, usw. an.

Abbildung 11.: Nummerierung der Punkte und zugehörigen Flächen bei einer Auflösung von 5 x 5

Wenn wir also die Nummer des ersten Punktes einer Fläche finden wollen, müssen wir zunächst die Startnummer der Zeile ausrechnen (y * anzahl_x) und dann die X-Position des Punktes dazurechnen (+ x). Der nächste Punkt der Fläche liegt direkt daneben. Man muss also zu der bisherigen Nummer nur 1 dazuzählen (y * anzahl_x + x + 1). Der dritte Punkt liegt eine Zeile tiefer (y + 1). Daher ist seine Nummer (y + 1) * anzahl_x + x + 1. Der vierte Punkt liegt in der gleichen Zeile vor dem dritten Punkt. Wir müssen also von der Nummer des dritten Punktes wieder 1 abziehen ((y + 1) * anzahl_x + x). Wenn wir dies für alle Flächen wiederholen, erhalten wir die gewünschte Liste der parabel_flaechen.

Für die Flächen der Basis können wir auf ähnliche Weise verfahren:

module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
	/* ... */

    anzahl_x = aufloesung.x + 1;

	/* ... */

    anzahl_parabelpunkte = len( parabelpunkte );
    
    basis_flaechen = [
        for ( 
            y = [0 : aufloesung.y - 1], 
            x = [0 : aufloesung.x - 1] 
        )
        [
        	 y    * anzahl_x + x     + anzahl_parabelpunkte, 
           	 y    * anzahl_x + x + 1 + anzahl_parabelpunkte,
          	(y+1) * anzahl_x + x + 1 + anzahl_parabelpunkte,
         	(y+1) * anzahl_x + x     + anzahl_parabelpunkte
        ]
    ];
    
}
/* ... */

Da wir später die basispunkte an die parabelpunkte anfügen werden, verschieben sich die Nummern der Basispunkte um die Anzahl der Parabelpunkte. Diese Verschiebung wird durch die Addition von anzahl_parabelpunkte erreicht. Ansonsten ändert sich an der Bestimmung der Flächen nichts.

Wir haben nun die Flächen der Parabel und der Basis definiert. Was uns noch fehlt sind die Flächen der Seiten. Fangen wir mit einer Seite an:

module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
	/* ... */

    anzahl_x = aufloesung.x + 1;

	/* ... */

    anzahl_parabelpunkte = len( parabelpunkte );
    
	/* ... */

    seiten_flaechen_1 = [
        for ( x = [0 : aufloesung.x - 1] )
        [ 
        	x, 
          	x + 1, 
          	x + 1 + anzahl_parabelpunkte, 
          	x     + anzahl_parabelpunkte 
        ]
    ];
    
}
/* ... */

Für die erste Seite laufen wir nur einmal entlang der X-Richtung und verbinden jeweils paarweise die Punkte aus dem Feld parabelpunkte mit ihren korrespondierenden Punkten aus dem Feld basispunkte. Da die Nummern der basispunkte bei anzahl_parabelpunkte beginnen, finden wir die entsprechende Addition bei den Punkten 3 und 4 der Fläche.

Die gegenüberliegende Seite kann auf ähnliche Weise beschrieben werden. Hier müssen wir die Nummern so verschieben, dass wir nicht entlang der ersten Zeile laufen, sondern entlang der letzten Zeile:

module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
	/* ... */

    anzahl_x = aufloesung.x + 1;

	/* ... */

    anzahl_parabelpunkte = len( parabelpunkte );
    
	/* ... */

    letzte_zeile = aufloesung.y * anzahl_x;
        
    seiten_flaechen_2 = [
        for ( x = [0 : aufloesung.x - 1] )
        [ 
            letzte_zeile + x, 
            letzte_zeile + x + 1, 
            letzte_zeile + x + 1 + anzahl_parabelpunkte, 
            letzte_zeile + x     + anzahl_parabelpunkte
        ]
    ];
    
}
/* ... */

Wir erreichen dies, indem wir die Startnummer der letzten Zeile ausrechnen (letzte_zeile = aufloesung.y * anzahl_x;) und bei jedem Punkt der Fläche hinzuaddieren. Ansonsten bleibt alles genauso wie bei Seitenfläche 1.

Nun kommen wir zu den anderen beiden Seitenflächen. Für diese müssen wir nicht entlang der äußeren Zeilen, sondern entlang der äußeren Spalten laufen:

module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
	/* ... */

    anzahl_x = aufloesung.x + 1;

	/* ... */

    anzahl_parabelpunkte = len( parabelpunkte );
    
	/* ... */

    seiten_flaechen_3 = [
        for ( y = [0 : aufloesung.y - 1] )
        [ 
            y      * anzahl_x, 
           (y + 1) * anzahl_x, 
           (y + 1) * anzahl_x + anzahl_parabelpunkte, 
            y      * anzahl_x + anzahl_parabelpunkte 
        ]
    ];

    letzte_spalte = aufloesung.x;
    seiten_flaechen_4 = [
        for ( y = [0 : aufloesung.y - 1] )
        [ 
            letzte_spalte +  y      * anzahl_x, 
            letzte_spalte + (y + 1) * anzahl_x, 
            letzte_spalte + (y + 1) * anzahl_x + anzahl_parabelpunkte, 
            letzte_spalte +  y      * anzahl_x + anzahl_parabelpunkte
        ]
    ];
    
}
/* ... */

Insgesamt sehen wir bei den Seitenflächen 3 und 4 das gleiche Schema wie bei den Seitenflächen 1 und 2. Seitenflächen 4 unterscheidet sich von Seitenflächen 3 nur dadurch, dass wir die Nummern um die Startnummer der letzten Spalte verschoben haben. Während die Nummern der Punkte entlang der X-Richtung den Abstand 1 haben, haben die Nummern der Punkte entlang der Y-Richtung den Abstand anzahl_x.

Hinter die Logik der Nummerierung der Punkte zu steigen ist gerade am Anfang nicht einfach. Es hilft sehr, sich die Geometrie auf einem Blatt Papier zu skizzieren und mit den Nummern der Punkte zu beschriften. Ganz so, wie es in Abbildung 11. zu sehen ist.

Nun ist der Punkt gekommen, wo wir endlich genügend Informationen zusammengetragen haben, um unsere Geometrie mit der Grundform polyhedron zu erzeugen:

module parabel( fokuspunkt, basis, aufloesung = [10, 10] ) {
    
	/* ... */

    polyhedron(
        points = concat( parabelpunkte, basispunkte ), 
        faces  = concat( parabel_flaechen, 
                         basis_flaechen, 
                         seiten_flaechen_1,
                         seiten_flaechen_2,
                         seiten_flaechen_3,
                         seiten_flaechen_4)
    );
    
}
/* ... */

Wir nutzen die concat-Funktion, um unsere Punkt- und Flächenmengen aneinanderzuhängen und übergeben sie als Parameter points (dt. Punkte) bzw. faces (dt. Flächen) an die 3D-Grundform polyhedron. Wenn wir nun eine Vorschau berechnen (F5) sollten wir unsere Parabel endlich sehen können.

Es gibt sicherlich Anwendungsfälle, bei denen man nicht den vollen Körper unterhalb der Parabel haben möchte. Wir können unser Modul parabel an einer Stelle anpassen, um die Möglichkeit zu schaffen, nur die Parabeloberfläche selbst mit einer gegebenen Dicke zu erzeugen:

module parabel( fokuspunkt, basis, aufloesung = [10, 10], dicke = 0 ) {
    
    /* ... */

    basispunkte = [
        for ( 
            y = [0 : basis.y / aufloesung.y : basis.y + 0.1], 
            x = [0 : basis.x / aufloesung.x : basis.x + 0.1] 
        ) 
        let ( p = parabelpunkt( fokuspunkt, [x,y,0] ) )
        if (dicke > 0)
            [x, y, p.z - dicke]
        else
            [x, y, 0]
    ];

    /* ... */
    
}
/* ... */

Für diese Erweiterung geben wir unserem Modul parabel einen weiteren Parameter dicke, der den Standardwert 0 erhält. Innerhalb des Moduls ändern wir die Berechnung der Basispunkte leicht ab. Mittels let berechnen wir erneut den aktuellen Oberflächenpunkt der Parabel und halten diesen in der Variable p vor. Im Anschluss führen wir eine Fallunterscheidung durch. Wenn der Parameter dicke größer null ist, dann erzeugen wir einen Basispunkt, der genau dicke vom Parabelpunkt p entfernt ist. Ansonsten, wenn dicke gleich 0 ist, erzeugen wir den Basispunkt wie gewohnt auf der X-Y-Ebene.

Abbildung 11.: Parabelform ohne Unterbau

Durch diese Änderung können wir nun auch reine Parabeloberflächen ohne Unterbau erzeugen (Abbildung 11.).

Die 3D-Grundform polyhedron ist sicherlich nicht die erste Wahl, wenn man eine 3D-Geometrie mit OpenSCAD beschreiben möchte. Die Verwendung von polyhedron erfordert einiges an Vorüberlegungen und ist nicht frei von subtilen Problemen. So kann es sein, dass Flächen nicht korrekt angezeigt werden. Dies hat im Allgemeinen zwei mögliche Ursachen. Wenn die Anzeigefehler nur in der Vorschau (F5) auftreten, nicht jedoch bei der tatsächlichen Berechnung (F6), dann kann eine Erhöhung des Parameters convexity der 3D-Grundform polyhedron helfen. Standardmäßig hat dieser Parameter den Wert 1.

Die zweite Ursache für einen Anzeigefehler ist etwas mühsamer zu beheben. Die Flächen, die sie definiert haben, sind nämlich nur aus einer Richtung sichtbar. Würde man aus dem Inneren des Objektes nach Außen schauen, so könnte man durch die Flächen hindurchsehen. Welche Seite der Flächen nun sichtbar und welche nicht sichtbar ist, hängt von der Reihenfolge der Punkte ab, mit denen wir eine Fläche aufbauen. Eine Fläche mit den Punkten [0, 1, 2, 3] oder den Punkten [3, 2, 1, 0] unterscheidet sich also dadurch, ob nun Vorderseite oder Rückseite sichtbar sind. Ein gutes Hilfsmittel, um solchen Problemen zu begegnen, besteht darin, sich eine Funktion flip zu definieren, die die Reihenfolge der Elemente in einem vierdimensionalen Vektor umdreht:

function flip(vec) = [ vec[3], vec[2], vec[1], vec[0] ];

Wenn dann die Darstellung einer Menge von Flächen nicht in Ordnung ist, kann man bei der Definition der Flächen die Funktion flip zum Einsatz bringen und schauen, ob sich dadurch das Problem lösen lässt. Dies sähe zum Beispiel so aus:

    seiten_flaechen_1 = [
        for ( x = [0 : aufloesung.x - 1] )
        flip ([ 
            x, 
            x + 1, 
            x + 1 + anzahl_parabelpunkte, 
            x     + anzahl_parabelpunkte 
        ])
    ];

Insgesamt wird man die polyhedron-Grundform vermutlich eher selten verwenden. Für den Fall der Fälle stellt sie jedoch ein mächtiges Werkzeug dar, mit dem man schwierige Modellierungsaufgaben lösen kann.

Tipps für den 3D-Druck

Je weiter der Fokuspunkt von der Parabelform entfernt ist, desto flacher wird die Form ausfallen. In so einem Fall sollte man versuchen, die Parabel senkrecht stehend zu drucken. Hierdurch wird die Oberfläche der Parabel feiner, da 3D-Drucker üblicherweise eine höhere Auflösung in der X-Y-Ebene als in der Z-Achse haben.

Benötigen sie die Parabel mit einer anderen Außenform, z.B. rund, dann erhalten Sie diese auf einfache Weise, wenn sie die Boolesche Operation intersection mit einer passenden Grundform, z.B. einem Zylinder, verwenden.

Download der OpenSCAD-Datei dieses Projektes

Projekt 10: Lüfterrad

In diesem Projekt wollen wir ein parametrisierbares Lüfterrad konstruieren, bei dem insbesondere der Anstellwinkel und die Verdrehung der Blätter einstellbar sind.

Abbildung 12.: Ein parametrisierbares Lüfterrad

Was ist neu?

Wir werden eine neue Seite der linear_extrude-Transformation kennenlernen und die mathematischen Funktionen abs und atan nutzen.

Los geht’s

Beginnen wir mit der Definition eines Hauptmoduls luefterrad, einer Reihe von Parametern und einem Untermodul blatt, in dem wir die Geometrie eines einzelnen Blattes des Lüfterrads beschreiben werden:

module luefterrad(
    blaetter        = 8,
    aussenradius    = 55,    
    innenradius     = 15,
    aussenbreite    = 50,
    innenbreite     = 20,
    materialstaerke = 2,    
    twist           = -30,    
    anstellwinkel   = 45,
){

    module blatt() {

    }

    blatt(); // debug
}

luefterrad();

Bis auf den Parameter blaetter, der die Anzahl der Lüfterblätter bestimmt, beziehen sich alle anderen Parameter auf geometrische Eigenschaften eines einzelnen Lüfterblattes. Abbildung 12. zeigt schematisch, welche Eigenschaften eines Blattes die Parameter aussenradius, innenradius, aussenbreite und innenbreite bestimmen. Die Werte innenhoehe und blatthoehe in Abbildung 12. werden wir später berechnen. Die Parameter twist und anstellwinkel beeinflussen die Verwindung des Lüfterblatts sowie den Winkel des Blatts an der Lüfternabe. Der Parameter materialstaerke bestimmt die Dicke des Lüfterblatts.

Abbildung 12.: Maße eines Blattes

Innerhalb des Moduls luefterrad haben wir das Untermodul blatt sowie eine Testinstanz dieses Moduls angelegt. Lassen Sie uns mit der Geometriebeschreibung eines einzelnen Blattes anfangen:

module luefterrad(
	/* ... */
){

    module blatt() {

        innenhoehe = 
            cos( 
                asin( innenbreite / ( 2 * innenradius ) ) 
            ) * innenradius;
        
        blatthoehe = aussenradius - innenhoehe;

        translate( [0, innenhoehe, 0] )
        rotate( [-90, 0, 0] )
        linear_extrude( 
            height    = blatthoehe, 
            twist     = twist, 
            slices    = 10 , 
            convexity = 10
        )
        square( [aussenbreite, materialstaerke], center = true );

    }

    blatt(); // debug
}
/* ... */

Wir berechnen zunächst die Werte innenhoehe und blatthoehe (Abbildung 12.). Die innenhoehe ist der Punkt, wo sich die Senkrechte mit Abstand innebreite / 2 vom Ursprung und der Kreis mit innenradius treffen. Um die innenhoehe auszurechnen brauchen wir den Winkel zwischen Radius und Senkrechte in diesem Schnittpunkt (asin( innenbreite / ( 2 * innenradius ) )). Anschließend können wir die innenhoehe mit Hilfe des Cosinus und des innenradius bestimmen. Die blatthoehe ist der Rest der Distanz von innenhoehe zum aussenradius.

Abbildung 12.: Extrudiertes und verdrehtes Rechteck

Das Blatt selbst modellieren wir mittels der 2D-Grundform square und der linearen Extrusion. Neu ist, dass wir nun den Parameter twist benutzen, um die Grundform während der Extrusion zu drehen (Abbildung 12.). Mit dem Parameter slices (dt. Scheiben) kann man einstellen, wie viele Unterteilungen linear_extrude entlang der Höhe vornehmen soll, um die Verdrehung abzubilden. Der Parameter convexity ist ein Hilfsparameter, der dafür sorgt, dass die Vorschau der Geometrie sauber berechnet wird und keine Löcher hat. Standardmäßig hat dieser Parameter den Wert 1. Sollte man in der Vorschau Fehler in der Geometrie entdecken, lohnt es sich, diesen Parameter zu vergrößern. Ein Wert von 10 sollte in praktisch allen Fällen ausreichend sein.

Nun werden wir die zulaufende Form des Lüfterblatts mittels einer Booleschen Differenzoperation beschreiben:

module luefterrad(
	/* ... */
){

    module blatt() {

		/* ... */

        // maße der Seitenteile
        seitenwinkel = 
            atan( 
                  ( ( aussenbreite / 2 * cos( abs( twist ) ) ) - 
                    ( innenbreite / 2 ) 
                  ) / blatthoehe 
            );    
        
        hoehe  = sin( abs( twist ) ) * aussenbreite / 2 + 
                 materialstaerke;
        
        breite = aussenbreite / 2 - innenbreite / 2;
        
        tiefe  = 
            sqrt( 
                pow( ( aussenbreite / 2 * cos( abs( twist ) ) ) - 
                     ( innenbreite / 2 ), 2 ) + 
                pow( blatthoehe, 2 )
            ) + materialstaerke;


        difference() {
            
            translate( [0, innenhoehe, 0] )
            rotate( [-90, 0, 0] )
            linear_extrude( 
                height    = blatthoehe, 
                twist     = twist, 
                slices    = 10,
                convexity = 10
            )
            square( [aussenbreite, materialstaerke], center = true );
            
            for (i = [0 : 1])
            mirror( [i, 0, 0] )
            translate([
                innenbreite / 2,
                innenhoehe,
                -hoehe
            ])
            rotate( [0, 0, -seitenwinkel] )
            translate( [0, -tiefe / 2, 0] )
            cube( [breite * 2, tiefe * 1.5, 2 * hoehe] );
            
        } // difference

    }

    blatt(); // debug
}
/* ... */

Wir berechnen zunächst den Winkel der Seite über die Arkustangensfunktion atan. Diese bekommt als Parameter den Überstand zwischen innenbreite und der um twist gedrehten aussenbreite. Wir verwenden hier den Absolutwert (abs) von twist, um auch bei einem negativen Verwindungsparameter das gleiche Ergebnis zu erhalten. Anschließend bestimmen wir noch die minimal notwendige hoehe, breite und tiefe des Quaders, den wir vom Lüfterblatt abziehen wollen.

Abbildung 12.: Abschneiden der Seitenteile mittels Boolescher Differenz führt zu zackigen Kanten

Nach diesen Vorbereitungen können wir nun unser Lüfterblatt in die Menge einer Booleschen Differenzoperation verschieben (difference) und zwei Quader definieren, die wir vom Lüfterblatt abziehen. Da die Seiten spiegelsymmetrisch sind, können wir die zwei Quader mittels einer Kombination von For-Schleife und mirror-Transformation erzeugen. Den abzuziehenden Quader machen wir etwas größer als es minimal notwendig wäre. Durch diese Zugabe decken wir auch Situationen ab, die durch eine “extreme” Parametrisierung des Lüfterrads entstehen können (z.B. bei einer sehr kleinen aussenbreite). Abbildung 12. zeigt die Position der beschriebenen Quader. Sie zeigt auch, dass nun die Seitenkanten unseres Lüfterblatts eine zackige Struktur bekommen haben. Diese zackige Struktur rührt daher, dass die 2D-Grundform unseres Blattes ein sehr dünnes Rechteck ist, welches intern nur aus zwei Dreiecken gebildet wird. Diese reichen nun nicht mehr aus, um die jetzt schrägen Seitenkanten des Lüfterblatts sauber zu beschreiben.

Wir können dieses Problem beheben, indem wir anstatt eines einfachen Rechtecks ein Polygon als 2D-Grundform verwenden, dass ausreichend viele Zwischenschritte macht. Dabei müssen wir einen kleinen Trick anwenden:

module luefterrad(
	/* ... */
){

    module blatt() {

		/* ... */

        blattunterteilung = 10;

        difference() {
            
            translate( [0, innenhoehe, 0] )
            rotate( [-90, 0, 0] )
            linear_extrude( 
                height    = blatthoehe, 
                twist     = twist, 
                slices    = 10,
                convexity = 10
            )
            translate([
                -aussenbreite / 2,
                -materialstaerke / 2
            ])
            polygon( concat(
                [for (i = [0 : blattunterteilung]) 
                    [i * aussenbreite / blattunterteilung, (i % 2) * 0.0001]
                ],
                [for (i = [blattunterteilung : -1 : 0]) 
                    [i * aussenbreite / blattunterteilung, 
                     materialstaerke + (i % 2) * 0.0001]
                ]
            ));
            
			/* ... */
            
        } // difference

    }

    blatt(); // debug
}
/* ... */

Wir erstellen das Polygon aus zwei aneinandergehängten (concat) Feldern. Das erste Feld beschreibt die Punkte des Polygons in X-Richtung auf Höhe 0. Das zweite Feld beschreibt die Punkte des Polygons in entgegengesetzter X-Richtung auf Höhe materialstaerke. Wenn alle unsere Punkte in Hin- und Rückrichtung exakt auf einer Linie liegen, dann wird OpenSCAD diese Punkte wegoptimieren. Damit dies nicht geschieht, müssen wir jeden zweiten Punkt minimal in Y-Richtung verschieben. Wir nutzen die Modulo-Operation für diesen Zweck. Der Ausdruck i % 2 wird für fortlaufende i abwechselnd 0 und 1. Unsere Y-Koordinate springt daher von Punkt zu Punkt zwischen 0 und 0.0001 hin und her. Hierdurch verhindern wir die Optimierung durch OpenSCAD und erzielen den gewünschten Zweck einer höheren geometrischen Auflösung unserer 2D-Grundform. Abbildung 12. zeigt, wie hierdurch die zackigen Seiten des Lüfterblatts vermieden werden.

Abbildung 12.: Reparatur der zackigen Kanten mittels Polygon

Unser Lüfterblatt ist nun fast fertig. Wir müssen es nur noch um seinen anstellwinkel drehen und den Außen- sowie Innenradius der Geometriebeschreibung hinzufügen. Für den Außenradius können wir dies mit der Booleschen Schnittoperation (intersection) und für den Innenradius mit einer weiteren Booleschen Differenzoperation (difference) erreichen:

module luefterrad(
	/* ... */
){

    module blatt() {

		/* ... */

        difference(){

            intersection(){

                rotate( [0, -anstellwinkel, 0] )
			    difference() {
                        
					/* ... */
            
        		} 

        		// Aussenradius
                translate( [0, 0, -hoehe] )
                cylinder( 
                    r = aussenradius, 
                    h = 2 * hoehe, 
                    $fn = 100
                );

            }
            
            // Innenradius
            translate( [0, 0, -hoehe] )
            cylinder( r = innenradius, h = 2 * hoehe, $fn = 50);

        }

    }

    blatt(); // debug
}
/* ... */

Wir wenden die Schnitt- und Differenzoperation erst nach der Drehung um anstellwinkel an, damit die Außen- und Innenkanten des Lüfterblatts unabhängig vom anstellwinkel senkrecht bleiben. Damit ist unser Untermodul blatt vollständig beschrieben (Abbildung 12.).

Abbildung 12.: Fertiges Lüfterblatt von oben und in der Perspektive

Die Fertigstellung des Hauptmoduls ist nun relativ überschaubar:

module luefterrad(
	/* ... */
){

    module blatt() {

    }

    // nabe
    naben_hoehe = 
        sin( abs( anstellwinkel ) ) * innenbreite / 2 + 
        materialstaerke;
    
    translate( [0, 0, -naben_hoehe] )
    cylinder( r = innenradius, h = naben_hoehe * 2, $fn = 50);

    // blaetter
    for(i = [0 : 360 / blaetter : 359])
    rotate( [0, 0, i] )
    blatt();

}
/* ... */

Wir bestimmen zunächst die Höhe unserer Nabe (naben_hoehe) in Abhängigkeit des Anstellwinkels, der Innenbreite und der Materialstärke. Anschließend erstellen wir die Nabe als einfachen Zylinder (cylinder) und zentrieren diesen Zylinder entlang der Z-Achse (translate). Die Blätter beschreiben wir nun mittels einer For-Schleife, bei der sich die Schrittweite der Schleifenvariablen i aus der parametrisierten Anzahl der Blätter ergibt (360 / blaetter). Wir nutzen die Schleifenvariable i, um unser Untermodul blatt mit entsprechend vielen Kopien über 360 Grad hinweg mittels Rotation um die Z-Achse (rotate( [0, 0, i] )) zu verteilen.

Abbildung 12.: Fertiges Lüfterblatt von oben und in der Perspektive

Damit ist unser Lüfterrad fertig (Abbildung 12.)!

Tipps für den 3D-Druck

Die Blätter des Lüfterrads haben insbesondere nach außen hin einen starken Überhang. Hier empfehlt es sich, mit einer geringen Schichtdicke (engl. layer height) und größerer Linienbreite (line width) zu drucken. Wenn ihr 3D-Drucker eine 0.4mm Druckdüse besitzt, können sie ohne Probleme Linien mit einer Breite von 0.5mm bis 0.6mm drucken. Als Schichtdicke kann man bei den meisten Druckern bis auf 0.075mm runter gehen. Ein weiterer Trick ist die Verlangsamung der Druckgeschwindigkeit für die äußeren Wandungen des Drucks (engl. outer wall speed). Hierdurch kann das Filament stärker abkühlen, da es länger dem Gebläse des Druckkopfes ausgesetzt ist.

Wenn Sie das Lüfterrad mit Stützstrukturen (engl. support) drucken, dann sollten Sie die Option für die Ausbildung eines support roofs aktivieren. Damit wird die Trennschicht zwischen Stützstruktur und 3D-Modell sauberer. Eine Alternative zur Verwendung von Stützstrukturen besteht darin, das Lüfterrad mit einer Booleschen Schnittoperation in eine obere und eine untere Hälfte zu zerteilen:

// obere Hälfte

intersection() {
    luefterrad();
    
    translate([-100,-100,0])
    cube([200,200,100]);
}


// untere Hälfte

translate([120,0,0])
rotate([180,0,0])
intersection() {
    luefterrad();
    
    translate([-100,-100,-100])
    cube([200,200,100]);
}

Nach dem Druck beider Hälften kann man diese dann miteinander Verkleben. Dies geht zum Beispiel mit Sekundenkleber und Aktivator, oder mit einem Zweikomponentenkleber. Um eine saubere Ausrichtung der beiden Hälften zu gewährleisten, kann es hilfreich sein, zwei Bohrungen mittels Boolescher Differenzoperation in die Nabe zu legen und beim verkleben zwei Stifte in diese Bohrungen einzusetzen und als Ausrichtungshilfen zu verwenden.

Download der OpenSCAD-Datei dieses Projektes

Was fehlt ?

In den vergangenen 10 Projekten haben wir fast den gesamten Funktionsumfang von OpenSCAD kennengelernt. Abbildungen 13., 13.2 und 13.3 liefern hierzu eine Übersicht. Im Folgenden werden wir uns kurz und knapp mit den Funktionen beschäftigen, die wir bislang außen vor gelassen haben.

Abbildung 13.: Übersicht der in den Projekten genutzten Funktionen. Teil 1 von 3
Abbildung 13.: Übersicht der in den Projekten genutzten Funktionen. Teil 2 von 3
Abbildung 13.: Übersicht der in den Projekten genutzten Funktionen. Teil 3 von 3

Geometriefunktionen

In Projekt 4 haben wir die import-Funktion genutzt, um .svg-Dateien als 2D-Grundform zu importieren. Die import-Funktion kann jedoch auch 3D-Daten in den Formaten .stl, .off, .amf und .3mf importieren. Insbesondere dann, wenn man eine komplexe und rechenintensive OpenSCAD-Geometrie in einem weiteren Projekt verwenden möchte, kann die Nutzung von import anstelle von include oder use sinnvoll sein. Auf diese Weise umgeht man in vielen Fällen eine erneute Berechnung der komplexen Geometrie. Der Nachteil ist natürlich, dass man die so importierte Geometrie nun nicht mehr direkt parametrisieren kann.


Die einzige Transformation, die wir nicht genutzt haben, ist die multmatrix-Transformation. Sie erlaubt es, eine affine Transformation der Geometrie über eine Transformationsmatrix durchzuführen. Die Matrix ist dabei eine 4 x 3 bzw. 4 x 4 Matrix deren Zellen die folgende Bedeutung haben:

M = [
	// die erste Zeile kümmert sich um X
	[ 
		1, // Skalierung in X-Richtung
	  	0, // Scherung von X entlang Y
	  	0, // Scherung von X entlang Z
	  	0, // Verschiebung entlang X
	],

	// die zweite Zeile kümmert sich um Y
	[ 
		0, // Scherung von Y entlang X
	  	1, // Skalierung in Y-Richtung
	  	0, // Scherung von Y entlang Z
	  	0, // Verschiebung entlang Y
	],

	// die dritte Zeile kümmert sich um Z
	[ 
		0, // Scherung von Z entlang X
	  	0, // Scherung von Z entlang Y
	  	0, // Skalierung in Z-Richtung 
	  	0, // Verschiebung entlang Z
	],

	// die vierte Zeile ist immer
	[0, 0, 0, 1]

];

multmatrix(M)
cube([10,10,10]);

Da die vierte Zeile der Matrix M fest ist, kann sie bei Nutzung von multmatrix auch weggelassen werden. Mit der entsprechenden Belegung der Einträge der Matrix M kann man jegliche Verschiebungen und Rotationen abbilden und mittels Matrixmultiplikation zusammenfassen. In der Praxis nutzt man multmatrix eher selten. Einzig die Möglichkeit, ein Objekt in eine bestimmte Richtung zu scheren, ist manchmal von nutzen.


Wir haben in den 10 Projekten an vielen Stellen For-Schleifen genutzt. Die so entstehenden Geometrien werden dabei implizit wie bei einer Booleschen Vereinigung zusammengefasst. Dies kann man gut am folgenden Beispiel sehen:

module test() {
    
    echo($children);
    
    children();
    
}

test() {
    sphere(5);
    
    translate( [10, 0, 0] )
    sphere(5);

    translate( [20, 0, 0] )
    sphere(5);
}

test()
for(i = [0:2])
translate( [10 * i, 10, 0] )
sphere(5);

Wir definieren ein Modul test, was uns in der Konsole die Anzahl der Elemente der nachfolgenden Geometrie ausgibt. Verwenden wir test mit einer Geometriemenge ({ ... }) erhalten wir im obigen Beispiel eine Ausgabe von 3. Verwenden wir test mit einer For-Schleife, dann erhalten wir als Ausgabe eine 1. Die Geometrien, die durch die For-Schleife definiert werden, werden also zu einer einzigen Geometrie zusammengefasst. Hierdurch fehlt die Möglichkeit, eine Menge von Geometrien mittels For-Schleife zu beschreiben und dann die Boolesche Operation intersection (dt. Schnitt) auf diese Geometriemenge anzuwenden. Genau für diesen Fall gibt es deshalb die intersection_for-Schleife in OpenSCAD. In der Praxis kommt diese Art der For-Schleife nur sehr selten zum Einsatz. Sie kann aber dazu dienen, interessante Geometrien zu erzeugen. Als Beispiel mag diese diamantartige Geometrie dienen:

M = [
    [1,0,1,0],
    [0,1,0,0],
    [0,0,1,0]
];  

intersection_for(i = [0:60:359])
rotate( [0, 0, i] )
multmatrix( M )
cube( 10, center = true );

Feldmethoden

Wir haben an vielen Stellen generative For-Schleifen genutzt, um Felder zu definieren. Innerhalb einer solchen generativen For-Schleife kann das Schlüsselwort each verwendet werden, um einen nachfolgenden Vektor “auszupacken”. Nehmen wir folgendes Beispiel:

vektoren = [
    [1,2,3],
    [4,5,6],
    [7,8,9]
];

flach = [ for (v = vektoren) each v];
    
echo( flach );

Das Feld flach ist im Ergebnis ein eindimensionales Feld, dass alle Einträge des zweidimensionalen Feldes vektoren enthält. Die Konsolenausgabe für flach ergibt daher ECHO: [1, 2, 3, 4, 5, 6, 7, 8, 9].


Die Funktion lookup erlaubt es, Werte in einem Feld von Zahlenpaaren linear zu interpolieren. Ein Beispiel:

werte = [
    [-3,  2],
    [ 0, 10],
    [ 1, 50],
    [ 4,  0]
];

echo( lookup(-1,   werte) );
echo( lookup( 0,   werte) );
echo( lookup( 0.5, werte) );
echo( lookup( 3,   werte) );

Die Konsolenausgabe hierzu sieht wie folgt aus:

ECHO: 7.33333
ECHO: 10
ECHO: 30
ECHO: 16.6667

Die Funktion lookup durchsucht das ihr übergebene Feld von Zahlenpaaren nach den zwei benachbarten Zahlenpaaren, deren jeweils erste Werte ein Intervall aufspannen, in dem der übergebene Suchparameter liegt. Anschließend werden die zweiten Werte dieser beiden Paare gewichtet gemittelt, so dass die Mittlung der Nähe des Suchparameters zu den jeweiligen ersten Werten entspricht.

Funktionen

Wir haben in unseren Projekten einen großen Teil der von OpenSCAD bereitgestellten Funktionen genutzt. Nicht genutzt haben wir die Funktionen str, sign, atan2, round, ceil, ln, log, min und max.

Die Funktion str erzeugt aus ihren Parametern eine einzelne, zusammenhängende Zeichenkette. Sie erinnern sich an die Variable srnd aus Projekt 8? Wir nutzten die Variable für die Initialisierung der Zufallszahlen und gaben mittels echo( srnd ); ihren Wert in der Konsole aus, um sie uns für eine spätere, manuelle Verwendung merken zu können. Wir hätten diese Konsolenausgabe mit der str-Funktion etwas lesbarer gestalten können, z.B. echo( str("Als Initialisierungswert wurde ", srnd, " genutzt.") );.

Die Funktion sign liefert das Vorzeichen einer Zahl zurück. Bei negativen Zahlen liefert sign eine -1, bei positiven Zahlen eine 1 und bei null eine 0 zurück.

Die Funktion atan2 ist eine Variante des Arkustangens. Der “normale” Arkustangens kann zwischen y/x und -y/-x nicht unterscheiden, da sich die Minuszeichen im zweiten Fall aufheben würden. Die Funktion atan2 löst dieses Problem, indem man y und x als getrennte Parameter an die Funktion übergeben kann.

Die Funktion round rundet den übergebenen Kommawert kaufmännisch. Die Funktion ceil rundet den übergebenen Wert immer auf.

Die Funktion ln liefert den natürlichen Logarithmus der übergebenen Zahl. Die Funktion log liefert den Logarithmus zur Basis 10.

Die Funktion min liefert als Ergebnis den kleinsten Wert der ihr übergebenen Parameter zurück. Besteht der Parameter aus nur einem einzigen Vektor, wir das kleinste Element innerhalb des Vektors zurückgegeben. Die Funktion max verhält sich analog. Sie liefert das jeweils größte Element zurück.

Systemmethoden

In seltenen Fällen kann es passieren, dass die Vorschau (F5) einer Geometrie fehlschlägt und es zu Artefakten kommt, denen man nicht mit anderen Mitteln (z.B. einem convexity Parameter) beikommen kann. Mit der Funktion render, die man jeder Geometrie wie eine Transformation voranstellen kann (z.B. render() sphere( 10 );), kann man OpenSCAD dazu zwingen, den entsprechenden Teil der Geometrie auch schon während einer Vorschau vollständig zu berechnen.

Ein anderer Einsatzzweck von render besteht darin, komplexe Geometrien bereits in der Vorschau in ein 3D-Gittermodell zu überführen und damit die Vorschau flüssiger zu machen. Dies kann insbesondere bei aufwendigen Booleschen Operationen hilfreich sein.


Die Funktion assert erlaubt die Prüfung von Annahmen und ermöglicht die Ausgabe einer entsprechenden Fehlermeldung, wenn diese Annahmen nicht erfüllt sind. Dies ist insbesondere dann nützlich, wenn wir Module entwickeln, die von Anderen als Library genutzt werden sollen. Nehmen wir mal an, wir erwarten einen dreidimensionalen Vektor als Eingabe. Dann könnte eine Prüfung wie folgt aussehen:

module mein_modul( groesse ) {

    assert( 
        len(groesse) == 3, 
        "Parameter groesse muss ein dreidimensionaler Vektor sein!" 
    );
}

Würden wir nun das Modul z.B. mit einer einfachen Zahl aufrufen, dann würden wir eine Fehlermeldung in der Konsole erhalten:

ERROR: Assertion '(len(groesse) == 3)' failed: "Parameter groesse muss ein dreidimensionaler Vektor sein!"

Die weiter unten beschriebenen Typfunktionen können in diesem Zusammenhang sehr nützlich sein.


Die Funktionen version und version_num liefern die aktuelle Version von OpenSCAD zurück. Die Funktion version liefert einen dreidimensionalen Vektor mit den einzelnen Versionsbestandteilen. Die Funktion version_num liefert eine einzelne Zahl, die die jeweilige Version eindeutig repräsentiert.

Eine derartige Versionsabfrage ist dann von Nutzen, wenn wir eine Geometriebibliothek entwickeln und Funktionen verwenden, die in früheren OpenSCAD-Versionen nicht vorhanden sind oder anders parametrisiert wurden. Durch entsprechende Fallunterscheidungen kann man es dann ggf. erreichen, dass die Bibliothek stabil über mehrere Versionen von OpenSCAD hinweg funktioniert.


Die Funktion parent_module liefert den Namen eines übergeordneten Moduls innerhalb einer Modulhierarchie, wenn alle Module innerhalb der Hierarchie mittels children auf nachgeordnete Elemente zugreifen. Das offizielle Handbuch von OpenSCAD gibt als Anwendungsbeispiel die Erzeugung einer Materialliste für eine gegebene Geometrie an. In der Praxis spielt diese Funktion keine Rolle.

Spezialvariablen

In unseren Projekten haben wir häufig die Spezialvariable $fn genutzt, um die Feinheit runder Geometrien einzustellen. Auf ähnliche Weise kann man die Feinheit runder Geometrien auch mittels der Variablen $fa und $fs steuern. Mit $fa kann man die minimalen Winkel einer Geometrie festlegen und mit $fs die minimale Größe der Teilflächen. In beiden Fällen erzeugen kleinere Werte eine feinere Geometrie. Standardmäßig steht $fa auf 12 und $fs auf 2.


Die Spezialvariablen $vpr, $vpt und $vpd liefern Informationen über die aktuelle Perspektive im Vorschaufenster wenn eine neue Vorschau berechnet wird. $vpr liefert die Rotation, $vpt die Verschiebung und $vpd die Distanz der Kamera.

Durch das Setzen dieser Variablen kann man die Perspektive im Anzeigefenster ändern. Dies könnte man zum Beispiel für die Erzeugung von Kamerafahrten im Zusammenhang mit den Animationsfähigkeiten von OpenSCAD nutzen.


Die Spezialvariable $preview liefert true, wenn eine Vorschau (F5) berechnet wird und false, wenn eine vollständige Geometrieberechnung (F6) ausgeführt wird.

Typfunktionen

OpenSCAD stellt eine Reihe von Funktionen zur Verfügung, mit denen der Typ einer Variable geprüft werden kann. Diese Prüfung ist insbesondere im Zusammenhang mit der assert-Funktion von nutzen, oder für den Fall, dass man einen Parameter mit einer doppelten Funktionalität ausstatten möchte. Ein existierendes Beispiel hierfür ist der scale-Parameter der linear_extrude-Transformation. Der Parameter kann entweder eine Zahl oder ein Vektor sein. Ist er eine Zahl, wird die zugrunde liegende 2D-Form in alle Richtungen gleichmäßig skaliert. Ist der Parameter ein Vektor, kann für die X- und Y-Richtung eine jeweils individuelle Skalierung angegeben werden.

Die zur Verfügung stehenden Typfunktionen sind: is_undef, is_bool, is_num, is_string, is_list und (in zukünftigen OpenSCAD-Versionen) is_function. Mit list sind hier jegliche Art von Feldern bzw. Vektoren gemeint.

Debugzeichen

Neben den bekannten Debugzeichen ! und # gibt es auch noch das *-Zeichen und das %-Zeichen. Mit dem *-Zeichen kann man einen Geometrieteil “abschalten”. Es stellt also eine Alternative zum auskommentieren dar. Mit dem %-Zeichen kann man einen Geometrieteil transparent schalten.


Mit dieser abschließenden Kurzzusammenfassung kennen Sie nun alle OpenSCAD-Funktionen!

Kurzreferenz

Zum Abschluss gibt es an dieser Stelle eine kompakte Kurzreferenz der wichtigsten OpenSCAD-Funktionen und ihrer Parameter.

2D Geometrie

Kreis

circle(
	r,  	// Radius	
	d   	// oder Durchmesser
);

Rechteck

square(
	size,		// ein einzelner Wert erzeugt ein Quadrat,
	            // ein zweidimensionaler Vektor ein Rechteck

	center  	// wenn "true", dann wird das Rechteck über 
				// dem Ursprung zentriert
);

Polygon

polygon(
	points,		// Feld mit zweidimensionalen Vektoren als Punktmenge

	paths,      // Optionales Feld mit Punktindizes, wenn das Polygon
	            // auch innere, negative Flächen besitzt

	convexity   // Optimierungsparameter für die Vorschau. Bei Darstellungs-
				// problemen sukzessive erhöhen. Ein Wert von 10 sollte passen.
);

Text

text(
	text,		// Der darzustellende Text als Zeichenkette

	size,	   	// Die Höhe "normaler" Buchstaben

	font,		// Die Schriftart als Zeichenkette (s. Projekt 6)

	halign,     // Die horizontale Ausrichtung als Zeichenkette
				// Mögliche Werte: "left", "center", "right"

	valign,     // Die vertikale Ausrichtung als Zeichenkette
				// Mögliche Werte: "top", "center", "baseline", "bottom"
				// "baseline" bezeichnet die Grundlinie

	spacing,	// Faktor, um den Buchstabenabstand zu verändern

	direction,	// Schreibrichtung. Mögliche Werte: 
				// "ltr" -> von links nach rechts
				// "rtl" -> von rechts nach links
				// "ttb" -> von oben nach unten
				// "btt" -> von unten nach oben

	language,	// Sprache. Englisch "en", Deutsch "de", ...

	script 		// Textart. Standard ist "latin"
);

Importieren von Geometrien

import(
	file,		// Dateinamen einer `.svg`- oder `.dxf`-Datei

	convexity,  // Optimierungsparameter für die Vorschau. Bei Darstellungs-
				// problemen sukzessive erhöhen. Ein Wert von 10 sollte passen.

	layer		// Folienname falls es eine `.dxf`-Datei ist
);

Projektion

projection(
	cut		// wenn "true", dann wird nachfolgende Geometrie in der X-Y-Ebene
			// geschnitten. Ansonsten wird die nachfolgende Geometrie auf die
			// X-Y-Ebene projiziert
);

3D Geometrie

Kugel

sphere(
	r,		// Radius
	d 		// oder Durchmesser
);

Quader

cube(
	size,		// ein einzelner Wert erzeugt einen Würfel
				// ein dreidimensionaler Vektor erzeugt einen Quader

	center 		// wenn "true", dann wir der Quader im Ursprung zentriert
);

Zylinder

cylinder(
	h,			// Höhe des Zylinders

	r,			// Radius, oder alternativ:
	r1,			// unterer Radius und	
	r2,			// oberer Radius des Zylinders

	d,			// Durchmesser, oder alternativ:
	d1,			// unterer Durchmesser und 
	d2,			// oberer Durchmesser des Zylinders

	center 		// wenn "true", dann wird der Zylinder entlang der
				// Z-Achse im Ursprung zentriert
);

Polyhedron

polyhedron(
	points,		// Feld mit dreidimensionalen Vektoren

	faces,		// Feld mit vierdimensionalen Vektoren, die per Indizes 
				// jeweils vier Punkte aus dem Feld points zu einer 
				// Fläche zusammenfassen

	convexity   // Optimierungsparameter für die Vorschau. Bei Darstellungs-
				// problemen sukzessive erhöhen. Ein Wert von 10 sollte passen.
);

Importieren von Geometrien

import(
	file,		// Dateinamen einer `.stl`, `.off`, `.amf` oder `.3mf`-Datei

	convexity   // Optimierungsparameter für die Vorschau. Bei Darstellungs-
				// problemen sukzessive erhöhen. Ein Wert von 10 sollte passen.
);

Oberfläche aus Bilddaten

surface(
	file,		// Dateinamen einer `.png`-Datei oder Textdatei

	center,		// wenn "true", wird das erzeugte Objekt im Ursprung zentriert

	invert,     // wenn "true", werden die Farben invertiert

	convexity   // Optimierungsparameter für die Vorschau. Bei Darstellungs-
				// problemen sukzessive erhöhen. Ein Wert von 10 sollte passen.
);

Transformationen

Lineare Extrusion

linear_extrude(
	height,		// Höhe der Extrusion

	center,		// wenn "true", wird die Extrusion entlang der Z-Achse zentriert

	twist,		// Verdrehung um die Z-Achse in Grad

	slices,		// Anzahl der Schichten entlang der Z-Achse

	scale,		// Skalierung entlang der Extrusion. Entweder ein einzelner
	            // Wert oder ein zweidimensionaler Vektor, der die Skalierung
	            // in X- und Y-Richtung separat definiert

	convexity   // Optimierungsparameter für die Vorschau. Bei Darstellungs-
				// problemen sukzessive erhöhen. Ein Wert von 10 sollte passen.
) 
eine_2D_Form();

Rotationsextrusion

rotate_extrude(
	angle,		// Winkel der Extrusion um die Z-Achse

	convexity   // Optimierungsparameter für die Vorschau. Bei Darstellungs-
				// problemen sukzessive erhöhen. Ein Wert von 10 sollte passen.
)
eine_2D_Form();

Verschiebung

translate(
	v 		// ein zwei- oder dreidimensionaler Vektor, der die Verschiebung 
			// relativ zum Ursprung beschreibt
)
eine_Geometrie();

Rotation

rotate(
	a,		// entweder ein einzelner Drehwinkel (in Kombination mit v)
			// oder ein dreidimensionaler Vektor, der die Drehwinkel
			// um die X-, Y- und Z-Achse beschreibt

	v 		// dreidimensionaler Vektor, der die Drehachse beschreibt,
			// wenn in a nur ein Winkel angegeben wird
)
eine_Geometrie();

Skalierung

scale(
	v 		// zwei- oder dreidimensionaler Vektor, der die Skalierung 
			// der Geometrie in X-, Y- und Z-Richtung beschreibt
)
eine_Geometrie();

Neubemessung

resize(
	newsize 	// zwei- oder dreidimensionaler Vektor, der die neuen
				// Abmaße der Geometrie in X-, Y- und Z-Richtung beschreibt
)
eine_Geometrie();

Spiegelung

mirror(
	v 		// zwei- oder dreidimensionaler Vektor, der die Achse angibt,
			// entlang derer die Geometrie gespiegelt werden soll
)
eine_Geometrie();

Farbe

color(
	c,		// entweder ein vierdimensionaler Vektor, der die Farbe als
			// RGBA mit Werten von 0 bis 1 beschreibt; oder 
			// ein dreidimensionaler Vektor, der die Farbe als RGB mit
			// Werten von 0 bis 1 beschreibt; oder
			// eine Zeichenkette, die die Farbe in Hexadezimal mit 
			// führendem '#'-Zeichen beschreibt; oder
			// eine Zeichenkette, die mit einem Farbnamen wie z.B.
			// "red" die Farbe beschreibt

	alpha	// ein Transparenzwert von 0 (transparent) bis 1 (opak)
)
eine_Geometrie();

Konvexe Hülle

hull(){
	eine_Geometrie(); 	// Es wird über alle Geometrien der Geometrie-
						// menge die konvexe Hülle gebildet.

	eine_Geometrie();

	...
}

Ausdehnung und Schrumpfung

offset(
	r,			// Ausdehnung in Form eines Kreises mit Radius r, der
				// entlang der 2D-Form bewegt wird. Alternativ:

	delta,		// Distanz der neuen Außenlinie von der ursprünglichen
				// Linie. Im Gegensatz zu 'r' werden hier Spitzen nicht
				// abgerundet

	chamfer		// wenn "true", werden Ecken abgeflacht (nur bei delta)
)
eine_2D_Form();

Minkowski-Summe

minkowski() {
	eine_Geometrie();	// Grundgeometrie

	eine_Geometrie(); 	// Geometrie, die an jeden Punkt der Grund-
						// geometrie kopiert wird, um diese mit den
						// Punkten der kopierten Geometrie zu erweitern
}

Affine Transformation

multmatrix(
	m 		// Eine 4x3 oder 4x4 Transformationsmatrix, die die affine
			// Transformation der Geometrie beschreibt
)
eine_Geometrie();

Boolesche Operationen

Vereinigung

union(){
	eine_Geometrie(); 	// Es wird die Boolesche Vereinigung aller 
						// Geometrien der Geometriemenge gebildet.

	eine_Geometrie();

	...
}

Differenz

difference(){
	eine_Geometrie(); 	// Grundgeometrie

	eine_Geometrie(); 	// Alle der Grundgeometrie folgenden Geometrien
						// der Geometriemenge werden von der Grundgeometrie
						// abgezogen

	...
}

Schnitt

intersection(){
	eine_Geometrie(); 	// Es wird der Boolesche Schnitt aller 
						// Geometrien der Geometriemenge gebildet.

	eine_Geometrie();

	...
}

Schleifen

For-Schleife

for ( i = [start : schritt : stop] )   // Schleifenvariable i läuft von 
eine_Geometrie();                      // start bis stop mit Schrittweite
									   // schritt (optional)

for ( v = feld )                       // Schleifenvariable v läuft durch
eine_Geometrie();                      // alle Elemente des Feldes

for ( i ... , j ... )                  // Mehrere Schleifenvariablen 
eine_Geometrie();                      // werden durch Kommata getrennt

For-Schleife (Boolescher Schnitt)

intersection_for( ... )		// gleiche Verwendung wie For-Schleife,
eine_Geometrie();			// jedoch mit Booleschem Schnitt der
							// Objekte anstelle einer Booleschen
							// Vereinigung

Generative For-Schleife

feld = [ for (i = ...) i ];		// Erzeugt ein Feld mittels For-Schleife

Mathematische Funktionen

absoluter Wert:		abs()
Vorzeichen:			sign()
Sinus:				sin()
Cosinus:			cos()
Tangens:			tan()
Arkussinus:			asin()
Arkuscosinus:		acos()
Arkustangens:		atan()
Arkustangens 2:		atan2()
Abrunden			floor()
Runden				round()
Aufrunden			ceil()
Natürlicher Logarithmus		ln()
Logarithmus zur Basis 10	log()
Exponentialfunktion			exp()
Potenz				pow()
Wurzel				sqrt()
Minimum				min()
Maximum				max()
Länge eines Vektors				norm()
Kreuzprodukt zweier Vektoren	cross()

Sonstige Funktionen

Anzahl Elemente eines Feldes 		len()
Anzahl Buchstaben in Zeichenkette	len()
Felder aneinanderhängen				concat()
Parameter zu einer Zeichenkette		str()
Suche in Feldern					search()
Zeichenkonvertierung				chr(), ord()

Lokale Variablendefinition

let (x = ...) { 	

}

Zufallszahlen

feld = rands (
	min_value,		// kleinster Zufallswert
	max_value,		// größter Zufallswert
	value_count,    // Anzahl an Zufallszahlen
	seed_value      // Initialwert (optional)
)

Zugriff auf nebenstehende Geometriemenge

module test() {

	$children			// Anzahl der Elemente in 
						// Geometriemenge

	children( i );		// i-te Geometrie der
						// Geometriemenge.
						// Falls der Parameter fehlt 
						// werden alle Geometrien
						// genommen.

}