Quellcode durchsuchen

refactoring part 1: gridlib created

olinox vor 8 Jahren
Ursprung
Commit
0cabbe8d3d
6 geänderte Dateien mit 661 neuen und 825 gelöschten Zeilen
  1. 2 158
      pypog/Piece.py
  2. 0 612
      pypog/geometry.py
  3. 0 35
      pypog/graphic.py
  4. 639 0
      pypog/gridlib.py
  5. 15 16
      pypog/painting.py
  6. 5 4
      pypog/pathfinding.py

+ 2 - 158
pypog/grid_objects.py → pypog/Piece.py

@@ -1,162 +1,8 @@
 '''
-    Game Grid
+Created on 6 mars 2017
 
-    ** By Cro-Ki l@b, 2017 **
+@author: olinox
 '''
-from pypog import geometry
-from pypog import pathfinding
-
-
-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
-        self.cell_shape = cell_shape
-
-        self._width = 0
-        self.width = width
-        self._height = 0
-        self.height = height
-
-        self._roof = roof
-
-        self._cells = {}
-        self._build()
-
-    def _build(self):
-        for x in range(self.width):
-            for y in range(self.height):
-                cell = Cell(self.cell_shape, x, y)
-                self._cells[(x, y)] = cell
-
-    # properties
-    @property
-    def cell_shape(self):
-        return self._cell_shape
-
-    @cell_shape.setter
-    def cell_shape(self, cell_shape):
-        if not cell_shape in geometry.CELL_SHAPES:
-            raise ValueError("'cell_shape' has to be a value from CELL_SHAPES")
-        self._cell_shape = cell_shape
-
-    @property
-    def width(self):
-        return self._width
-
-    @width.setter
-    def width(self, width):
-        if not isinstance(width, int) or not width > 0:
-            raise ValueError("'width' has to be a strictly positive integer")
-        self._width = width
-
-    @property
-    def height(self):
-        return self._height
-
-    @height.setter
-    def height(self, height):
-        if not isinstance(height, int) or not height > 0:
-            raise ValueError("'width' has to be a strictly positive integer")
-        self._height = height
-
-    @property
-    def roof(self):
-        return self._roof
-
-    def cell(self, x, y):
-        return self._cells[(x, y)]
-
-    @property
-    def cells(self):
-        return self._cells
-
-
-    # geometric methods
-    def cases_number(self):
-        return self.height * self.width
-
-    def in_grid(self, x, y):
-        """return True if the coordinates are in the grid"""
-        return (x > 0 and x <= self._width and y > 0 and y <= self._height)
-
-    def line(self, x1, y1, x2, y2):
-        return geometry.line(self.cell_shape, x1, y1, x2, y2)
-
-    def line3d(self, x1, y1, z1, x2, y2, z2):
-        return geometry.line3d(self.cell_shape, x1, y1, z1, x2, y2, z2)
-
-    def zone(self, x, y, radius):
-        return geometry.zone(self.cell_shape, x, y, radius)
-
-    def triangle(self, xa, ya, xh, yh, iAngle):
-        return geometry.triangle(self.cell_shape, xa, ya, xh, yh, iAngle)
-
-    def triangle3d(self, xa, ya, za, xh, yh, zh, iAngle):
-        return geometry.triangle3d(self.cell_shape, xa, ya, za, xh, yh, zh, iAngle)
-
-    def rect(self, x1, y1, x2, y2):
-        return geometry.rectangle(x1, y1, x2, y2)
-
-    def hollow_rect(self, x1, y1, x2, y2):
-        return geometry.hollow_rectangle(x1, y1, x2, y2)
-
-
-    # pathfinding methods
-    def moving_cost(self, *args):
-        return 1
-
-    def path(self, x1, y1, x2, y2):
-        return pathfinding.path(self, (x1, y1), (x2, y2), self.moving_cost_function)
-
-
-class HexGrid(Grid):
-    def __init__(self, width, height):
-        Grid.__init__(self, geometry.FLAT_HEX, width, height)
-
-class SquareGrid(Grid):
-    def __init__(self, width, height):
-        Grid.__init__(self, geometry.SQUARE, width, height)
-
-
-
-class Cell(object):
-    def __init__(self, cell_shape, x, y, z=0):
-        self._cell_shape = cell_shape
-        self._x = x
-        self._y = y
-        self._z = z
-
-    @property
-    def x(self):
-        return self._x
-
-    @property
-    def y(self):
-        return self._y
-
-    @property
-    def z(self):
-        return self._z
-
-    @property
-    def coord(self):
-        return (self._x, self._y)
-
-    @property
-    def coord3d(self):
-        return (self._x, self._y, self._z)
-
-    def __repr__(self):
-        return "Cell {}".format(self.coord)
-
 
 class Piece(object):
     def __init__(self):
@@ -296,5 +142,3 @@ class Piece(object):
         """pivot the Piece i times (counterclockwise rotation, i can be negative)"""
         new_rotation = self.rotation + i
         self.rotation = new_rotation % self.cell_shape
-
-

+ 0 - 612
pypog/geometry.py

