Benutzer-Werkzeuge

Webseiten-Werkzeuge


ss20:neg_drawables

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen gezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen Revision Vorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
ss20:neg_drawables [2020/09/11 14:27]
srather [Ray]
ss20:neg_drawables [2020/09/11 14:28] (aktuell)
srather [Portal]
Zeile 262: Zeile 262:
 ==== Polygon ==== ==== Polygon ====
 <file python polygon.py>​ <file python polygon.py>​
 +import colors
 from drawable import * from drawable import *
 from ray import * from ray import *
Zeile 268: Zeile 269:
 class Polygon(drawable):​ class Polygon(drawable):​
     """​     """​
-    Polygon class as drawable ​object. Implements functionality of abstract drawable class.+    Polygon class as 'drawable' objects. 
 +    ​Implements functionality of abstract drawable class.
     """​     """​
  
  
-    ​# constructor:​ create self as drawable object +    def __init__(self,​ *points, color=colors.BLACK): 
-    ​def __init__(self,​ *points, color=(0,0,0)):+        """​Initilise '​self'​ with standard color '​BLACK'​."""​
         drawable.__init__(self,​ *points, color=color)         drawable.__init__(self,​ *points, color=color)
  
  
-    # Implements draw() method of abstract drawable class 
     def draw(self, screen, offset):     def draw(self, screen, offset):
 +        """​
 +        Draw '​self'​ as a polygon on '​screen'​.
 +        """​
 +
         if not self: # empty         if not self: # empty
             return             return
Zeile 284: Zeile 289:
         polygon = self.copy()         polygon = self.copy()
  
-        # move to real position on screen+        # calculate ​real position on screen
         rect = Vector(*screen.get_rect()[2:​])         rect = Vector(*screen.get_rect()[2:​])
         for i, _ in enumerate(polygon):​         for i, _ in enumerate(polygon):​
             polygon[i] = round(polygon[i] - offset + rect / 2)             polygon[i] = round(polygon[i] - offset + rect / 2)
-            if polygon[i].length2() > 30000:+            if polygon[i].length2() > 30000: ​# hotfix
                 return                 return
  
 +        # point
         if len(polygon) == 1:         if len(polygon) == 1:
             pygame.gfxdraw.pixel(screen,​ *polygon[0],​ self.color)             pygame.gfxdraw.pixel(screen,​ *polygon[0],​ self.color)
 +        # line
         elif len(polygon) == 2:         elif len(polygon) == 2:
             pygame.draw.aaline(screen,​ self.color, polygon[0], polygon[1])             pygame.draw.aaline(screen,​ self.color, polygon[0], polygon[1])
 +        # polygon
         else:         else:
             pygame.gfxdraw.aapolygon(screen,​ polygon, self.color)             pygame.gfxdraw.aapolygon(screen,​ polygon, self.color)
Zeile 300: Zeile 308:
  
  
-    ​# Implements draw_shadow() from abstract drawable class +    def draw_shadow(self,​ screen, offset, source, color=colors.DARK_GRAY): 
-    ​def draw_shadow(self,​ screen, offset, source, color=(31,31,31)): +        """​ 
-        if len(self) == 0:+        Draw draw a shadow of '​self'​ on '​screen'​ respecting the light-'​source'​. 
 +        """​ 
 + 
 +        if not self: # empty
             return             return
  
 +        # point
         elif len(self) == 1:         elif len(self) == 1:
             Ray(self[0],​ self[0] - source, color=color).draw(screen,​ offset)             Ray(self[0],​ self[0] - source, color=color).draw(screen,​ offset)
  
 +        # line
         elif len(self) == 2:         elif len(self) == 2:
-            ​if not self[0].onscreen(screen,​ offset) and not self[1].onscreen(screen,​ offset): +            ​# edgecasewould lead to DivisionByZeroError
-                return +
             if self[0] == source or self[1] == source:             if self[0] == source or self[1] == source:
                 for point in self:                 for point in self:
                     Ray(point, point - source, color=color).draw(screen,​ offset)                     Ray(point, point - source, color=color).draw(screen,​ offset)
