Browse Source

update geometry and pencil documentation, add PaintPotPencil class to pencil.py

olinox 9 years ago
parent
commit
51fb9e1729
4 changed files with 183 additions and 55 deletions
  1. 9 1
      pypog/Grid.py
  2. 7 4
      pypog/Piece.py
  3. 16 16
      pypog/geometry.py
  4. 151 34
      pypog/pencil.py

+ 9 - 1
pypog/Grid.py

@@ -7,6 +7,14 @@ from pypog import geometry
 from pypog import pathfinder
 
 
+ORIGIN_HPOSITION_LEFT = 0
+ORIGIN_HPOSITION_MIDDLE = 1
+ORIGIN_HPOSITION_RIGHT = 2
+ORIGIN_VPOSITION_TOP = 10
+ORIGIN_VPOSITION_MIDDLE = 11
+ORIGIN_VPOSITION_BOTTOM = 12
+
+
 class Grid(object):
     def __init__(self, cell_shape, width, height, roof = None):
         self._cell_shape = None
@@ -108,7 +116,7 @@ class Grid(object):
     def path(self, x1, y1, x2, y2):
         return pathfinder.path( self, (x1, y1), (x2,y2), self.moving_cost_function )
 
-    
+
 class HexGrid(Grid):
     def __init__(self, width, height):
         Grid.__init__(self, geometry.HEX, width, height)

+ 7 - 4
pypog/Piece.py

@@ -134,13 +134,16 @@ class Piece(object):
         occupation = self.occupation()
         return [(x, y, z) for x, y in occupation for z in range(dz + self.zR, dz + self.zR + self.height)]
 
-    def move(self, x, y, zR = None):
-        """move the piece to (x, y) position and zR relative altitude"""
+    def move_to(self, x, y, zR = None):
+        """move the piece to (x, y) position and is requested to zR relative altitude"""
         self.position = x, y
         if zR != None:
             self.zR = zR
     
     def rotate(self, i):
-        """pivot the Piece i times (counterclockwise rotation)"""
-        self.rotation += i
+        """pivot the Piece i times (counterclockwise rotation, i can be negative)"""
+        new_rotation = self.rotation + i
+        self.rotation = new_rotation % self.cell_shape
+        
+        
         

+ 16 - 16
pypog/geometry.py

@@ -1,9 +1,6 @@
 '''
-Created on 5 dec. 2016
     Geometric functions on hexagonal or square grids
-    
-    > Line algorithm is an implementation of bresenham algorithm
-    
+
     2D functions return lists of (x, y) coordinates
     3D functions return lists of (x, y, z) coordinates
     
@@ -15,8 +12,9 @@ Created on 5 dec. 2016
     * hollow_rect function return the list of the cells on the borders of the rectangle between the cells (x1, y1), (x2, y1), , (x2, y2) and , (x1, y2)
     * triangle function return the list of the cells in a triangle from its apex (xa, ya) to its base (xh, yh)
     * triangle3d function return the list of the cells in a cone from its apex (xa, ya, za) to its base (xh, yh, zh)
+    * pivot function return a list of coordinates after a counterclockwise rotation of a given list of coordinates, around a given center
     
-@author: olinox
+@author: olinox14
 '''
 from math import sqrt
 
@@ -26,9 +24,10 @@ SQUARE = 4
 HEX = 6
 ANGLES = (1, 2, 3)
 
-## neigbours
+## neighbours
 
 def neighbours_of(cell_shape, x, y):
+    """ returns the list of coords of the neighbours of a cell"""
     if cell_shape == SQUARE:
         return squ_neighbours_of(x, y)
     elif cell_shape == HEX: 
@@ -291,6 +290,9 @@ def hollow_rect(x1, y1, x2, y2):
 
 
 def triangle(cell_shape, xa, ya, xh, yh, iAngle):
+    """Returns a list of (x, y) coordinates in a triangle
+    A is the top of the triangle, H if the middle of the base
+    """
     if cell_shape == SQUARE:
         return triangle_sq(xa, ya, xh, yh, iAngle)
     elif cell_shape == HEX: 
@@ -299,6 +301,9 @@ def triangle(cell_shape, xa, ya, xh, yh, iAngle):
         raise ValueError("'geometry' has to be a value from GRID_GEOMETRIES")
 
 def triangle3d(cell_shape, xa, ya, za, xh, yh, zh, iAngle):
+    """Returns a list of (x, y, z) coordinates in a 3d-cone
+    A is the top of the cone, H if the center of the base
+    """
     if cell_shape == SQUARE:
         return triangle_sq_3d(xa, ya, za, xh, yh, zh, iAngle)
     elif cell_shape == HEX: 