@@ -1,612 +0,0 @@
-'''
-    Geometric functions on hexagonal or square grids
-
-    2D functions return lists of (x, y) coordinates
-    3D functions return lists of (x, y, z) coordinates
-
-    * neighbours_of function return the list of the cells around the (x, y) cell
-    * zone function return the list of the cells surrounding the (x, y) cell within a 'radius' distance
-    * line2d function return the list of the cells on a line between the (x1, y1) cell and the (x2, y2) cell
-    * line3d function return the list of the cells on a line between the (x1, y1, z1) cell and the (x2, y2, z2) cell
-    * rect function return the list of the cells in the rectangle between the cells (x1, y1), (x2, y1), , (x2, y2) and , (x1, y2)
-    * 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
-
-    Performances are given for dimensions of 1, 10, 100
-    ex: line(0,0,1,1), line(0,0,10,10), line(0,0,100,100) will give (x ms. / y ms. / z ms).
-
-
-
-    ** By Cro-Ki l@b, 2017 **
-'''
-from math import sqrt
-
-# ## Cell shapes
-SQUARE = 4
-FLAT_HEX = 61
-TOP_HEX = 62
-CELL_SHAPES = (SQUARE, FLAT_HEX, TOP_HEX)
-
-class UnknownCellShape(ValueError):
-    msg = "'cell_shape' has to be a value from CELL_SHAPES"
-
-# ## NEIGHBOURS
-
-def neighbours(cell_shape, x, y):
-    """ returns the list of coords of the neighbours of a cell"""
-    if cell_shape == SQUARE:
-        return squ2_neighbours(x, y)
-    elif cell_shape == FLAT_HEX:
-        return fhex2_neighbours(x, y)
-    elif cell_shape == TOP_HEX:
-        raise NotImplementedError()
-    else:
-        raise UnknownCellShape()
-
-def fhex2_neighbours(x, y):
-    """ returns the list of coords of the neighbours of a cell on an hexagonal grid """
-    # (0.0148 / - / -)
-    if x % 2 == 0:
-        return [(x, y - 1), (x + 1, y - 1), (x + 1, y), (x, y + 1), (x - 1, y), (x - 1, y - 1)]
-    else:
-        return [(x, y - 1), (x + 1, y), (x + 1, y + 1), (x, y + 1), (x - 1, y + 1), (x - 1, y)]
-
-def squ2_neighbours(x, y):
-    """ returns the list of coords of the neighbours of a cell on an square grid"""
-    # (0.0152 / - / -)
-    return [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
-            (x - 1, y), (x + 1, y)  , \
-            (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
-
-def zone(cell_shape, x0, y0, radius):
-    """ returns the list of the coordinates of the cells in the zone around (x0, y0)
-    """
-    # 0.0311 / 17.2039 / ?
-    if not all(isinstance(c, int) for c in [x0, y0, radius]):
-        raise TypeError("x0, y0, radius have to be integers")
-    if not radius >= 0:
-        raise ValueError("radius has to be positive")
-    buffer = frozenset([ (x0, y0) ])
-
-    for _ in range(0, radius):
-        current = buffer
-        for x, y in current:
-            buffer |= frozenset(neighbours(cell_shape, x, y))
-
-    return list(buffer)
-
-
-# ## LINES (implementations of bresenham algorithm)
-
-def line(cell_shape, x1, y1, x2, y2):
-    """returns a line from x1,y1 to x2,y2
-    grid could be one of the GRIDTYPES values"""
-    if not all(isinstance(c, int) for c in [x1, y1, x2, y2]):
-        raise TypeError("x1, y1, x2, y2 have to be integers")
-    if cell_shape == FLAT_HEX:
-        return fhex2_line(x1, y1, x2, y2)
-    elif cell_shape == SQUARE:
-        return squ2_line(x1, y1, x2, y2)
-    elif cell_shape == TOP_HEX:
-        raise NotImplementedError()
-    else:
-        raise UnknownCellShape()
-
-def line3d(cell_shape, x1, y1, z1, x2, y2, z2):
-    """returns a line from x1,y1,z1 to x2,y2,z2
-    grid could be one of the GRIDTYPES values"""
-    if not all(isinstance(c, int) for c in [z1, z2]):
-        raise TypeError("x1, y1, z1, x2, y2, z2 have to be integers")
-    hoLine = line(cell_shape, x1, y1, x2, y2)
-    if z1 == z2:
-        return [(x, y, z1) for x, y in hoLine]
-    else:
-        ligneZ = squ2_line(0, z1, (len(hoLine) - 1), z2)
-        return [(hoLine[d][0], hoLine[d][1], z) for d, z in ligneZ]
-
-def squ2_line(x1, y1, x2, y2):
-    """Line Bresenham algorithm for square grid"""
-    # 0.0195 / 0.0222 / 0.0517
-    result = []
-
-    if (x1, y1) == (x2, y2):
-        return [(x1, y1)]
-
-    # DIAGONAL SYMETRY
-    V = (abs(y2 - y1) > abs(x2 - x1))
-    if V: y1, x1, y2, x2 = x1, y1, x2, y2
-
-    # VERTICAL SYMETRY
-    reversed_sym = (x1 > x2)
-    if reversed_sym:
-        x2, y2, x1, y1 = x1, y1, x2, y2
-
-    DX = x2 - x1 ; DY = y2 - y1
-    offset = 0.0
-    step = 1 if DY > 0 else -1
-    alpha = (abs(DY) / DX)
-
-    y = y1
-    for x in range(x1, x2 + 1):
-        coord = (y, x) if V else (x, y)
-        result.append(coord)
-
-        offset += alpha
-        if offset > 0.5:
-            y += step
-            offset -= 1.0
-
-    if reversed_sym:
-        result.reverse()
-    return result
-
-def fhex2_line(x1, y1, x2, y2):
-    """Line Bresenham algorithm for hexagonal grid"""
-    # 0.0220 / 0.0388 / 0.1330
-
-    if (x1, y1) == (x2, y2):
-        return [(x1, y1)]
-
-    reversed_sym = (x1 > x2)
-    if reversed_sym:
-        x1, x2 = x2, x1
-        y1, y2 = y2, y1
-
-    if abs(x2 - x1) < (2 * abs((y2 - y1)) + abs(x2 % 2) - abs(x1 % 1)):
-        # vertical quadrants
-
-        # unit is half the width: u = 0.5773
-        # half-height is then 0.8860u, or sqrt(3)/2
-        direction = 1 if y2 > y1 else -1
-
-        dx = 1.5 * (x2 - x1)
-        dy = direction * (y2 - y1)
-        if (x1 + x2) % 2 == 1:
-            if x1 % 2 == 0:
-                dy += direction * 0.5
-            else:
-                dy -= direction * 0.5
-
-        k = dx / (dy * sqrt(3))
-        pas = 0.5 * sqrt(3)
-
-        result = []
-        offset = 0.0
-        pos = (x1, y1)
-        result.append(pos)
-
-        while pos != (x2, y2):
-            offset += (k * pas)
-            if offset <= 0.5:
-                x, y = pos
-                pos = x, y + direction
-                result.append(pos)
-                offset += (k * pas)
-            else:
-                x, y = pos
-                if (x % 2 == 0 and direction == 1) or (x % 2 == 1 and direction == -1):
-                    pos = x + 1, y
-                else:
-                    pos = x + 1, y + direction
-                result.append(pos)
-                offset -= 1.5
-
-            # in case of error in the algorithm, we should avoid infinite loop:
-            if direction * pos[1] > direction * y2:
-                result = []
-                break
-
-    else:
-        # horizontal quadrants
-        dx = x2 - x1 ; dy = y2 - y1
-        if (x1 + x2) % 2 == 1:
-            dy += 0.5 if x1 % 2 == 0 else -0.5
-
-        k = dy / dx
-        pas = 1
-
-        result = []
-        d = 0.0
-        pos = (x1, y1)
-        result.append(pos)
-
-        while pos != (x2, y2):
-            d += k * pas
-            if d > 0:
-                x, y = pos
-                if x % 2 == 0:
-                    pos = x + 1, y
-                else:
-                    pos = x + 1, y + 1
-                result.append(pos)
-                d -= 0.5
-            else:
-                x, y = pos
-                if x % 2 == 0:
-                    pos = x + 1, y - 1
-                else:
-                    pos = x + 1, y
-                result.append(pos)
-                d += 0.5
-
-            # in case of error in the algorithm, we should avoid infinite loop:
-            if pos[0] > x2:
-                result = []
-                break
-
-    if reversed_sym:
-        result.reverse()
-    return result
-
-
-# ## RECTANGLES
-
-def rectangle(x1, y1, x2, y2):
-    """return a list of cells in a rectangle between (X1, Y1), (X2, Y2)"""
-    # squ: 0.0226 / 0.0361 / 1.0091
-    # fhex: ? / ? / ?
-    if not all(isinstance(val, int) for val in [x1, y1, x2, y2]):
-        raise TypeError("x1, y1, x2, y2 should be integers")
-    xa, ya, xb, yb = min([x1, x2]), min([y1, y2]), max([x1, x2]), max([y1, y2])
-    return [(x, y) for x in range(xa, xb + 1) for y in range(ya, yb + 1)]
-
-def hollow_rectangle(x1, y1, x2, y2):
-    """return a list of cells composing the sides of the rectangle between (X1, Y1), (X2, Y2)"""
-    # squ: 0.0254 / 0.0590 / 2.6669
-    # fhex: ? / ? / ?
-    if not all(isinstance(val, int) for val in [x1, y1, x2, y2]):
-        raise TypeError("x1, y1, x2, y2 should be integers")
-    return [(x, y) for x, y in rectangle(x1, y1, x2, y2)
-            if (x == x1 or x == x2 or y == y1 or y == y2)]
-
-
-# ## TRIANGLES
-
-ANGLES = (1, 2, 3)
-
-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 not all(isinstance(c, int) for c in [xa, ya, xh, yh]):
-        raise TypeError("xa, ya, xh, yh should be integers")
-    if not iAngle in ANGLES:
-        raise ValueError("iAngle should be one of the ANGLES values")
-
-    if cell_shape == SQUARE:
-        return squ2_triangle(xa, ya, xh, yh, iAngle)
-    elif cell_shape == FLAT_HEX:
-        return fhex2_triangle(xa, ya, xh, yh, iAngle)
-    elif cell_shape == TOP_HEX:
-        raise NotImplementedError()
-    else:
-        raise UnknownCellShape()
-
-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
-
-    WARNING: result is a dictionnary of the form {(x, y): (-z, +z)}
-
-    This is for performance reason, because on a 2d grid, you generrally don't need a complete list of z coordinates
-    as you don't want to display them: you just want to know if an altitude is inside a range.
-
-    That could change in later version
-    """
-    # TODO: review the result form
-
-    if not all(isinstance(c, int) for c in [za, zh]):
-        raise TypeError("xa, ya, za, xh, yh, zh should be integers")
-        # a triangle2d will be built during algo, so other args will be checked later
-
-    if cell_shape == SQUARE:
-        return squ3_triangle(xa, ya, za, xh, yh, zh, iAngle)
-    elif cell_shape == FLAT_HEX:
-        return fhex3_triangle(xa, ya, za, xh, yh, zh, iAngle)
-    elif cell_shape == TOP_HEX:
-        raise NotImplementedError()
-    else:
-        raise UnknownCellShape()
-
-
-def squ2_triangle(xa, ya, xh, yh, iAngle):
-    """ triangle algorithm on square grid
-    """
-    # 0.0393 / 0.1254 / 40.8067
-
-    if (xa, ya) == (xh, yh):
-        return [(xa, ya)]
-
-    result = []
-
-    # direction vector
-    dx_dir, dy_dir = xh - xa, yh - ya
-
-    # normal vector
-    dx_n, dy_n = -dy_dir, dx_dir
-
-    # B and C positions
-    k = 1 / (iAngle * sqrt(3))
-    xb, yb = xh + (k * dx_n), yh + (k * dy_n)
-    xc, yc = xh + (-k * dx_n), yh + (-k * dy_n)
-
-    xb, yb = round(xb), round(yb)
-    xc, yc = round(xc), round(yc)
-
-    # sides:
-    lines = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
-
-    # base (lower slope)
-    x1, y1, x2, y2 = min(lines, key=lambda x: (abs ((x[3] - x[1]) / (x[2] - x[0])) if x[2] != x[0] else 10 ** 10))
-    base = line(SQUARE, x1, y1, x2, y2)
-    y_base = y1
-    lines.remove((x1, y1, x2, y2))
-
-    # 'hat' (2 other sides)
-    hat = []
-    y_top = None
-    for x1, y1, x2, y2 in lines:
-        if y_top == None:
-            y_top = y2
-        hat.extend(line(SQUARE, x1, y1, x2, y2))
-
-    # sense (1 if top is under base, -1 if not)
-    sense = 1 if y_top > y_base else -1
-
-    # rove over y values from base to hat
-    for x, y in base:
-        while not (x, y) in hat:
-            result.append((x, y))
-            y += sense
-    result.extend(hat)
-
-    return result
-
-def fhex2_triangle(xa, ya, xh, yh, iAngle):
-    """  triangle algorithm on hexagonal grid
-    """
-    # 0.0534 / 0.2351 / 111.8207
-
-    if (xa, ya) == (xh, yh):
-        return [(xa, ya)]
-
-    result = []
-
-    # convert to cubic coodinates (see 'cube_coords' lib)
-    xua, yua, _ = cv_off_cube(xa, ya)
-    xuh, yuh, zuh = cv_off_cube(xh, yh)
-
-    # direction vector
-    dx_dir, dy_dir = xuh - xua, yuh - yua
-
-    # normal vector
-    dx_n, dy_n = -(2 * dy_dir + dx_dir), (2 * dx_dir + dy_dir)
-    dz_n = (-dx_n - dy_n)
-
-    # B and C positions
-    k = 1 / (iAngle * sqrt(3))
-    xub, yub, zub = xuh + (k * dx_n), yuh + (k * dy_n), zuh + (k * dz_n)
-    xuc, yuc, zuc = xuh + (-k * dx_n), yuh + (-k * dy_n), zuh + (-k * dz_n)
-
-    xub, yub, zub = cube_round(xub, yub, zub)
-    xuc, yuc, zuc = cube_round(xuc, yuc, zuc)
-
-    xb, yb = cv_cube_off(xub, yub, zub)
-    xc, yc = cv_cube_off(xuc, yuc, zuc)
-
-    # sides
-    segments = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
-
-    # base (lower slope)
-    x1, y1, x2, y2 = min(segments, key=lambda x: (abs ((x[3] - x[1]) / (x[2] - x[0])) if x[2] != x[0] else 10 ** 10))
-    base = line(FLAT_HEX, x1, y1, x2, y2)
-    y_base = y1
-    segments.remove((x1, y1, x2, y2))
-
-    # 'hat' (the 2 other sides)
-    chapeau = []
-    y_sommet = None
-    for x1, y1, x2, y2 in segments:
-        if y_sommet == None:
-            y_sommet = y2
-        chapeau.extend(line(FLAT_HEX, x1, y1, x2, y2))
-
-    # sense (1 if top is under base, -1 if not)
-    sens = 1 if y_sommet > y_base else -1
-
-    # rove over y values from base to hat
-    for x, y in base:
-        while not (x, y) in chapeau:
-            result.append((x, y))
-            y += sens
-    result.extend(chapeau)
-
-    return result
-
-
-def squ3_triangle(xa, ya, za, xh, yh, zh, iAngle):
-    """ 3d triangle algorithm on square grid"""
-    result = []
-
-    flat_triangle = triangle(SQUARE, xa, ya, xh, yh, iAngle)
-    k = 1 / (iAngle * sqrt(3))
-
-    length = max(abs(xh - xa), abs(yh - ya))
-
-    vertical_line = line(SQUARE, 0, za, length, zh)
-
-    # build a dict with X key and value is a list of Z values
-    vertical_line_dict = {d:[] for d, z in vertical_line}
-    for d, z in vertical_line:
-        vertical_line_dict[d].append(z)
-
-    # this is approximative: height is update according to the manhattan distance to center
-    for x, y in flat_triangle:
-        distance = int(max(abs(x - xa), abs(y - ya)))
-        try:
-            z_list = vertical_line_dict[ distance ]
-        except KeyError:
-            distance = length
-            z_list = vertical_line_dict[ distance ]
-        dh = int(k * distance) + 1 if distance > 0 else 0
-        result[ (x, y) ] = ((min(z_list) - dh) , (max(z_list) + dh))
-    return result
-
-def fhex3_triangle(xa, ya, za, xh, yh, zh, iAngle):
-    """ 3d triangle algorithm on hexagonal grid """
-
-    flat_triangle = triangle(FLAT_HEX, xa, ya, xh, yh, iAngle)
-
-    result = {}
-
-    k = 1 / (iAngle * sqrt(3))
-
-    # use cubic coordinates
-    xua, yua, zua = cv_off_cube(xa, ya)
-    xuh, yuh, zuh = cv_off_cube(xh, yh)
-
-    length = max(abs(xuh - xua), abs(yuh - yua), abs(zuh - zua))
-
-    vertical_line = line(SQUARE, 0, za, length, zh)
-
-    # build a dict with X key and value is a list of Z values
-    vertical_line_dict = {d:[] for d, z in vertical_line}
-    for d, z in vertical_line:
-        vertical_line_dict[d].append(z)
-
-    # this is approximative: height is update according to the manhattan distance to center
-    for x, y in flat_triangle:
-        xu, yu, zu = cv_off_cube(x, y)
-        distance = int(max(abs(xu - xua), abs(yu - yua), abs(zu - zua)))
-        try:
-            z_list = vertical_line_dict[ distance ]
-        except KeyError:
-            distance = length
-            z_list = vertical_line_dict[ distance ]
-        dh = int(k * distance) + 1 if distance > 0 else 0
-        result[ (x, y) ] = ((min(z_list) - dh) , (max(z_list) + dh))
-    return result
-
-
-# ## TRANSLATIONS / ROTATIONS
-
-def pivot(cell_shape, center, coordinates, rotations):
-    """pivot 'rotations' times the coordinates (list of (x, y) tuples)
-    around the center coordinates (x,y)
-    Rotation is counterclockwise"""
-
-    # check the args:
-    try:
-        x, y = center
-    except ValueError:
-        raise TypeError("'center' should be an tuple of (x, y) coordinates with x and y integers (given: {})".format(center))
-    if not isinstance(x, int) or not isinstance(y, int):
-        raise ValueError("'center' should be an tuple of (x, y) coordinates with x and y integers (given: {})".format(center))
-
-    try:
-        for coord in coordinates:
-            try:
-                x, y = coord
-                if not isinstance(x, int) or not isinstance(y, int):
-                    raise ValueError()
-            except ValueError:
-                raise ValueError("'coordinates' should be an list of (x, y) coordinates with x and y integers (given: {})".format(coordinates))
-    except TypeError:
-        raise TypeError("'coordinates' should be an list of (x, y) coordinates with x and y integers (given: {})".format(coordinates))
-
-    if not isinstance(rotations, int):
-        raise TypeError("'rotations' should be an integer (given: {})".format(rotations))
-
-    # call the method according to cells shape
-    if cell_shape == SQUARE:
-        return squ2_pivot(center, coordinates, rotations)
-    elif cell_shape == FLAT_HEX:
-        return fhex2_pivot(center, coordinates, rotations)
-    elif cell_shape == TOP_HEX:
-        raise NotImplementedError()
-    else:
-        raise UnknownCellShape()
-
-def fhex2_pivot(center, coordinates, rotations):
-    """pivot 'rotations' times the coordinates (list of (x, y) tuples)
-    around the center coordinates (x,y)
-    On hexagonal grid, rotates of 60 degrees each time"""
-    # ? / ? / ?
-
-    if coordinates == [center] or rotations % 6 == 0:
-        return coordinates
-    x0, y0 = center
-    xu0, yu0, zu0 = cv_off_cube(x0, y0)
-    result = []
-
-    for x, y in coordinates:
-        xu, yu, zu = cv_off_cube(x, y)
-        dxu, dyu, dzu = xu - xu0, yu - yu0, zu - zu0
-        for _ in range(rotations):
-            dxu, dyu, dzu = -dzu, -dxu, -dyu
-        xru, yru, zru = dxu + xu0, dyu + yu0, dzu + zu0
-        xr, yr = cv_cube_off(xru, yru, zru)
-        result.append((xr, yr))
-    return result
-
-def squ2_pivot(center, coordinates, rotations):
-    """pivot 'rotations' times the coordinates (list of (x, y) tuples)
-    around the center coordinates (x,y)
-    On square grid, rotates of 90 degrees each time"""
-    # ? / ? / ?
-
-    if coordinates == [center] or rotations % 4 == 0:
-        return coordinates
-    x0, y0 = center
-    result = []
-    for x, y in coordinates:
-        dx, dy = x - x0, y - y0
-        for _ in range(rotations):
-            dx, dy = dy, -dx
-        xr, yr = dx + x0, dy + y0
-        result.append((xr, yr))
-    return result
-
-
-# ## CUBIC COORDINATES
-def cv_cube_off(xu, yu, zu):
-    """convert cubic coordinates (xu, yu, zu) in standards coordinates (x, y) [offset]"""
-    y = int(xu + (zu - (zu & 1)) / 2)
-    x = zu
-    return (x, y)
-
-def cv_off_cube(x, y):
-    """converts standards coordinates (x, y) [offset] in cubic coordinates (xu, yu, zu)"""
-    zu = x
-    xu = int(y - (x - (x & 1)) / 2)
-    yu = int(-xu - zu)
-    return (xu, yu, zu)
-
-# > unused
-def cube_round(x, y, z):
-    """returns the nearest cell (in cubic coords)
-    x, y, z can be floating numbers, no problem."""
-    rx, ry, rz = round(x), round(y), round(z)
-    x_diff, y_diff, z_diff = abs(rx - x), abs(ry - y), abs(rz - z)
-    if x_diff > y_diff and x_diff > z_diff:
-        rx = -ry - rz
-    elif y_diff > z_diff:
-        ry = -rx - rz
-    else:
-        rz = -rx - ry
-    return (rx, ry, rz)
-
-# > unused
-def hex_distance_cube(xa, ya, za, xb, yb, zb):
-    """returns the manhattan distance between the two cells"""
-    return max(abs(xa - xb), abs(ya - yb), abs(za - zb))
-
-# > unused
-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))

+ 0 - 35
pypog/graphic.py

@@ -1,35 +0,0 @@
-'''
-    Graphical functions
-
-    ** By Cro-Ki l@b, 2017 **
-'''
-from pypog import geometry
-
-def g_cell(shape, *args):
-    if shape == geometry.FLAT_HEX:
-        return g_flathex(*args)
-    elif shape == geometry.SQUARE :
-        return g_square(*args)
-    else:
-        raise geometry.UnknownCellShape()
-
-def g_flathex(x, y, scale=120):
-    if x % 2 != 0:
-        y += 0.5
-    return [
-               (((x * 0.866) + 0.2886) * scale , y * scale), \
-               (((x * 0.866) + 0.866) * scale  , y * scale), \
-               (((x * 0.866) + 1.1547) * scale , (y + 0.5) * scale), \
-               (((x * 0.866) + 0.866) * scale  , (y + 1) * scale), \
-               (((x * 0.866) + 0.2886) * scale , (y + 1) * scale), \
-               ((x * 0.866) * scale          , (y + 0.5) * scale)
-            ]
-
-def g_square(x, y, scale=120):
-    return  [
-                (x * scale, y * scale), \
-                ((x + 1) * scale, y * scale), \
-                ((x + 1) * scale, (y + 1) * scale), \
-                (x * scale, (y + 1) * scale)
-            ]
-

+ 639 - 0
pypog/gridlib.py

@@ -0,0 +1,639 @@
+'''
+    Grid objects
+
+    ** By Cro-Ki l@b, 2017 **
+'''
+from math import sqrt
+
+
+class BaseGrid(object):
+    """ Base class for grids
+    This class should be overriden """
+    def __init__(self, width, height):
+        """ instanciate a new BaseGrid object """
+        self._width = 0
+        self.width = width
+        self._height = 0
+        self.height = height
+
+    def __repr__(self):
+        return "<{} object>".format(self.__class__.__name__)
+
+    @staticmethod
+    def _assertCoordinates(*args):
+        """ raise a ValueError if the args are not (x, y) iterables, where x and y are integers
+        usage:
+            self._assertCoordinates((x1, y1), (x2, y2), ...)
+        """
+        try:
+            if all([isinstance(i, int) for x, y in args for i in (x, y)]):
+                return
+        except (TypeError, ValueError):
+            pass
+        raise ValueError("{} is not a valid (x, y) coordinates iterable".format(args))
+
+    # properties
+    @property
+    def width(self):
+        """ the width of the grid """
+        return self._width
+
+    @width.setter
+    def width(self, width):
+        """ set a new width for the grid.
+        the new width has to be a strictly positive integer"""
+        if not isinstance(width, int) or not width > 0:
+            raise ValueError("'width' has to be a strictly positive integer")
+        self._width = width
+
+    @property
+    def height(self):
+        """ the height of the grid """
+        return self._height
+
+    @height.setter
+    def height(self, height):
+        """ set a new height for the grid.
+        the new height has to be a strictly positive integer"""
+        if not isinstance(height, int) or not height > 0:
+            raise ValueError("'width' has to be a strictly positive integer")
+        self._height = height
+
+    # geometric methods
+    def __len__(self):
+        """ return the number of cells in the grid """
+        return self.height * self.width
+
+    def __contains__(self, key):
+        """return True if the (x, y) coordinates are in the grid"""
+        try:
+            self._assertCoordinates(key)
+        except ValueError:
+            pass
+        else:
+            return 0 <= key[0] < self._width and 0 <= key[1] < self._height
+        return False
+
+    def __iter__(self):
+        """ iterate over the coordinates of the grid """
+        for item in ((x, y) for x in range(self.width) for y in range(self.height)):
+            yield item
+        raise StopIteration()
+
+    @classmethod
+    def _bounding_rect(cls, *args):
+        """ return (xmin, ymin, xmax, ymax) from (x, y) coordinates """
+        cls._assertCoordinates(*args)
+        xs, ys = zip(*args)
+        xs.sort()
+        ys.sort()
+        return xs[0], xs[-1], ys[0], ys[-1]
+
+    # graphical methods
+    @staticmethod
+    def graphicsitem(x, y, scale=120):
+        """ returns the list of the points which compose the (x, y) cell """
+        raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
+
+    # geometrical algorithms
+    @classmethod
+    def neighbors(cls, x, y):
+        """ returns a list of the neighbors of (x, y) """
+        return [key for key in cls._neighbors(x, y) if cls._is_in(key)]
+
+    @classmethod
+    def _neighbors(cls, x, y):
+        raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
+
+    @classmethod
+    def line(cls, x1, y1, x2, y2):
+        raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
+
+    @classmethod
+    def line3d(cls, x1, y1, z1, x2, y2, z2):
+        """ returns a line from (x1 ,y1, z1) to (x2, y2, z2)
+        as a list of (x, y, z) coordinates """
+        if not all(isinstance(c, int) for c in [z1, z2]):
+            raise TypeError("x1, y1, z1, x2, y2, z2 have to be integers")
+        hoLine = cls.line(x1, y1, x2, y2)
+        if z1 == z2:
+            return [(x, y, z1) for x, y in hoLine]
+        else:
+            ligneZ = SquareGrid.line(0, z1, (len(hoLine) - 1), z2)
+            return [(hoLine[d][0], hoLine[d][1], z) for d, z in ligneZ]
+
+    @classmethod
+    def zone(cls, x, y, radius):
+        """ returns the list of the coordinates of the cells in a zone around (x, y)
+        """
+        cls._assertCoordinates((x, y))
+        if not isinstance(radius, int):
+            raise TypeError("radius has to be an integer (given: {})".format(radius))
+        if not radius >= 0:
+            raise ValueError("radius has to be positive")
+        buffer = frozenset([(x, y)])
+
+        for _ in range(0, radius):
+            current = buffer
+            for x, y in current:
+                buffer |= frozenset(cls.neighbors(x, y))
+        return list(buffer)
+
+    @classmethod
+    def triangle(cls, xa, ya, xh, yh, iAngle):
+        """ return the list of the (x, y) coordinates in a triangle
+        with (xa, ya) apex and (xh, yh) middle of the base """
+        raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
+
+    @classmethod
+    def triangle3d(self, 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
+
+        WARNING: result is a dictionary of the form {(x, y): (-z, +z)}
+
+        This is for performance reason and because on a 2d grid, you generally don't need a complete list of z coordinates
+        as you don't want to display them: you just want to know if an altitude is inside a range.
+
+        That could change in later version
+        """
+        raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
+
+    @classmethod
+    def rectangle(cls, x1, y1, x2, y2):
+        """return a list of cells in a rectangle between (X1, Y1), (X2, Y2)"""
+        cls._assertCoordinates((x1, y1), (x2, y2))
+        xa, ya, xb, yb = min([x1, x2]), min([y1, y2]), max([x1, x2]), max([y1, y2])
+        return [(x, y) for x in range(xa, xb + 1) for y in range(ya, yb + 1)]
+
+    @classmethod
+    def hollow_rectangle(cls, x1, y1, x2, y2):
+        """return a list of cells composing the sides of the rectangle between (X1, Y1), (X2, Y2)"""
+        cls._assertCoordinates((x1, y1), (x2, y2))
+        xmin, ymin, xmax, ymax = cls._bounding_rect((x1, y1), (x2, y2))
+        return [(x, ymin) for x in range(xmin, xmax + 1)] + \
+               [(x, ymax) for x in range(xmin, xmax + 1)] + \
+               [(xmin, y) for y in range(ymin, ymax + 1)] + \
+               [(xmax, y) for y in range(ymin, ymax + 1)]
+
+    @classmethod
+    def rotate(cls, center, coordinates, rotations):
+        """ return the 'coordinates' list of (x, y) coordinates
+        after a rotation of 'rotations' times around the (x, y) center """
+        raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
+
+#     def moving_cost(self, *args):
+#         return 1
+#
+#     def path(self, x1, y1, x2, y2):
+#         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
+
+class SquareGrid(BaseGrid):
+    """ Square grid object """
+    def __init__(self, *args, **kwargs):
+        BaseGrid.__init__(self, *args, **kwargs)
+
+    @staticmethod
+    def graphicsitem(x, y, scale=120):
+        """ reimplemented from BaseGrid.graphicsitem """
+        return  [
+                    (x * scale, y * scale), \
+                    ((x + 1) * scale, y * scale), \
+                    ((x + 1) * scale, (y + 1) * scale), \
+                    (x * scale, (y + 1) * scale)
+                ]
+
+    @classmethod
+    def _neighbors(cls, x, y):
+        """ reimplemented from BaseGrid._neighbors """
+        return [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
+                (x - 1, y), (x + 1, y)  , \
+                (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
+
+    @classmethod
+    def line(cls, x1, y1, x2, y2):
+        """ reimplemented from BaseGrid.line
+        Implementation of bresenham's algorithm
+        """
+        result = []
+
+        if (x1, y1) == (x2, y2):
+            return [(x1, y1)]
+
+        # DIAGONAL SYMETRY
+        V = (abs(y2 - y1) > abs(x2 - x1))
+        if V: y1, x1, y2, x2 = x1, y1, x2, y2
+
+        # VERTICAL SYMETRY
+        reversed_sym = (x1 > x2)
+        if reversed_sym:
+            x2, y2, x1, y1 = x1, y1, x2, y2
+
+        DX = x2 - x1 ; DY = y2 - y1
+        offset = 0.0
+        step = 1 if DY > 0 else -1
+        alpha = (abs(DY) / DX)
+
+        y = y1
+        for x in range(x1, x2 + 1):
+            coord = (y, x) if V else (x, y)
+            result.append(coord)
+
+            offset += alpha
+            if offset > 0.5:
+                y += step
+                offset -= 1.0
+
+        if reversed_sym:
+            result.reverse()
+        return result
+
+    @classmethod
+    def triangle(cls, xa, ya, xh, yh, iAngle):
+        """ reimplemented from BaseGrid.triangle """
+        if (xa, ya) == (xh, yh):
+            return [(xa, ya)]
+
+        result = []
+
+        # direction vector
+        dx_dir, dy_dir = xh - xa, yh - ya
+
+        # normal vector
+        dx_n, dy_n = -dy_dir, dx_dir
+
+        # B and C positions
+        k = 1 / (iAngle * sqrt(3))
+        xb, yb = xh + (k * dx_n), yh + (k * dy_n)
+        xc, yc = xh + (-k * dx_n), yh + (-k * dy_n)
+
+        xb, yb = round(xb), round(yb)
+        xc, yc = round(xc), round(yc)
+
+        # sides:
+        lines = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
+
+        # base (lower slope)
+        x1, y1, x2, y2 = min(lines, key=lambda x: (abs ((x[3] - x[1]) / (x[2] - x[0])) if x[2] != x[0] else 10 ** 10))
+        base = cls.line(x1, y1, x2, y2)
+        y_base = y1
+        lines.remove((x1, y1, x2, y2))
+
+        # 'hat' (2 other sides)
+        hat = []
+        y_top = None
+        for x1, y1, x2, y2 in lines:
+            if y_top == None:
+                y_top = y2
+            hat.extend(cls.line(x1, y1, x2, y2))
+
+        # sense (1 if top is under base, -1 if not)
+        sense = 1 if y_top > y_base else -1
+
+        # rove over y values from base to hat
+        for x, y in base:
+            while not (x, y) in hat:
+                result.append((x, y))
+                y += sense
+        result.extend(hat)
+
+        return result
+
+    @classmethod
+    def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle):
+        """ reimplemented from BaseGrid.triangle3d """
+        result = []
+
+        flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
+        k = 1 / (iAngle * sqrt(3))
+
+        length = max(abs(xh - xa), abs(yh - ya))
+
+        vertical_line = cls.line(0, za, length, zh)
+
+        # build a dict with X key and value is a list of Z values
+        vertical_line_dict = {d:[] for d, z in vertical_line}
+        for d, z in vertical_line:
+            vertical_line_dict[d].append(z)
+
+        # this is approximative: height is update according to the manhattan distance to center
+        for x, y in flat_triangle:
+            distance = int(max(abs(x - xa), abs(y - ya)))
+            try:
+                z_list = vertical_line_dict[ distance ]
+            except KeyError:
+                distance = length
+                z_list = vertical_line_dict[ distance ]
+            dh = int(k * distance) + 1 if distance > 0 else 0
+            result[ (x, y) ] = ((min(z_list) - dh) , (max(z_list) + dh))
+        return result
+
+
+    @classmethod
+    def rotate(cls, center, coordinates, rotations):
+        """ reimplemented from BaseGrid.rotate """
+        if coordinates == [center] or rotations % 4 == 0:
+            return coordinates
+        x0, y0 = center
+        result = []
+        for x, y in coordinates:
+            dx, dy = x - x0, y - y0
+            for _ in range(rotations):
+                dx, dy = dy, -dx
+            xr, yr = dx + x0, dy + y0
+            result.append((xr, yr))
+        return result
+
+class _HexGrid(BaseGrid):
+    """ Base class for hexagonal grids classes
+    This class should be overridden """
+    def __init__(self, *args, **kwargs):
+        BaseGrid.__init__(self, *args, **kwargs)
+
+    @staticmethod
+    def cv_cube_off(xu, yu, zu):
+        """convert cubic coordinates (xu, yu, zu) in standards coordinates (x, y) [offset]"""
+        y = int(xu + (zu - (zu & 1)) / 2)
+        x = zu
+        return (x, y)
+
+    @staticmethod
+    def cv_off_cube(x, y):
+        """converts standards coordinates (x, y) [offset] in cubic coordinates (xu, yu, zu)"""
+        zu = x
+        xu = int(y - (x - (x & 1)) / 2)
+        yu = int(-xu - zu)
+        return (xu, yu, zu)
+
+    # > unused
+    @staticmethod
+    def cube_round(x, y, z):
+        """returns the nearest cell (in cubic coords)
+        x, y, z can be floating numbers, no problem."""
+        rx, ry, rz = round(x), round(y), round(z)
+        x_diff, y_diff, z_diff = abs(rx - x), abs(ry - y), abs(rz - z)
+        if x_diff > y_diff and x_diff > z_diff:
+            rx = -ry - rz
+        elif y_diff > z_diff:
+            ry = -rx - rz
+        else:
+            rz = -rx - ry
+        return (rx, ry, rz)
+
+    # > unused
+    @staticmethod
+    def hex_distance_cube(xa, ya, za, xb, yb, zb):
+        """returns the manhattan distance between the two cells"""
+        return max(abs(xa - xb), abs(ya - yb), abs(za - zb))
+
+    # > unused
+    @staticmethod
+    def distance_off(xa, ya, xb, yb):
+        """ distance between A and B (offset coordinates)"""
+        # 10 times quicker if no conversion...
+        xua, yua, zua = FHexGrid.cv_off_cube(xa, ya)
+        xub, yub, zub = FHexGrid.cv_off_cube(xb, yb)
+        return max(abs(xua - xub), abs(yua - yub), abs(zua - zub))
+
+
+class FHexGrid(_HexGrid):
+    """ Flat-hexagonal grid object """
+
+    def __init__(self, *args, **kwargs):
+        _HexGrid.__init__(self, *args, **kwargs)
+
+    @staticmethod
+    def graphicsitem(x, y, scale=120):
+        """ reimplemented from BaseGrid.graphicsitem """
+        if x % 2 != 0:
+            y += 0.5
+        return [
+                   (((x * 0.866) + 0.2886) * scale , y * scale), \
+                   (((x * 0.866) + 0.866) * scale  , y * scale), \
+                   (((x * 0.866) + 1.1547) * scale , (y + 0.5) * scale), \
+                   (((x * 0.866) + 0.866) * scale  , (y + 1) * scale), \
+                   (((x * 0.866) + 0.2886) * scale , (y + 1) * scale), \
+                   ((x * 0.866) * scale          , (y + 0.5) * scale)
+                ]
+
+    @classmethod
+    def _neighbors(cls, x, y):
+        if x % 2 == 0:
+            return [(x, y - 1), (x + 1, y - 1), (x + 1, y), (x, y + 1), (x - 1, y), (x - 1, y - 1)]
+        else:
+            return [(x, y - 1), (x + 1, y), (x + 1, y + 1), (x, y + 1), (x - 1, y + 1), (x - 1, y)]
+
+    @classmethod
+    def line(cls, x1, y1, x2, y2):
+        """ reimplemented from BaseGrid.line
+        Implementation of bresenham's algorithm """
+        if (x1, y1) == (x2, y2):
+            return [(x1, y1)]
+
+        reversed_sym = (x1 > x2)
+        if reversed_sym:
+            x1, x2 = x2, x1
+            y1, y2 = y2, y1
+
+        if abs(x2 - x1) < (2 * abs((y2 - y1)) + abs(x2 % 2) - abs(x1 % 1)):
+            # vertical quadrants
+
+            # unit is half the width: u = 0.5773
+            # half-height is then 0.8860u, or sqrt(3)/2
+            direction = 1 if y2 > y1 else -1
+
+            dx = 1.5 * (x2 - x1)
+            dy = direction * (y2 - y1)
+            if (x1 + x2) % 2 == 1:
+                if x1 % 2 == 0:
+                    dy += direction * 0.5
+                else:
+                    dy -= direction * 0.5
+
+            k = dx / (dy * sqrt(3))
+            pas = 0.5 * sqrt(3)
+
+            result = []
+            offset = 0.0
+            pos = (x1, y1)
+            result.append(pos)
+
+            while pos != (x2, y2):
+                offset += (k * pas)
+                if offset <= 0.5:
+                    x, y = pos
+                    pos = x, y + direction
+                    result.append(pos)
+                    offset += (k * pas)
+                else:
+                    x, y = pos
+                    if (x % 2 == 0 and direction == 1) or (x % 2 == 1 and direction == -1):
+                        pos = x + 1, y
+                    else:
+                        pos = x + 1, y + direction
+                    result.append(pos)
+                    offset -= 1.5
+
+                # in case of error in the algorithm, we should avoid infinite loop:
+                if direction * pos[1] > direction * y2:
+                    result = []
+                    break
+
+        else:
+            # horizontal quadrants
+            dx = x2 - x1 ; dy = y2 - y1
+            if (x1 + x2) % 2 == 1:
+                dy += 0.5 if x1 % 2 == 0 else -0.5
+
+            k = dy / dx
+            pas = 1
+
+            result = []
+            d = 0.0
+            pos = (x1, y1)
+            result.append(pos)
+
+            while pos != (x2, y2):
+                d += k * pas
+                if d > 0:
+                    x, y = pos
+                    if x % 2 == 0:
+                        pos = x + 1, y
+                    else:
+                        pos = x + 1, y + 1
+                    result.append(pos)
+                    d -= 0.5
+                else:
+                    x, y = pos
+                    if x % 2 == 0:
+                        pos = x + 1, y - 1
+                    else:
+                        pos = x + 1, y
+                    result.append(pos)
+                    d += 0.5
+
+                # in case of error in the algorithm, we should avoid infinite loop:
+                if pos[0] > x2:
+                    result = []
+                    break
+
+        if reversed_sym:
+            result.reverse()
+        return result
+
+    @classmethod
+    def triangle(cls, xa, ya, xh, yh, iAngle):
+        """ reimplemented from BaseGrid.triangle """
+        if (xa, ya) == (xh, yh):
+            return [(xa, ya)]
+
+        result = []
+
+        # convert to cubic coodinates (see 'cube_coords' lib)
+        xua, yua, _ = cls.cv_off_cube(xa, ya)
+        xuh, yuh, zuh = cls.cv_off_cube(xh, yh)
+
+        # direction vector
+        dx_dir, dy_dir = xuh - xua, yuh - yua
+
+        # normal vector
+        dx_n, dy_n = -(2 * dy_dir + dx_dir), (2 * dx_dir + dy_dir)
+        dz_n = (-dx_n - dy_n)
+
+        # B and C positions
+        k = 1 / (iAngle * sqrt(3))
+        xub, yub, zub = xuh + (k * dx_n), yuh + (k * dy_n), zuh + (k * dz_n)
+        xuc, yuc, zuc = xuh + (-k * dx_n), yuh + (-k * dy_n), zuh + (-k * dz_n)
+
+        xub, yub, zub = cls.cube_round(xub, yub, zub)
+        xuc, yuc, zuc = cls.cube_round(xuc, yuc, zuc)
+
+        xb, yb = cls.cv_cube_off(xub, yub, zub)
+        xc, yc = cls.cv_cube_off(xuc, yuc, zuc)
+
+        # sides
+        segments = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
+
+        # base (lower slope)
+        x1, y1, x2, y2 = min(segments, key=lambda x: (abs ((x[3] - x[1]) / (x[2] - x[0])) if x[2] != x[0] else 10 ** 10))
+        base = cls.line(x1, y1, x2, y2)
+        y_base = y1
+        segments.remove((x1, y1, x2, y2))
+
+        # 'hat' (the 2 other sides)
+        chapeau = []
+        y_sommet = None
+        for x1, y1, x2, y2 in segments:
+            if y_sommet == None:
+                y_sommet = y2
+            chapeau.extend(cls.line(x1, y1, x2, y2))
+
+        # sense (1 if top is under base, -1 if not)
+        sens = 1 if y_sommet > y_base else -1
+
+        # rove over y values from base to hat
+        for x, y in base:
+            while not (x, y) in chapeau:
+                result.append((x, y))
+                y += sens
+        result.extend(chapeau)
+
+        return result
+
+    @classmethod
+    def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle):
+        """ reimplemented from BaseGrid.triangle3d """
+
+        flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
+
+        result = {}
+
+        k = 1 / (iAngle * sqrt(3))
+
+        # use cubic coordinates
+        xua, yua, zua = cls.cv_off_cube(xa, ya)
+        xuh, yuh, zuh = cls.cv_off_cube(xh, yh)
+
+        length = max(abs(xuh - xua), abs(yuh - yua), abs(zuh - zua))
+
+        vertical_line = SquareGrid.line(0, za, length, zh)
+
+        # build a dict with X key and value is a list of Z values
+        vertical_line_dict = {d:[] for d, z in vertical_line}
+        for d, z in vertical_line:
+            vertical_line_dict[d].append(z)
+
+        # this is approximative: height is update according to the manhattan distance to center
+        for x, y in flat_triangle:
+            xu, yu, zu = cls.cv_off_cube(x, y)
+            distance = int(max(abs(xu - xua), abs(yu - yua), abs(zu - zua)))
+            try:
+                z_list = vertical_line_dict[ distance ]
+            except KeyError:
+                distance = length
+                z_list = vertical_line_dict[ distance ]
+            dh = int(k * distance) + 1 if distance > 0 else 0
+            result[ (x, y) ] = ((min(z_list) - dh) , (max(z_list) + dh))
+        return result
+
+    @classmethod
+    def rotate(cls, center, coordinates, rotations):
+        """ reimplemented from BaseGrid.rotate """
+        if coordinates == [center] or rotations % 6 == 0:
+            return coordinates
+        x0, y0 = center
+        xu0, yu0, zu0 = cls.cv_off_cube(x0, y0)
+        result = []
+
+        for x, y in coordinates:
+            xu, yu, zu = cls.cv_off_cube(x, y)
+            dxu, dyu, dzu = xu - xu0, yu - yu0, zu - zu0
+            for _ in range(rotations):
+                dxu, dyu, dzu = -dzu, -dxu, -dyu
+            xru, yru, zru = dxu + xu0, dyu + yu0, dzu + zu0
+            xr, yr = cls.cv_cube_off(xru, yru, zru)
+            result.append((xr, yr))
+        return result
+
+
+

+ 15 - 16
pypog/painting.py

@@ -13,8 +13,8 @@
 
     ** By Cro-Ki l@b, 2017 **
 '''
+from pypog import gridlib
 
-from pypog import geometry, grid_objects
 
 class NotStartedException(Exception):
     pass
@@ -30,7 +30,7 @@ class BasePencil(object):
     def __init__(self, grid):
 
         # do we really need the grid ref? cell_shape could be enough?
-        if not isinstance(grid, grid_objects.Grid):
+        if not isinstance(grid, gridlib.BaseGrid):
             raise TypeError("'grid' should be a Grid object (given: {})".format(grid))
         self._grid = grid
 
@@ -129,12 +129,12 @@ class LinePencil(BasePencil):
         # use a set because of performance (should we generalize the use of sets for coordinates lists?)
         result = set([])
 
-        line = set(geometry.line(self._grid.cell_shape, x0, y0, x, y))
+        line = set(self._grid.line(x0, y0, x, y))
 
         # apply size with geometry.zone
         if self._grid.size >= 1:
             for x, y in line:
-                result |= set(geometry.zone(self._grid.cell_shape, x, y, self.size - 1))
+                result |= set(self._grid.zone(x, y, self.size - 1))
 
         self._added = result - self._selection
         self._removed = self._selection - result
@@ -148,7 +148,7 @@ class FreePencil(BasePencil):
 
     def _update(self):
         x, y = self.position
-        zone_set = set(geometry.zone(self._grid.cell_shape, x, y, self.size))
+        zone_set = set(self._grid.zone(x, y, self.size))
 
         self._added = zone_set - self._selection
         # there can't be any removed coordinates with this pencil
@@ -201,17 +201,16 @@ class PaintPotPencil(BasePencil):
     def _update(self):
         x0, y0 = self._origin
         current_selection = { (x0, y0) }
-        buffer = set(geometry.neighbours(self._grid._cell_shape, x0, y0))
+        buffer = set(self._grid.neighbours(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(self._grid._cell_shape, x, y)) - current_selection)
+                buffer |= (set(self._grid.neighbours(x, y)) - current_selection)
 
         self._selection = current_selection
 
-
 class RectanglePencil(BasePencil):
     """ RectanglePencil draw a plain rectangle with origin being the
     top left corner, and position the bottom right corner"""
@@ -222,7 +221,7 @@ class RectanglePencil(BasePencil):
         x1, y1 = self._origin
         x2, y2 = self._position
 
-        new_selection = set(geometry.rectangle(x1, y1, x2, y2))
+        new_selection = set(self._grid.rectangle(x1, y1, x2, y2))
 
         self._added = new_selection - self._selection
         self._removed = self._selection - new_selection
@@ -238,7 +237,7 @@ class HollowRectanglePencil(BasePencil):
         x1, y1 = self._origin
         x2, y2 = self._position
 
-        new_selection = set(geometry.hollow_rectangle(x1, y1, x2, y2))
+        new_selection = set(self._grid.hollow_rectangle(x1, y1, x2, y2))
 
         self._added = new_selection - self._selection
         self._removed = self._selection - new_selection
@@ -265,22 +264,22 @@ class BoundaryPencil(BasePencil):
         dx, dy = x - x0, y - y0
 
         if dx == 0:  # vertical boudary
-            selection = {(x, y) for x, y in self._grid.cells.keys() if (x - x0) * dy >= 0}
+            selection = {(x, y) for x, y in self._grid if (x - x0) * dy >= 0}
 
         elif dy == 0:  # horizontal boundary
-            selection = {(x, y) for x, y in self._grid.cells.keys() if (y - y0) * (-dx) >= 0}
+            selection = {(x, y) for x, y in self._grid if (y - y0) * (-dx) >= 0}
 
         elif dx > 0 and dy < 0:  # normal vector to the top left
-            selection = {(x , y) for x, y in self._grid.cells.keys() if (x - x0) + (y - y0) <= 0}
+            selection = {(x , y) for x, y in self._grid if (x - x0) + (y - y0) <= 0}
 
         elif dx > 0 and dy > 0:  # normal vector to the top right
-            selection = {(x , y) for x, y in self._grid.cells.keys() if (x - x0) - (y - y0) >= 0}
+            selection = {(x , y) for x, y in self._grid if (x - x0) - (y - y0) >= 0}
 
         elif dx < 0 and dy < 0:  # normal vector to bottom left
-            selection = {(x , y) for x, y in self._grid.cells.keys() if -(x - x0) + (y - y0) >= 0}
+            selection = {(x , y) for x, y in self._grid if -(x - x0) + (y - y0) >= 0}
 
         elif dx < 0 and dy > 0:  # normal vector to bottom right
-            selection = {(x , y) for x, y in self._grid.cells.keys() if -(x - x0) - (y - y0) <= 0}
+            selection = {(x , y) for x, y in self._grid if -(x - x0) - (y - y0) <= 0}
 
         self._added = selection - self._selection
         self._removed = self._selection - selection

+ 5 - 4
pypog/pathfinding.py

@@ -30,14 +30,15 @@
 
     ** By Cro-Ki l@b, 2017 **
 '''
-from pypog import geometry
+from pypog.gridlib import _HexGrid
+
 
 def distance(coord1, coord2):
     """distance between 1 and 2"""
     x1, y1 = coord1
-    xu1, yu1, zu1 = geometry.cv_off_cube(x1, y1)
+    xu1, yu1, zu1 = _HexGrid.cv_off_cube(x1, y1)
     x2, y2 = coord2
-    xu2, yu2, zu2 = geometry.cv_off_cube(x2, y2)
+    xu2, yu2, zu2 = _HexGrid.cv_off_cube(x2, y2)
     return max(abs(xu1 - xu2), abs(yu1 - yu2), abs(zu1 - zu2))
 
 def square_distance(coord1, coord2):
@@ -74,7 +75,7 @@ class Node():
         """distance (en cases) entre deux coordonnees"""
         x1, y1 = coord1
         x2, y2 = coord2
-        return geometry.distance_off(x1, y1, x2, y2)
+        return _HexGrid.distance_off(x1, y1, x2, y2)
 
 def _default_moving_cost_function(from_coord, to_coord):
     return 1