-            else: +                return
-                rect = screen.get_rect() +
-                corner = Vector(*rect[2:​]) / 2 +
-                corners = [ +
-                    corner * Vector( 1, 1), +
-                    corner * Vector(-1, 1), +
-                    corner * Vector(-1,​-1),​ +
-                    corner * Vector( 1,-1), +
-                    corner * Vector( 1, 1), +
-                    corner * Vector( 1,-1), +
-                ]+
  
-                ​ray1 = Ray(self[0],​ self[0] - source) +            # corners of screen area 
-                ray2 = Ray(self[1],​ self[1] - source) +            rect = screen.get_rect() 
-                if ray1.dir.angle_to(ray2.dir) > 180: +            corner = Vector(*rect[2:​]) / 2 
-                    ray1, ray2 = ray2, ray1+            corners = [ 
 +                corner * Vector( 1, 1), 
 +                corner * Vector(-1, 1), 
 +                corner * Vector(-1,​-1),​ 
 +                corner * Vector( 1,-1), 
 +                corner * Vector( 1, 1), 
 +                corner * Vector( 1,-1), 
 +            ] 
 +            # rays from source to both ends 
 +            ​ray1 = Ray(self[0],​ self[0] - source) 
 +            ray2 = Ray(self[1],​ self[1] - source) 
 +            # garantees anticlockwise order (important for calculations) 
 +            ​if ray1.dir.angle_to(ray2.dir) > 180: 
 +                ray1, ray2 = ray2, ray1
  
-                ​shadow = Polygon(color=color) +            # creates minimal polygon 
-                shadow.append(ray2.offscreen(screen,​ offset)) +            ​shadow = Polygon(color=color) 
-                shadow.append(ray2.start) +            shadow.append(ray2.offscreen(screen,​ offset)) 
-                shadow.append(ray1.start) +            shadow.append(ray2.start) 
-                shadow.append(ray1.offscreen(screen,​ offset))+            shadow.append(ray1.start) 
 +            shadow.append(ray1.offscreen(screen,​ offset))
  
-                ​for i, _ in enumerate(corners[1:​-1]):​ +            # adds corners of screen to polygon if needed 
-                    if corners[i].is_between(ray1.dir,​ ray2.dir):​ +            ​for i, _ in enumerate(corners[1:​-1]):​ 
-                        if corners[i-1].is_between(ray1.dir,​ ray2.dir):​ +                if corners[i].is_between(ray1.dir,​ ray2.dir):​ 
-                            shadow.append(offset + corners[i-1]) +                    if corners[i-1].is_between(ray1.dir,​ ray2.dir):​ 
-                        shadow.append(offset + corners[i]) +                        shadow.append(offset + corners[i-1]) 
-                        if corners[i+1].is_between(ray1.dir,​ ray2.dir):​ +                    shadow.append(offset + corners[i]) 
-                            shadow.append(offset + corners[i+1]) +                    if corners[i+1].is_between(ray1.dir,​ ray2.dir):​ 
-                        break+                        shadow.append(offset + corners[i+1]) 
 +                    break
  
-                ​shadow.draw(screen,​ offset)+            # finally draws calculated shadow 
 +            ​shadow.draw(screen,​ offset)
  
-        else:+        ​# polygon 
 +        ​else: # draws shadow for every line in polygon
             for i, _ in enumerate(self[:​-1]):​             for i, _ in enumerate(self[:​-1]):​
                 Polygon(self[i],​ self[i+1]).draw_shadow(screen,​ offset, source)                 Polygon(self[i],​ self[i+1]).draw_shadow(screen,​ offset, source)
Zeile 355: Zeile 373:
  
  
-    # Implements dist_to() method of abstract drawable class 
     def dist_to(self,​ source):     def dist_to(self,​ source):
 +        """​
 +        Return euclidean distance to the player ('​source'​).
 +        """​
 +
         def sdf_line(a, b, p):         def sdf_line(a, b, p):
-            ​b_a = b - a +            ​"""​ 
-            ​p_a = a+            ​Return distance from point to line a-b. 
 +            (source: https://​youtu.be/​PMltMdi1Wzg) 
 +            """​ 
             if b == a:             if b == a:
                 h = 0                 h = 0
             else:             else:
-                h = min(1, max(0, ​p_a.dot(b_a) / b_a.dot(b_a))) +                h = min(1, max(0, ​(p-a).dot(b-a) / (b-a).dot(b-a))) 
-            return (p_a - h * b_a).length()+            return ((p-a) - h * (b-a)).length()
  
-        if len(self) == 0:+        if not self: # empty
             return float('​inf'​)             return float('​inf'​)
  
-        ​if len(self) == 1: +        ​# minimum distance to a side 
-            return (source - self[0]).length() +        m = float('​inf'​) 
- +        for i, _ in enumerate(self):​ 
-        ​else: +            m = min(m, sdf_line(self[i-1],​ self[i], source)) 
-            ​m = float('​inf'​) +        return m
-            for i, _ in enumerate(self):​ +
-                m = min(m, sdf_line(self[i-1],​ self[i], source)) +
-            return m+
  
  
-    # Implements cut() from abstract drawable class 
     def cut(self, source, portal):     def cut(self, source, portal):
 +        """​
 +        Return '​self'​ but cut to fit in the view from '​source'​ though '​portal'​.
 +        This algorithm isn't perfect, but the best I got at the moment.
 +        """​
 +
         def position(point):​         def position(point):​
 +            """​
 +            Return the area '​point'​ is in:
 +            -1 -- in the triangle between source and portal line
 +             0 -- to the right of visible area
 +             1 -- in the visible portal area
 +             2 -- to the left of visible area
 +            """​
 +
             vec = point - source             vec = point - source
             if vec.is_between(ray1.dir,​ ray2.dir):             if vec.is_between(ray1.dir,​ ray2.dir):
Zeile 388: Zeile 421:
                 else:                 else:
                     return -1                     return -1
- 
             if vec.is_between(null,​ ray1.dir):             if vec.is_between(null,​ ray1.dir):
                 return 0                 return 0
Zeile 394: Zeile 426:
                 return 2                 return 2
  
-        ray1 = Ray(portal[0], portal[0] - source) +        ​# rays that are borders of different areas 
-        ray2 = Ray(portal[1], portal[1] - source) +        ​ray1 = Ray(portal.start, portal.start ​- source) ​# area 0 <--> 1 
-        border = Ray(ray1.start,​ ray2.start - ray1.start)+        ray2 = Ray(portal.end, portal.end - source) ​# area 1 <--> 2 
 +        border = Ray(ray1.start,​ ray2.start - ray1.start) # area -1 <--> 1 
 +        null = -(ray1.dir + ray2.dir) # area 0 <--> 2 
 +        # garantees anticlockwise order (important for calculations)
         if ray1.dir.angle_to(ray2.dir) > 180:         if ray1.dir.angle_to(ray2.dir) > 180:
             ray1, ray2 = ray2, ray1             ray1, ray2 = ray2, ray1
 +
         rest = Polygon()         rest = Polygon()
         rest.__dict__ = self.__dict__         rest.__dict__ = self.__dict__
-        null = -(ray1.dir + ray2.dir) 
  
-        if len(self) == 0:+        if not self: # empty
             return rest             return rest
  
 +        # edgecase for when a portal is in its own portal world in the same place
         if len(self) == 2:         if len(self) == 2:
             if self[0] in portal and self[1] in portal:             if self[0] in portal and self[1] in portal:
                 return rest                 return rest
  
 +        # algorithm for cutting polygon
         nxt_pos = position(self[-1])         nxt_pos = position(self[-1])
         for i, _ in enumerate(self):​         for i, _ in enumerate(self):​
Zeile 415: Zeile 452:
             nxt_pos = position(self[i])             nxt_pos = position(self[i])
  
-            if cur_pos == 1:+            if cur_pos == 1: # visible
                 rest.append(self[i-1])                 rest.append(self[i-1])
  
 +            # same position, nothing changes
             if nxt_pos == cur_pos:             if nxt_pos == cur_pos:
                 continue                 continue
  
 +            # different position, intersections with visible area needed
             inter = []             inter = []
  
 +            # through the left or right boundary of visible area -> intersection
             if 1 in (cur_pos, nxt_pos):             if 1 in (cur_pos, nxt_pos):
                 if 0 in (cur_pos, nxt_pos):                 if 0 in (cur_pos, nxt_pos):