@@ -600,14 +605,9 @@ def hex_distance_cube(xa, ya, za, xb, yb, zb):
     return max(abs(xa - xb), abs(ya - yb), abs(za - zb))
 
 def distance_off(xa, ya, xb, yb):
-        """ distance between A and B (offset coordinates)"""
-        # 10 times quicker if no conversion...
-        xua, yua, zua = cv_off_cube(xa, ya)
-        xub, yub, zub = cv_off_cube(xb, yb)
-        return max(abs(xua - xub), abs(yua - yub), abs(zua - zub))
-    
-
-
-    
-    
+    """ distance between A and B (offset coordinates)"""
+    # 10 times quicker if no conversion...
+    xua, yua, zua = cv_off_cube(xa, ya)
+    xub, yub, zub = cv_off_cube(xb, yb)
+    return max(abs(xua - xub), abs(yua - yub), abs(zua - zub))
     

+ 151 - 34
pypog/pencil.py

@@ -1,100 +1,217 @@
 '''
-Created on 5 déc. 2016
-
+Created on 5 dec. 2016
+    Pencil classes allow you to get an evolving selection of coordinates, according to the type of pencil you use.
+    
+    Start it, set size (default: 1), update it as many times you want with new positions on the grid, then
+    get the list of selected coordinates, or the list of added / removed coorinates by the last update.
+    
+    Connect those functions to your graphical grid, and let it do the job.
+    
+    Example of use:
+        On mouse left button down on a (x, y) cell: pencil.start(x, y)
+        On hover event on cells: pencil.update(), then display the result on your grid
+        On mouse left button up: Apply the modification on your grid, then delete your pencil object
+        
 @author: olinox
 '''
 from abc import ABCMeta
 
-from pypog.geometry import line2d, zone
+from prompt_toolkit import selection
+
+from pypog import geometry
+from pypog.Grid import HexGrid
 
 
 class BasePencil(object):
+    """Base class of all pencils
+    This is an abstract class: override it!"""
     __metaclass__ = ABCMeta
     
     def __init__(self, grid):
+        
+        # do we really need the grid ref? cell_shape could be enough?
         self._grid = grid
         
         self._origin = None
         self._position = None
+        self._positions_history = []
         
         self._size = 1
-        self._selection = []
+        self._selection = set()
 
-        self._added = []
-        self._removed = []
+        self._added = set()
+        self._removed = set()
 
     @property
     def origin(self):
-        return self._coord0
-
-    @origin.setter
-    def origin(self, x, y):
-        self._origin = (x, y)
+        """returns the coordinates of the cell where the painting began
+        This property is read-only: use the 'start' method to set it"""
+        return self._origin
 
     @property
     def size(self):
+        """returns the current size of the pencil"""
         return self._size
 
     @size.setter
     def size(self, size):
+        """sets the current size of the pencil
+        size has to be a strictly positive integer"""
+        if not isinstance(size, int):
+            raise TypeError("size has to be an integer (given: {})".format(size))
         if not size > 0:
-            raise ValueError("size has to be strictly positive")
+            raise ValueError("size has to be strictly positive (given: {})".format(size))
         self._size = size
 
     @property
     def position(self):
+        """returns the current (x, y) position of the pencil
+        This property is read-only: use the 'update' method to give it a new value"""
         return self._position
 
-    @position.setter
-    def position(self, x, y):
-        self._position = (x, y)
-        self._update()
-
     @property
     def selection(self):
-        return self._selection
+        """return the current list of coordinates selected by the pencil (read-only)"""
+        return list( self._selection )
         
     @property
     def added(self):
-        return self._added
+        """return the list of coordinates added to the last selection by the last update (read-only)
+        
+        This property exists for performances reasons: you probably don't want to update your whole graphic scene if 
+        just one or two cells where added or removed from the selection!
+        """
+        return list( self._added )
 
     @property
     def removed(self):
-        return self._removed
+        """return the list of coordinates removed from the last selection by the last update (read-only)
+                
+        This property exists for performances reasons: you probably don't want to update your whole graphic scene if 
+        just one or two cells where added or removed from the selection!
+        """
+        return list( self._removed )
 
     def _update(self):
