This commit is contained in:
2022-01-13 22:12:01 +01:00
parent 42cbbe788e
commit 7d4884946f
4 changed files with 183 additions and 67 deletions

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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),
]