Zeile 431: Zeile 471:
                     inter += border.intersect(Ray(self[i-1],​ self[i] - self[i-1]))                     inter += border.intersect(Ray(self[i-1],​ self[i] - self[i-1]))
  
 +            # line goes outside the visible area around the portal -> edges of portal
             elif -1 in (cur_pos, nxt_pos):             elif -1 in (cur_pos, nxt_pos):
                 if 0 in (cur_pos, nxt_pos):                 if 0 in (cur_pos, nxt_pos):
Zeile 437: Zeile 478:
                     inter += [ray2.start]                     inter += [ray2.start]
  
 +            # line goes from the left to the right -> may intersect visible area
             else: # 0 and 2             else: # 0 and 2
                 if cur_pos == 0:                 if cur_pos == 0:
Zeile 445: Zeile 487:
                     inter += ray1.intersect(Ray(self[i-1],​ self[i] - self[i-1]))                     inter += ray1.intersect(Ray(self[i-1],​ self[i] - self[i-1]))
  
 +            # add intersections
             for pt in inter:             for pt in inter:
-                if pt.length2() > 10000:+                if pt.length2() > 10000: ​# hotfix
                     print(f"​CalculationError while cutting {self} at {ray1, ray2}:​\n ​   {pt} is very big")                     print(f"​CalculationError while cutting {self} at {ray1, ray2}:​\n ​   {pt} is very big")
                 else:                 else:
                     rest.append(pt)                     rest.append(pt)
- 
-        if nxt_pos == 1: 
-            rest.append(self[-1]) 
  
         return rest         return rest
Zeile 467: Zeile 507:
 import colors import colors
 from polygon import * from polygon import *
 +
  
 class Floor(Polygon):​ class Floor(Polygon):​
     """​     """​
-    ​Class for polygons ​just drawn on the floor as drawable object. Implements functionality of abstract drawable class.+    ​Subclass ​for polygons as floor tiles wich don't have collisions.
     """​     """​
  
  
-    # constructor:​ create self as drawable object 
     def __init__(self,​ *points, color=colors.DARK_RED):​     def __init__(self,​ *points, color=colors.DARK_RED):​
 +        """​Initilise '​self'​ with standard color '​DARK_RED'​."""​
         Polygon.__init__(self,​ *points, color=color)         Polygon.__init__(self,​ *points, color=color)
  
  
-    # Implements draw_shadow() from abstract drawable class 
     def draw_shadow(self,​ screen, offset, source, color=None):​     def draw_shadow(self,​ screen, offset, source, color=None):​
 +        """​Overide '​draw_shadow'​ to not draw a shadow."""​
         pass         pass
  
  
-    # Implements cut() from abstract drawable class 
     def cut(self, source, portal):     def cut(self, source, portal):
 +        """​
 +        Return '​self'​ but cut to fit in the view from '​source'​ though '​portal'​.
 +        Makes use of '​Polygon.cut'​.
 +        """​
 +
         rest = Polygon.cut(self,​ source, portal)         rest = Polygon.cut(self,​ source, portal)
         if len(rest) > 2:         if len(rest) > 2:
             return Floor(*rest,​ color=self.color)             return Floor(*rest,​ color=self.color)
- +        else: 
-        return Polygon()+            return Polygon()
  
  
     def dist_to(self,​ source):     def dist_to(self,​ source):
 +        """​Overide '​dist_to'​ to never cause collisions."""​
         return float('​inf'​)         return float('​inf'​)
  
Zeile 505: Zeile 551:
 ==== Portal ==== ==== Portal ====
 <file python portal.py>​ <file python portal.py>​
 +import colors
 from polygon import * from polygon import *
 from matrix import * from matrix import *
-import colors 
  
  
 class Portal(Polygon):​ class Portal(Polygon):​
     """​     """​
-    Portal class as drawable ​object. ​Implements functionality ​of abstract drawable class.+    Portal class as a 2-element '​Polygon' ​object. 
 +    In for both sides of the portal there is 
 +    an offset vector and a distortion matrix.
     """​     """​
  
  
     # constructor:​ create self as Polygon object     # constructor:​ create self as Polygon object