-        pass
-        
+        """this method update the selection, added and removed lists, according to the current size, position, origin
+        Override it to create your own pencil"""
+        # this method should be overrided to update : self._selection, self._added, self._removed
+        raise NotImplementedError("_update method has to be overrided")
+
+    def start(self, x0, y0):
+        """start a new painting
+        (x0, y0) is the origin of the painting, and will not be modified."""
+        if len(self._selection) > 0:
+            raise Exception("the pencil has already been started")
+        self._origin = (x0, y0)
+        self.update(x0, y0)
+
+    def update(self, x, y):
+        """Updates the current position of the pencil
+        If (x, y) is not in the position's history, it updates the current selection, and the added / removed lists."""
+        if not all(isinstance(var, int) for var in (x, y)):
+            raise TypeError("x, y has to be an integers (given: {})".format((x, y)))
+        if self._origin == None:
+            raise Exception("Pencil has to be started before any update: use 'start' method")
+        self._position = (x, y)
+        if not (x, y) in  self._positions_history:
+            self._positions_history.append( (x, y) )
+            self._update()
         
 class LinePencil(BasePencil):
+    """Paint a 2d line between origin and position"""
     def __init__(self, *args):
-        BasePencil.__init__(*args)
+        BasePencil.__init__(self, *args)
         
     def _update(self):
-        x0, y0 = self.origin
-        x, y = self.position
+        x0, y0 = self._origin
+        x, y = self._position
         
+        # use a set because of performance (should we generalize the use of sets for coordinates lists?)
         result = set([])
-        line = line2d(self._grid.cell_shape, x0, y0, x, y)
+        
+        line = set( geometry.line2d(self._grid.cell_shape, x0, y0, x, y) )
+        
+        # apply size with geometry.zone
         for x, y in line:
-            result |= set( zone(self._grid.cell_shape, x, y, self.size) )
+            result |= set( geometry.zone(self._grid.cell_shape, x, y, self.size) )
         
-        self._added = list( result - self._selection )
-        self._removed = list( self._selection - result ) 
-        self._selection = list( result )
+        self._added = result - self._selection
+        self._removed = self._selection - result
+        self._selection = result
 
 
 class SimplePencil(BasePencil):
+    """Free handed pencil"""
     def __init__(self, *args):
-        BasePencil.__init__(*args)
+        BasePencil.__init__(self, *args)
         
     def _update(self):
         x, y = self.position
-        zone = zone(self._grid.cell_shape, x, y, self.size)
+        zone_set = set( geometry.zone(self._grid.cell_shape, x, y, self.size) )
 
-        self._added = list( set(zone) - set(self._selection) )
-        self._selection = list( set(self._selection) + set(zone))
+        self._added = zone_set - self._selection
+        # there can't be any removed coordinates with this pencil
+        self._selection = self._selection + zone_set
         
         
+class PaintPotPencil(BasePencil):
+    """This particular pencil selects all cells of same nature from nearest to nearest
+    
+    The 'same nature' is confirmed by passing a comparaison method to the pencil
+    This method should take x1, y1, x2, y2 as args, and return a boolean: True for similar cells, False in other case
+    
+    WARNING: this pencil result is not modified by the update method, because the result is already defined when it starts
+    
+    WARNING 2: take care of what you put in your comparing method, it could end with a infinite loop if you are not working on a finite grid!
+    
+    example of use:
+        
+        # your comparison function
+        my_grid = HexGrid(30,30)
+        
+        def identical_cells_function(x1, y1, x2, y2):
+            if my_grid.cell(x1, y1).color == my_grid.cell(x2, y2).color:
+                return True
+            return False
+            
+        my_pencil = PaintPotPencil(my_grid)
+        my_pencil.start(3, 3, identical_cells_function)
+        
+        print(my_pencil.selection)
+    
+    """
+    
+    def __init__(self, *args):
+        BasePencil.__init__(self, *args)
+        self._comparing_method = (lambda x: False)
+
+    def start(self, x0, y0, comparing_method_pointer):
+        self._comparing_method_pointer = comparing_method_pointer
+        BasePencil.start(self, x0, y0)
+
+    def update(self, *args):
+        if not len(self._selection) > 0:
+            BasePencil.update(self, *args)
+
+    @property
+    def added(self):
+        return self._selection
+
+    def _update(self):
+        x0, y0 = self._origin 
+        current_selection = { (x0, y0) }
+        buffer = set( geometry.neighbours_of(self._grid._cell_shape, x0, y0) )
+        
+        while len(buffer) > 0:
+            x, y = buffer.pop()
+            if self._comparing_method_pointer(x0, y0, x, y):
+                current_selection.add( (x, y) )
+                buffer |= ( set( geometry.neighbours_of(self._grid._cell_shape, x, y) ) - current_selection)
+                
+        self._selection = current_selection
+        
+        
+