From 7d4884946fff765fda5bcb4df92ad46768886d5c Mon Sep 17 00:00:00 2001 From: MrGeorgen Date: Thu, 13 Jan 2022 22:12:01 +0100 Subject: [PATCH] comments --- lindenmayer/__main__.py | 19 ++++- lindenmayer/drawer.py | 77 +++++++++++++++----- lindenmayer/inputHelper.py | 142 +++++++++++++++++++++++++------------ lindenmayer/lSystems.py | 12 +++- 4 files changed, 183 insertions(+), 67 deletions(-) diff --git a/lindenmayer/__main__.py b/lindenmayer/__main__.py index 291a8aa..bfeee71 100644 --- a/lindenmayer/__main__.py +++ b/lindenmayer/__main__.py @@ -1,12 +1,27 @@ import inputHelper import turtle import drawer +import sys +def quit(): + print() # Letzte Zeile wird abgeschlossen + sys.exit() + +# Dummy-Turtle, die erzeugt wird damit sich das Fenster öffnet und getestet werden kann, +# ob der Nutzer eine korrekte Farbeingabe gemacht hat. Allerdings wird mit dieser Turtle nichts gezeichnet. turtleObject = drawer.newTurtle() screen = turtleObject.screen -screen.colormode(255) +screen.colormode(255) # ändert den Farbmodus auf RGB mit 24 Bits +# Programm wird geschlossen, wenn das Fenster geschlossen wird +# http://ostack.cn/?qa=1086384/how-to-detect-x-close-button-in-python-turtle-graphics +screen.getcanvas().winfo_toplevel().protocol("WM_DELETE_WINDOW", quit) try: inputHelper.takeInput(turtleObject) +# Die Python integrierte "input"-Funktion wirft diesen Fehler, wenn der Nutzer die Tastenkombi Strg-D +# auf einem Unix-System verwendet. Es ist üblich, dass das Programm daraufhin beendet wird. +# Wir folgen dieser Konvention und beenden das Programm wobei die Fehlermeldung abgefangen wird, +# damit der Nutzer nicht von einer Fehlermeldung verwirrt wird, wenn dieser das Programm absichtlich +# beendet. EOF-Fehler von anderen Ein- oder Augabeoperationen werden bereits voher abgefangen. except EOFError: quit() -screen.mainloop() +screen.mainloop() # führt den Tk-mainloop aus diff --git a/lindenmayer/drawer.py b/lindenmayer/drawer.py index b49ab99..087e535 100644 --- a/lindenmayer/drawer.py +++ b/lindenmayer/drawer.py @@ -2,11 +2,13 @@ import turtle from dataclasses import dataclass import lSystems +# um die Rotation und Position einer turtle zu speichern @dataclass class State: heading: float position: turtle.Vec2D +# enthält alle Information, die notwendig sind, um ein Lindenmayer-System zu zeichnen @dataclass class DrawingInfo: lSystem: lSystems.LSytem @@ -16,78 +18,100 @@ class DrawingInfo: scale: float recursionDepth: int +# enthält zusätzlich zu DrawingInfo die turtle mit der die Zeichnung gemacht wurde @dataclass class Drawing: info: DrawingInfo turtle: turtle.Turtle -drawings = [] +drawings = [] # speichert alle Zeichnungen (Objekte der Klasse Drawing), die anzeigt werden class Drawer(): def __init__(self, drawing): + # verschiedensten Variablen werden Werte zugewiesen self.startWord = drawing.info.lSystem.startWord self.recursionDepth = drawing.info.recursionDepth self.productionRules = drawing.info.lSystem.productionRules self.angel = drawing.info.lSystem.angel self.forwardDistance = drawing.info.scale self.turtle = drawing.turtle + + # bewegt die turtle zur Sartposition und rotiert die turtle + # Zuvor wird das Zeichnen temporär deaktiviert, damit die turtle beim Bewegen + # zur neuen Position keine Linie zeichnet. self.turtle.penup() self.turtle.setposition(drawing.info.position) self.turtle.setheading(drawing.info.rotation) self.pendown() - self.turtle.pencolor(drawing.info.color) + self.turtle.pencolor(drawing.info.color) # ändert die Farbe + + # Diese Methode wird nur bei der Klasse DrawerSimulation gebraucht, um die Eckpunkte zu speichern. def storeEdges(self): pass + # wrappt die pendown Methode der turtle, damit das zeichnen bei überschreiben der Methode + # deaktiviert werden kann. def pendown(self): self.turtle.pendown() + # Die Methode zeichnet das Lindenmayer-System. + # n gibt die (verbleibende) Rekursiontiefe an. + # Beim ersten Aufrufen wird die Methode ohne Paramenter aufgerufen. def draw(self, word = None, n = None): + # weist die Standartwerte zu if n == None: n = self.recursionDepth if word == None: word = self.startWord + turtleStates = [] - if n == 0: - self.turtle.screen.update() + if n == 0: # zeichnen beendet + self.turtle.screen.update() # updated den Bildschirm, sodass das Lindenmayer-System anzeigt wird return - for character in word: + for character in word: # itteriert über das Wort match character: case "F": - self.turtle.forward(self.forwardDistance) - self.storeEdges() + self.turtle.forward(self.forwardDistance) # bewegt die turtle nach vorne + self.storeEdges() # speichert die Eckpunkte (für die Klasse DrawerSimulation) case "+": - self.turtle.left(self.angel) + self.turtle.left(self.angel) # dreht die turtle nach links case "-": - self.turtle.right(self.angel) + self.turtle.right(self.angel) # dreht die turtle nach rechts case "[": + # speichert Position und Rotation der turtle turtleStates.append(State(self.turtle.heading(), self.turtle.position())) case "]": - self.turtle.penup() + # Die letzte abgespeicherte Position und Rotation wird wiederhergestellt + self.turtle.penup() # um einen Strich zu der neuen Position zu vermeiden state = turtleStates.pop() self.turtle.setposition(state.position) self.turtle.setheading(state.heading) self.pendown() + # Bei einem Nicht-Terminal werden die jeweiligen Produktionsregeln ausgeführt if character in self.productionRules: self.draw(self.productionRules[character], n - 1) +# Klasse für das Speichern der Eckpunkte class Edges: def __init__(self): + # Die Klasse DrawerSimulation wird immer mit der Startpostion (0, 0) aufgerufen. + # Deshalb sind die minimalen und maximalen Werte zu anfangs auch alle 0. self.min = [0, 0] self.max = [0, 0] + # Methoden, die die beiden Eckpunkte als Vektor zurückgeben def minVec(self): return turtle.Vec2D(self.min[0], self.min[1]) - def maxVec(self): return turtle.Vec2D(self.max[0], self.max[1]) class DrawerSimulation(Drawer): def __init__(self, drawing): - super(DrawerSimulation, self).__init__(drawing) + super(DrawerSimulation, self).__init__(drawing) # Konstruktor der Superklasse self.edges = Edges() + # ändert die Koordinaten der Eckpunkte, wenn die turtle über die bisherigen hinaus geht. def storeEdges(self): for i, koord in enumerate(self.turtle.position()): if koord < self.edges.min[i]: @@ -95,36 +119,55 @@ class DrawerSimulation(Drawer): elif koord > self.edges.max[i]: self.edges.max[i] = koord + # deaktiviert das Zeichnen def pendown(self): pass +# Die Funktion simuliert erst das Lindenmayer-System, um die Größe zu erhalten. +# Danach wird das Lindenmayer-System gezeichnet und auf die agegebene Größe skalliert def draw(lSystem, recursionDepth, middle, rotation, size, color): + # Informationen zum Zeichnen des Lindenmayer-System drawingInfo = DrawingInfo(lSystem, turtle.Vec2D(0, 0), rotation, color, 1, recursionDepth) - turtleObject = newTurtle() + turtleObject = newTurtle() # erstellt eine neue turtle drawing = Drawing(drawingInfo, turtleObject) simulatedDraw = DrawerSimulation(drawing) - simulatedDraw.draw() + simulatedDraw.draw() # simuliert das Lindenmayer-System + + # die beiden diagonalen Eckpunkte der Fläche in der die Zeichnung liegen würde maxVec = simulatedDraw.edges.maxVec() minVec = simulatedDraw.edges.minVec() + distance = maxVec - minVec - xScale = size[0] / distance[0] - yScale = size[1] / distance[1] + xScale = size[0] / distance[0] # Skallierungswert für die Höhe + yScale = size[1] / distance[1] # Skallierungswert für die Breite + + # Der kleinere Skallierungswert wird benutzt damit die Zeichnung noch innerhalb des Bereichs ist. scale = yScale if xScale > yScale else xScale + + # Die Sartposition für die Zeichnung wird berechnet indem die Startpostion berechnet wird, + # wenn der Mittelpunkt bei (0, 0) liegen soll. Dieser Punkt wird dann um den gewählten Mittelpunkt verschoben. pos = middle + (-minVec - distance * (1/2)) * scale + + # zeichnet das Lindenmayer-System mit den berechneten Werten drawing.info.position = pos drawing.info.scale = scale drawScaled(drawing) +# erstellt eine neue turtle und nimmt einige Einstellungen vor. def newTurtle(): turtleObject = turtle.Turtle() - turtleObject.hideturtle() + turtleObject.hideturtle() # turtle wird nicht angezeigt + + # macht das Zeichnen so schnell wie möglich und für das Sehen der Änderungen ist ein Update des Bildschirms erforderlich turtleObject._tracer(0, 0) return turtleObject +# löscht eine Zeichnung def delete(i): drawings[i].turtle.clear() del drawings[i] +# zeichnet ein Lindenmayer-System und es der Liste hinzu def drawScaled(drawing): actualDrawer = Drawer(drawing) actualDrawer.draw() diff --git a/lindenmayer/inputHelper.py b/lindenmayer/inputHelper.py index 7f8e6d1..a9db68c 100644 --- a/lindenmayer/inputHelper.py +++ b/lindenmayer/inputHelper.py @@ -5,34 +5,42 @@ import pickle from dataclasses import dataclass from PIL import Image import io +import sys +# Klasse mit allen nötigen Informationen, um den Zustand des Programms abzuspeichern. @dataclass class Save: backgroundColor: any drawingInfos: [drawer.DrawingInfo] +# Pfad der geladenen Datei, um eine Option anzubieten schnell unter der gleichen Datei zu speichern. loadedFilepath = None -backgroundColor = "white" +backgroundColor = "white" # aktuelle Hintergrundfarbe des Fensters +# Funktion, um eine Information über den Standartwert einer Benutzereingabe anzugeben def defaultValueMsg(defaultValue): if defaultValue == None: return "" return f" (Standartwert: {defaultValue})" +# liest eine Nutzereingabe mit der entsprechenden Beschreibung ein und infomiert den +# Nutzer über den Standartwert def inputWithDefault(description, defaultValue): return input(f"{description}{defaultValueMsg(defaultValue)}: ") -def inputNum(inputType, description, rangeErrorMsg, minRange, maxRange, defaultValue = None): - while True: - inputValue = inputWithDefault(description, defaultValue) +# Hilfsfunktion für die Nutzereingabe einer Zahl in einem bestimmten Bereich +def inputNum(numberType, description, rangeErrorMsg, minRange, maxRange, defaultValue = None): + while True: # Die Eingabeauffordung wird wiederholt, wenn die Eingabe ungültig ist. + inputValue = inputWithDefault(description, defaultValue) # Eingabe wird eingelesen try: - number = inputType(inputValue) - inputError = number < minRange or number > maxRange - if inputError: - print(rangeErrorMsg) - else: - return number + # Der Eingabewert Wert wird zu einer Zahl konvertiert (float oder int). + # Wurde keine Zahl eingegeben, wird ein ValueError ausgelöst. + number = numberType(inputValue) + if number >= minRange and number <= maxRange: # liegt die Zahl im entsprechenden Bereich, + return number # wird diese zurückgegeben. + print(rangeErrorMsg) # Ansonsten wird eine Fehlermeldung ausgegeben. except ValueError: + # Wurde nichts eingegeben und es gibt einen Standartwert, wird dieser zurückgegeben. if inputValue == "" and defaultValue != None: return defaultValue print("Fehler: keine gültige Zahl") @@ -40,38 +48,49 @@ def inputNum(inputType, description, rangeErrorMsg, minRange, maxRange, defaultV def inputColorError(): print("Fehler: ungültige Farbeingabe") +# fragt eine TK-Farbe ab def inputColor(turtleObject, question, defaultValue = None): - while True: + while True: # Die Eingabeauffordung wird wiederholt, wenn die Eingabe ungültig ist. inputValue = input(f"{question} RGB-Wert eingeben, wobei die Farben mit Leerzeichen getrennt werden, oder einen Tk-Farbnamen eingeben [0 0 0 - 255 255 255 oder Farben auf https://www.tcl.tk/man/tcl8.4/TkCmd/colors.html]{defaultValueMsg(defaultValue)}: ") - if inputValue == "": + if inputValue == "": # Wenn keine Farbe eingeben wurde, if defaultValue != None: - return defaultValue - inputColorError() + return defaultValue # wird der Standartwert verwendet + inputColorError() # bzw. ein Fehler ausgegeben und die Abfrage wiederholt, + # wenn es keinen Standartwert gibt else: - try: + try: + # Die Eingabe wird in eine RGB-Tuple umgewandelt. color = tuple([int(color) for color in inputValue.split()]) - turtleObject.pencolor(color) + turtleObject.pencolor(color) # der RGB-Wert wird getestet return color + # hat der Benutzer keinen gültigen RGB-Wert eingeben, wird ein Fehler geworfen und es + # wird probiert ob ein Tk-Farbname verwendet wurde. except: try: turtleObject.pencolor(inputValue) return inputValue except: + # Wurde kein Tk-Farbname verwendet, ist die Eingabe ungültig und + # der Nutzer wird erneut nach einer Farbe gefragt inputColorError() +# fordert den Nutzer auf einen String einzugeben def inputString(question, defaultValue = None): - while True: + while True: # Die Eingabeauffordung wird wiederholt, wenn die Eingabe ungültig ist. inputValue = inputWithDefault(question, defaultValue) if inputValue != "": return inputValue - if defaultValue != None: + if defaultValue != None: # wurde nichts eingegeben, wird der Standartwert verwendet return defaultValue +# Die Funktion führt einen Befehl aus und ruft sich daraufhin rekursiv auf. def takeInput(turtleObject): global backgroundColor + global loadedFilepath inputValue = input("Bitte einen Befehl eingeben. h für Hilfe: ") match inputValue: case "h": + # Hilfeseite print("""h: Hilfe anzeigen d: ein neues Lindenmayer-System zeichnen l: ein Lindenmayer-System löschen @@ -81,88 +100,121 @@ s: Lindenmayer-Systeme speichern r: zuvor gespeicherte Lindenmayer-Systeme wiederherstellen e: als Bilddatei exportieren""") case "d": + # gibt vorhandene Lindenmayer-Systeme aus for i, lSystem in enumerate(lSystems.LSystems): print(f"{i}: {lSystem.name}") - lSystemIndex = inputNum(int, "Bitte ein Lindenmayer-System auswählen und die entsprechende Nummer eingeben: ", "Es gibt kein L-System mit dieser Nummer.", 0, len(lSystems.LSystems) - 1) + + # verschiedene Nutzereingaben + lSystemIndex = inputNum(int, "Bitte ein Lindenmayer-System auswählen und die entsprechende Nummer eingeben", "Es gibt kein L-System mit dieser Nummer.", 0, len(lSystems.LSystems) - 1) lSystem = lSystems.LSystems[lSystemIndex] recursionDepth = inputNum(int, "Rekursiontiefe des Lindenmayer-Systems eingeben [1-50]", "Rekursionstiefe nicht im vorgegebenen Bereich.", 1, 50, lSystem.recursionDepth) rotation = inputNum(float, "Bitte die Rotation in Grad gegen den Uhrzeigersinn angeben, wobei 0° rechts ist", "nur Gradzahlen von 0 bis 360 werden akzeptiert.", 0, 360, 90) - inputError = True color = inputColor(turtleObject, "Welche Farbe soll das Lindenmayer-System haben?", "black") + + inputError = True while inputError: - match input("Möchtest du das das Lindenmayer-System das ganze Fenster ausfüllt? [J/n]: ").lower(): - case "j"|"": + match input("Möchtest du, dass das Lindenmayer-System das ganze Fenster ausfüllt? [J/n]: ").lower(): + case "j"|"": # Ja ist die Standartantwort. inputError = False + + # zeichnet das Lindenmayer-System + # Von der Größe des Fensters werden drei Pixel abgezogen, da die Linien auch + # eine Breite haben und sonst die Linien am Rand nicht zu sehen sind. drawer.draw(lSystem, recursionDepth, turtle.Vec2D(0, 0), rotation, turtle.Vec2D(turtleObject.screen.window_width(), turtleObject.screen.window_height()) - turtle.Vec2D(3, 3), color) - takeInput(turtleObject) + takeInput(turtleObject) # ruft sich rekursiv auf, um den nächsten Befehl entgegen zu nehmen case "n": inputError = False - pos = [] + print("Klicke bitte die beiden diagonalen Eckpunkte der Fläche an in der das Lindenmayer-System gezeichnet werden soll.") + pos = [] # Liste, in der die angeklickte Punkte gespeichert werden + + # Die Funktion wird nach einem Klick mit einem Ortsvektor des angeklickten Punktes aufgerufen. def afterClick(vec): pos.append(vec) - if len(pos) == 2: + if len(pos) == 2: # Wenn zwei Punkte angeklickt wurden + # Nach einem Klick wird die Funktion nicht mehr aufgerufen turtleObject.screen.onclick(None) - size = pos[1] - pos[0] - middle = pos[0] + (1/2) * size - size = turtle.Vec2D(abs(size[0]), abs(size[1])) - drawer.draw(lSystem, recursionDepth, middle, rotation, size, color) - takeInput(turtleObject) + from0to1 = pos[1] - pos[0] # Vektor von Punkt Index 0 zu Punkt Index 1 + middle = pos[0] + (1/2) * from0to1 # Mittelpunkt der beiden angeklickten Punkte + size = turtle.Vec2D(abs(from0to1[0]), abs(from0to1[1])) + drawer.draw(lSystem, recursionDepth, middle, rotation, size, color) # zeichnet das Lindenmayer-System + takeInput(turtleObject) # ruft sich rekursiv auf, um den nächsten Befehl entgegen zu nehmen + # registriert die afterClick-Funktion als event turtleObject.screen.onclick(lambda x, y: afterClick(turtle.Vec2D(x, y))) case _: print("Bitte j oder n eingeben") case "b": + # fragt nach einer Hintergrundfarbe und ändert diese entsprechend. backgroundColor = inputColor(turtleObject, "Hintergrundfarbe eingeben") turtleObject.screen.bgcolor(backgroundColor) case "q": - quit() + sys.exit() # beendet das Programm case "l": if len(drawer.drawings) == 0: print("Es gibt nichts, was man löschen könnte.") else: + # gibt die angezeigten Lindenmayer-Systeme aus for i, drawing in enumerate(drawer.drawings): print(f"{i}: {drawing.info.lSystem.name} Position: {drawing.info.position}") - iDelete = inputNum(int, "Nummer des zu löschenden Zeichnung eingeben: ", "Es gibt keine Zeichnung mit dieser Nummer", 0, len(drawer.drawings) - 1) - drawer.delete(iDelete) + iDelete = inputNum(int, "Nummer des zu löschenden Zeichnung eingeben", "Es gibt keine Zeichnung mit dieser Nummer", 0, len(drawer.drawings) - 1) + drawer.delete(iDelete) # löscht das Lindenmayer-System case "s": + # fragt nach dem Dateipfad. Wenn bereits eine Datei geladen oder gespeichert wurde, + # wird der dazugehörige Pfad als Standartwert verwendet. filepath = inputString("Dateipfad zum Speichern eingeben", loadedFilepath) - drawingInfo = [drawing.info for drawing in drawer.drawings] - save = Save(backgroundColor, drawingInfo) + loadedFilepath = filepath + + # Beim Speichern der angezeigten Lindenmayer-Systeme werden die turtles nicht benötigt. + drawingInfos = [drawing.info for drawing in drawer.drawings] + save = Save(backgroundColor, drawingInfos) # Objekt mit allen Daten, die gespeichert werden sollen. try: file = open(filepath, "wb") - pickle.dump(save, file) + pickle.dump(save, file) # schreibt die Daten in die Datei file.close() + + # infomiert den Nutzer, dass seine Daten gespeichert wurden print("erfolgreich gespeichert") except Exception as err: - print(err) + print(err) # gibt mögliche IO-Fehler aus case "r": filepath = inputString("Datei, die geladen werden soll eingeben") + loadedFilepath = filepath try: file = open(filepath, "rb") - save = pickle.load(file) + save = pickle.load(file) # liest die Datei file.close() - turtleObject.screen.clear() + turtleObject.screen.clear() # leert das Fenster + + # ändert die Hintergrundfarbe backgroundColor = save.backgroundColor turtleObject.screen.bgcolor(save.backgroundColor) + + # zeichnet die Lindenmayer-Systeme for drawingInfo in save.drawingInfos: drawing = drawer.Drawing(drawingInfo, drawer.newTurtle()) drawer.drawScaled(drawing) except Exception as err: - print(err) + print(err) # gibt mögliche IO-Fehler aus case "e": + # speichert das Bild. für genauere Informationen siehe: # https://stackoverflow.com/questions/34777676/how-to-convert-a-python-tkinter-canvas-postscript-file-to-an-image-file-readable canvas = turtleObject.screen.getcanvas() - postscript = canvas.postscript(colormode = "color") - image = Image.open(io.BytesIO(postscript.encode("utf-8"))) + postscript = canvas.postscript(colormode = "color") # exportiert das Bild im postscript Format + image = Image.open(io.BytesIO(postscript.encode("utf-8"))) # lädt das postscript mit PIL + + # fragt den Nutzer nach einem Dateipfad filepath = inputString("Dateipfad für das zu exportierende Bild eingeben. Das Bildformat wird über die Dateiendung bestimmt") try: - image.save(filepath) + image.save(filepath) # speichert das Bild print("Bild gespeichert") - except ValueError: + except ValueError: # PIL löst einen ValueError aus, wenn die Dateiendung unbekannt ist print("Fehler: unbekannte Dateiendung") except Exception as err: - print(err) + print(err) # gibt andere Fehler aus case _: print("unbekannter Befehl") + + # Bei dem d(raw)-Befehl wurde die Funktion bereits rekursiv aufgerufen. + # Ansonsten wird sie noch aufgerufen und der nächste Befehl kann ausgeführt werden. if inputValue != "d": takeInput(turtleObject) diff --git a/lindenmayer/lSystems.py b/lindenmayer/lSystems.py index 5d557eb..93d85ff 100644 --- a/lindenmayer/lSystems.py +++ b/lindenmayer/lSystems.py @@ -1,6 +1,7 @@ from dataclasses import dataclass import typing +# beschreibt ein Lindenmayer-System @dataclass class LSytem: name: str @@ -9,12 +10,17 @@ class LSytem: angel: float recursionDepth: int +# dem Programm bekannte Lindenmayer-Systeme LSystems = [ + # Lindenmayer-Systeme vom AB LSytem("toter Busch", "F", {"F": "F[+F]F[-F]F"}, 25.7, 5), LSytem("Gretenbaum", "F", {"F": "F[+F]F[-F][F]"}, 20.0, 5), LSytem("Laubbaum", "F", {"F": "FF-[-F+F+F]+[+F-F-F]"}, 22.5, 4), - LSytem("AB d", "X", {"X": "F[+X]F[-X]+X", "F": "FF"}, 20.0, 7), - LSytem("AB e", "X", {"X": "F[+X][-X]FX", "F": "FF"}, 25.7, 7), - LSytem("AB f", "X", {"X": "F-[[X]+X]+F[+FX]-X", "F": "FF"}, 22.5, 5), + LSytem("dürrer Strauch", "X", {"X": "F[+X]F[-X]+X", "F": "FF"}, 20.0, 7), + LSytem("sysmetrisches Pflänzchen", "X", {"X": "F[+X][-X]FX", "F": "FF"}, 25.7, 7), + LSytem("schiefer Strauch", "X", {"X": "F-[[X]+X]+F[+FX]-X", "F": "FF"}, 22.5, 5), + + # bekannte Lindenmayer-Systeme LSytem("Drachenkurve", "FX", {"X": "X+YF+", "Y": "-FX-Y"}, 90.0, 15), + LSytem("Kochsche Schneeflocke", "F--F--F", {"F": "F+F--F+F"}, 60.0, 5), ]