-    def __init__(self,​ start, end, M=((1,​0),​(0,​1)),​ M2=None, offset=None,​ offset2=None,​ color=(0,0,255)):+    def __init__(self,​ start, end, M=((1,​0),​(0,​1)),​ M2=None, offset=None,​ offset2=None,​ color=colors.BRIGHT_BLUE):​ 
 +        """​ 
 +        Initilises '​self'​ with standard color '​BRIGHT_BLUE'​. 
 +        offset ​ -- for side 1 (std: None == portal will remain in same place) 
 +        offset2 -- for side 2 (std: None == portal will remain in same place) 
 +        M       -- distortion matrix for side 1 (stdunit matrix) 
 +        M2      -- distortion matrix for side 2 (std: None == inverse of M) 
 +        """​ 
         Polygon.__init__(self,​ start, end, color=color)         Polygon.__init__(self,​ start, end, color=color)
 +
         self.M = Matrix(*M)         self.M = Matrix(*M)
         if M2 is None:         if M2 is None:
Zeile 534: Zeile 591:
  
  
-    # Overrides attribute access 
     def __getattr__(self,​ attr):     def __getattr__(self,​ attr):
         """​         """​
Zeile 544: Zeile 600:
         if attr == '​end':​         if attr == '​end':​
             return self[1]             return self[1]
-        if attr == '​M':​ 
-            return self.__dict__['​M'​] 
-        if attr == '​offset':​ 
-            return self.__dict__['​offset'​] 
-        if attr == '​color':​ 
-            return self.__dict__['​color'​] 
- 
-        raise AttributeError(f"​{self.__class__.__name__} has no Attribute {attr}."​) 
  
  
     def go_through(self,​ dir, world):     def go_through(self,​ dir, world):
 +        """​
 +        Return the portal world of '​world'​ for the side in direction '​dir'​.
 +        """​
 +
         portal_world = []         portal_world = []
  
Zeile 561: Zeile 613:
             port.__dict__ = object.__dict__             port.__dict__ = object.__dict__
  
 +            # gone through side 1
             if dir.is_between(self.end - self.start, self.start - self.end):             if dir.is_between(self.end - self.start, self.start - self.end):
                 for i, _ in enumerate(object):​                 for i, _ in enumerate(object):​
                     if self.offset is None:                     if self.offset is None:
 +                        # middle of portal in portal world
                         offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2                         offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                     else:                     else:
                         offset = self.offset                         offset = self.offset
  
 +                    # apply distortion and offset
                     port[i] = self.M.prod(object[i]) - offset                     port[i] = self.M.prod(object[i]) - offset
  
 +                # if world gets mirrored the side-sensitive portals have to be reverted
                 if type(object) is Portal:                 if type(object) is Portal:
                     if self.M.i.angle_to(self.M.j) > 180:                     if self.M.i.angle_to(self.M.j) > 180:
                         port[0], port[1] = port[1], port[0]                         port[0], port[1] = port[1], port[0]
 +
 +            # gone through side 2
             else:             else:
                 for i, _ in enumerate(object):​                 for i, _ in enumerate(object):​
                     if self.offset2 is None:                     if self.offset2 is None:
 +                        # middle of portal in portal world
                         offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2                         offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                     else:                     else:
                         offset2 = self.offset2                         offset2 = self.offset2
  
 +                    # apply distortion and offset
                     port[i] = self.M2.prod(object[i]) - offset2                     port[i] = self.M2.prod(object[i]) - offset2
  
 +                # if world gets mirrored the side-sensitive portals have to be reverted
                 if type(object) is Portal:                 if type(object) is Portal:
                     if self.M2.i.angle_to(self.M2.j) > 180:                     if self.M2.i.angle_to(self.M2.j) > 180:
                         port[0], port[1] = port[1], port[0]                         port[0], port[1] = port[1], port[0]
  
 +            # add distorted object to portal world
             portal_world.append(port)             portal_world.append(port)
  
Zeile 598: Zeile 660:
             port.__dict__ = object.__dict__             port.__dict__ = object.__dict__
  
 +            # gone through side 1
             if (self.start - source).is_between(self.end - self.start, self.start - self.end):             if (self.start - source).is_between(self.end - self.start, self.start - self.end):
                 for i, _ in enumerate(object):​                 for i, _ in enumerate(object):​
                     if self.offset is None:                     if self.offset is None:
 +                        # middle of portal in portal world
                         offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2                         offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                     else:                     else:
                         offset = self.offset                         offset = self.offset
  
 +                    # apply distortion and offset
                     port[i] = self.M.prod(object[i]) - offset                     port[i] = self.M.prod(object[i]) - offset
  
 +                # if world gets mirrored the side-sensitive portals have to be reverted
                 if type(object) is Portal:                 if type(object) is Portal:
                     if self.M.i.angle_to(self.M.j) > 180:                     if self.M.i.angle_to(self.M.j) > 180:
                         port[0], port[1] = port[1], port[0]                         port[0], port[1] = port[1], port[0]
 +
 +            # gone through side 2
             else:             else:
                 for i, _ in enumerate(object):​                 for i, _ in enumerate(object):​
                     if self.offset2 is None:                     if self.offset2 is None:
 +                        # middle of portal in portal world
                         offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2                         offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                     else:                     else:
                         offset2 = self.offset2                         offset2 = self.offset2
  
 +                    # apply distortion and offset
                     port[i] = self.M2.prod(object[i]) - offset2                     port[i] = self.M2.prod(object[i]) - offset2
  
 +                # if world gets mirrored the side-sensitive portals have to be reverted
                 if type(object) is Portal:                 if type(object) is Portal:
                     if self.M2.i.angle_to(self.M2.j) > 180:                     if self.M2.i.angle_to(self.M2.j) > 180:
                         port[0], port[1] = port[1], port[0]                         port[0], port[1] = port[1], port[0]
  
 +            # add distorted and truncated object to portal world
             cut = port.cut(source,​ self)             cut = port.cut(source,​ self)
             if cut: # not empty             if cut: # not empty
Zeile 631: Zeile 703:
  
     def draw(self, screen, offset):     def draw(self, screen, offset):
-        ​pass+        ​"""​ 
 +        Draw the endpoints of '​self'​ on '​screen'​. 
 +        """​ 
 + 
 +        for point in self: 
 +            Polygon(point).draw(screen,​ offset) 
  
-    # overrides draw_shadow() inherited from Polygon class 
     def draw_shadow(self,​ screen, offset, source):     def draw_shadow(self,​ screen, offset, source):
         """​         """​
-        ​Draws shadow of portal +        ​Draw draw a shadow of 'self' on 'screen' ​respecting the light-'​source'​.
-        Called by 'self.draw_shadow(screen, offset)'+
         """​         """​
  
 +        # draw the shadow in the same color as the background to wipe the canvas
         Polygon.draw_shadow(self,​ screen, offset, source, color=colors.GRAY)         Polygon.draw_shadow(self,​ screen, offset, source, color=colors.GRAY)
-        # Polygon.draw_shadow(self,​ screen, offset, source, color=(150,​150,​150)) 
  
 +        # draw shadows for the end points of portal
         for point in self:         for point in self:
-            ​Ray(point, point - source, color=(0,​0,​0)).draw(screen, offset)+            ​Polygon(point).draw_shadow(screen, offset, source)
  
  
-    # Implements cut() from abstract drawable class 
     def cut(self, source, portal):     def cut(self, source, portal):
 +        """​
 +        Return '​self'​ but cut to fit in the view from '​source'​ though '​portal'​.
 +        Makes use of '​Polygon.cut'​.
 +        """​
 +
         rest = Polygon.cut(self,​ source, portal)         rest = Polygon.cut(self,​ source, portal)
-        if len(rest) ​>= 2:+        if len(rest) ​== 2:
             return Portal(*reversed(rest[:​2]),​ M=self.M, color=self.color)             return Portal(*reversed(rest[:​2]),​ M=self.M, color=self.color)
  
ss20/neg_drawables.1599827231.txt.gz · Zuletzt geändert: 2020/09/11 14:27 von srather