diff --git a/.gitignore b/.gitignore index 68bc17f..2dc53ca 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ diff --git a/buildings/Building.py b/buildings/Building.py new file mode 100644 index 0000000..32ea625 --- /dev/null +++ b/buildings/Building.py @@ -0,0 +1,43 @@ +import random as rd +from utils.Enums import DIRECTION +from gdpc import Editor, Block, geometry +from buildings.Foundations import Foundations +from buildings.Facade import Facade +from buildings.Entrance import Entrance +from buildings.Roof import Roof + +class Building: + def __init__(self,rdata, position : tuple[int,int], size : tuple[int, int], matrice : list[list[int]], floors : int): + self.position = position + self.length, self.width = size + self.matrice = matrice + self.floors = floors + + # Generate every random components here + tile_size = self.gen_tile_size() + + self.foundations = Foundations(rdata["foundations"], size, matrice, tile_size,) + self.facade = Facade(rdata["facade"], self.foundations.vertices, self.foundations.is_inner_or_outer) + self.entrance = Entrance(rdata, self.foundations.vertices, DIRECTION.EAST, self.foundations.is_inner_or_outer) + self.roof = Roof(rdata["roof"], self.foundations.polygon) + + def build(self, editor : Editor, materials : list[str]): + for y in range(self.floors+1): + with editor.pushTransform((self.position[0], y*(self.foundations.floor_height+1) -1, self.position[1])): + if y == self.floors: + self.roof.build(editor, materials) + break + self.foundations.build(editor, materials) + if y == 0: self.entrance.build(editor, materials) + else : self.facade.build(editor, materials) + + def gen_tile_size(self) -> int: + # Tiles are constant square units different for each buildings + smaller_side = min(self.length, self.width) + + # area is too small, will work but not very well + if smaller_side <= 5 : return smaller_side + if smaller_side <= 15 : return smaller_side // 5 + + return rd.randint(3, smaller_side // len(self.matrice)) + \ No newline at end of file diff --git a/buildings/Entrance.py b/buildings/Entrance.py new file mode 100644 index 0000000..c82d38b --- /dev/null +++ b/buildings/Entrance.py @@ -0,0 +1,114 @@ +import random as rd +from gdpc import Editor, Block, geometry +from utils.Enums import DIRECTION,COLLUMN_STYLE +from buildings.geometry.Vertice import Vertice +from buildings.Facade import Facade + +class Entrance: + def __init__(self, + rdata, + vertices : list[Vertice], + direction : DIRECTION, + collumn_style : COLLUMN_STYLE): + self.vertices = self.correct_vertices(vertices) + self.direction = direction + self.rdata = rdata + self.collumn_style = collumn_style + self.is_centered = self.is_centered() + self.door_vertice, self.facade = self.get_door_and_facade() + self.door_width, self.door_height, self.padding, self.ypadding = self.get_door_dimention() + self.editor, self.materials = None,None + + def build(self, editor : Editor, materials : list[str]): + self.editor = editor + self.materials = materials + self.correct_facade() + with self.editor.pushTransform((0,1,0)): + self.facade.build(self.editor, self.materials) + self.build_door() + + def build_door(self): + # self.padding is the padding from the door to the facade, padding is the padding from the door+self.padding to the end of the vertice + padding = (len(self.door_vertice) - (self.padding*2 + self.door_width // 2)) // 2 + self.door_vertice.fill(self.editor, self.materials[0], + y = self.door_height+self.ypadding, + xpadding = padding, + zpadding = padding) + # padding is now the padding from the door to the end of the vertice + padding += self.padding + self.door_vertice.fill(self.editor, "air", + y = self.door_height, + xpadding = padding, + zpadding = padding) + + def correct_facade(self): + self.facade.has_balcony = False + + def correct_vertices(self, vertices : list[Vertice]) -> list[Vertice]: + for v in vertices: + v.point2.set_position(y=v.point2.y-1) + return vertices + + def is_centered(self) -> bool: + return rd.random() <= self.rdata["entrance"]["centered"] + + def get_door_and_facade(self) -> tuple[Vertice, Facade]: + oriented_vertices = self.get_oriented_vertices() + door_vertice = None + + if self.is_centered: + oriented_vertices.sort(key = lambda v: v.point1.x if self.direction.value % 2 == 0 else v.point1.z) # if direction is north or south, sort by x, else sort by z + mid = len(oriented_vertices) // 2 + ver1, ver2 = oriented_vertices[mid], oriented_vertices[-mid-1] + + if ver1.point1.x != ver2.point1.x and ver1.point1.z != ver2.point1.z: + door_vertice = rd.choice([ver1, ver2]) + elif ver1.point1.position == ver2.point1.position: + door_vertice = ver1 + else : + door_vertice = Vertice(ver2.point1.copy(), ver1.point2.copy()) + + else: + door_vertice = rd.choice(oriented_vertices) + + facade = Facade(self.rdata["facade"], self.vertices, self.collumn_style) + return(door_vertice, facade) + + def get_oriented_vertices(self) -> list[Vertice]: + # Get all the vertice that can contain the door + + # if direction is north or south, compare by x, else compare by z + compare = lambda v: (v.point1.z,v.point1.x) if self.direction.value % 2 == 0 else (v.point1.x,v.point1.z) + # if direction is north or west, the most off_centered is the maximum, else it is the minimum + off_centered = lambda p1,p2: max(p1,p2) if self.direction == DIRECTION.NORTH or self.direction == DIRECTION.WEST else min(p1,p2) + + oriented_vertices = [] + for v in self.vertices: + if v.facing != self.direction: continue + sortby,position = compare(v) + alreadyset = False + for ov in oriented_vertices: + ov_sorted, ov_position = compare(ov) + if position == ov_position: + if off_centered(sortby,ov_sorted) == sortby: oriented_vertices.remove(ov) + else: alreadyset = True + if not alreadyset: oriented_vertices.append(v) + + return oriented_vertices + + def get_door_dimention(self) -> tuple[int,int,int,int]: # return width, height, padding, ypadding + max_width = len(self.door_vertice) - 2 + max_height = self.door_vertice.get_height() - 1 + + door_width = rd.randint(self.rdata["entrance"]["door"]["size"]["min_width"], self.rdata["entrance"]["door"]["size"]["max_width"]) + door_height = rd.randint(self.rdata["entrance"]["door"]["size"]["min_height"], self.rdata["entrance"]["door"]["size"]["max_height"]) + xpadding = rd.randint(1, self.rdata["entrance"]["door"]["padding"]["max"]) + ypadding = rd.randint(1, self.rdata["entrance"]["door"]["padding"]["max_top"]) + + if door_width > max_width: door_width = max_width + if door_height > max_height: door_height = max_height + if xpadding*2 + door_width > max_width: xpadding += (max_width - (xpadding*2 + door_width)-1)//2 + if ypadding + door_height > max_height: ypadding += max_height - (ypadding + door_height) + + return door_width,door_height,xpadding,ypadding + \ No newline at end of file diff --git a/buildings/Facade.py b/buildings/Facade.py new file mode 100644 index 0000000..f1da338 --- /dev/null +++ b/buildings/Facade.py @@ -0,0 +1,89 @@ +import random as rd +from utils.functions import * +from utils.Enums import COLLUMN_STYLE,DIRECTION,INTER_FLOOR_BORDER +from gdpc import Editor, Block, geometry, Transform +from buildings.geometry.Vertice import Vertice +from buildings.geometry.Point import Point +from buildings.elements.Window import Window +from buildings.elements.Balcony import Balcony + +class Facade: + def __init__(self, + rdata, + vertices : list[Vertice], + collumn_style : COLLUMN_STYLE): + self.rdata = rdata + self.vertices = vertices + self.collumn_style = collumn_style + self.height, self.length = self.get_dimentions() + self.padding = 0 + self.window = self.get_window() + self.has_balcony = self.has_balcony() + self.balcony = self.get_balcony() + self.has_inter_floor, self.inter_floor_border_style = self.has_inter_floor() + self.editor, self.materials = None,None + + def build(self, editor : Editor, materials : list[str]): + self.editor = editor + self.materials = materials + points = sum([[vertice.point1, vertice.point2] for vertice in self.vertices], []) + + for vertice in self.vertices: + flip=(vertice.facing == DIRECTION.WEST or vertice.facing == DIRECTION.SOUTH, False, False) + vertice.fill(editor, materials[0], self.height, xpadding = self.padding, zpadding = self.padding) + with editor.pushTransform(Transform(vertice.point1.position,rotation = vertice.facing.value, flip = flip)): + self.window.build(editor, materials) + if self.has_inter_floor: self.build_inter_floor() + if self.has_balcony: self.balcony.build(editor, materials) + self.correct_corners(points,vertice) + + def correct_corners(self,points : list[Point], v : Vertice): + if self.padding == 0: + if self.window.border_radius != 0 and self.window.width == self.length: + if points.count(v.point1) >= 2: + self.editor.placeBlock((0,self.window.ypadding,0), Block(self.materials[8])) + self.editor.placeBlock((0,self.window.ypadding+self.window.height,0), Block(self.materials[8], {"type": "top"})) + if points.count(v.point2) >= 2: + self.editor.placeBlock((self.length-1,self.window.ypadding,0), Block(self.materials[8])) + self.editor.placeBlock((self.length-1,self.window.ypadding+self.window.height,0), Block(self.materials[8], {"type": "top"})) + + if self.has_inter_floor: + material = Block("air") + if self.inter_floor_border_style == INTER_FLOOR_BORDER.SLAB: + material = Block(self.materials[8], {"type": "top"}) + elif self.inter_floor_border_style == INTER_FLOOR_BORDER.STAIRS: + material = Block(self.materials[4], {"facing": "south", "half": "top"}) + + if points.count(v.point1) >= 2: + self.editor.placeBlock((-1,self.height,-1), material) + if points.count(v.point2) >= 2: + self.editor.placeBlock((self.length,self.height,-1), material) + + + def get_window(self) -> Window: + if self.collumn_style.value >= 2: # collumn_style >= 2 = outer collumns + self.padding = 1 + + max_width = self.length-2*self.padding + max_height = min(self.height, self.rdata["windows"]["size"]["max_height"]) + + return Window(self.rdata["windows"] ,max_width, max_height, self.length, self.height) + + def get_balcony(self) -> Balcony|None: + if not self.has_balcony: return None + max_width = self.length-2*self.padding + return Balcony(self.rdata["balcony"], max_width, self.window, self.collumn_style) + + def build_inter_floor(self): + geometry.placeCuboid(self.editor,(self.padding,self.height,0),(self.length-1-self.padding,self.height,0),Block(self.materials[0])) + geometry.placeCuboid(self.editor,(self.padding,self.height,-1),(self.length-1-self.padding,self.height,-1),Block(self.materials[4], {"facing": "south", "half": "top"})) + + def has_balcony(self) -> bool: + return self.rdata["balcony"]["proba"] >= rd.random() + + def has_inter_floor(self) -> bool: + return (self.rdata["inter_floor"]["proba"] >= rd.random(), select_random(self.rdata["inter_floor"]["border_style"], INTER_FLOOR_BORDER)) + + def get_dimentions(self) -> tuple[int,int]: + return ( self.vertices[0].get_height(), len(self.vertices[0])) + \ No newline at end of file diff --git a/buildings/Foundations.py b/buildings/Foundations.py new file mode 100644 index 0000000..e6003a8 --- /dev/null +++ b/buildings/Foundations.py @@ -0,0 +1,150 @@ +import random as rd +import numpy as np +import math + +from utils.Enums import COLLUMN_STYLE +from utils.functions import * +from buildings.geometry.Tile import Tile +from buildings.geometry.Polygon import Polygon +from buildings.geometry.Point import Point +from buildings.elements.Collumn import Collumn + +class Foundations: + # TODO : gérer les collones sur les tiles trop petites et les colones 1tile/2 + + def __init__(self, + rdata, + size : tuple[int, int], + matrice : list[list[int]], + tile_size : int): + # Foundations are the base of the building, they are made of tiles and based on a matrice + + # Random components + self.tile_size = tile_size + self.is_inner_or_outer = select_random(rdata["collumn_style"], COLLUMN_STYLE) + self.floor_height = rd.randint(rdata["floor"]["min_height"], rdata["floor"]["max_height"])-1 + + self.size = size + self.length, self.width = size + self.matrice = matrice + self.tiles = [] + self.vertices = [] + self.length_in_tiles = self.length // self.tile_size + self.width_in_tiles = self.width // self.tile_size + self.x_distribution = [] + self.z_distribution = [] + self.polygon = self.get_polygon() + self.collumns = self.get_columns() + + def build(self, editor, materials : list[str]): + self.polygon.fill(editor, materials[5],0) + self.polygon.fill(editor, materials[6], self.floor_height) + self.build_collumns(editor, materials) + + def build_collumns(self, editor, materials : list[str]): + for collumn in self.collumns: + if collumn.is_outer and self.is_inner_or_outer == COLLUMN_STYLE.INNER: continue + if not collumn.is_outer and self.is_inner_or_outer == COLLUMN_STYLE.OUTER: continue + collumn.fill(editor, materials[7], self.floor_height+1) + + def add_tile(self, tile : Tile): + self.tiles.append(tile) + + def get_polygon(self) -> Polygon: + ## The polygon is a shape of tiles representing the foundation shape + polygon = Polygon(self.size) + + # we save the distribution, usefull for the next steps + self.x_distribution = self.get_distribution(len(self.matrice), self.length_in_tiles) + self.z_distribution = self.get_distribution(len(self.matrice[0]), self.width_in_tiles) + + # this bullshit is to create tiles from the matrice and the distribution + x_padding = 0 + for x,xsize in enumerate(self.x_distribution): + z_padding = 0 + for z,zsize in enumerate(self.z_distribution): + if self.matrice[x][z] == 1: + for xi in range(xsize): + for zi in range(zsize): + tile = Tile(self.tile_size, (x_padding + xi*self.tile_size, z_padding + zi*self.tile_size)) + self.add_tile(tile) + z_padding += zsize * self.tile_size + x_padding += xsize * self.tile_size + + polygon.set_vertices_and_neighbors(self.tiles, self.vertices, self.floor_height) + polygon.compress(self.tiles, self.vertices) + return polygon + + + + def get_distribution(self,length,avaliable_tiles): + # foundations are based on a matrice, + # this function gives the number of tiles for each row/collumn of the matrice, giving a more random shape + # The real shit start here + if length == avaliable_tiles: + return [1 for i in range(avaliable_tiles)] + + if length == 1: + return [avaliable_tiles] + + if length == 2: + l = rd.randint(1,avaliable_tiles-1) + return [l,avaliable_tiles-l] + + if length >= 3: + sizes = [] + intersections_count = math.ceil(length/2)-1 + tiles_per_side = avaliable_tiles//2 + correction = 0 + + intersect_values = np.random.choice(np.arange(1,tiles_per_side), size=intersections_count, replace=False) + + #we generate only half of the distribution + last_pos = 0 + intersect_values = np.append(intersect_values,tiles_per_side) + for intersect in intersect_values: + sizes.append(intersect - last_pos) + last_pos = intersect + + # we duplicate the side for the symetry + symetry = sizes.copy() + symetry.reverse() + if avaliable_tiles%2 == 1: correction = 1 # if there is a tile left, add it randomly + if length%2 == 1 : sizes[-1], symetry = sizes[-1]*2 + correction, symetry[1:] + sizes += symetry + + return sizes + + def get_columns(self) -> list[Collumn]: + if self.is_inner_or_outer == COLLUMN_STYLE.NONE: return [] + collumns = [] + + for tile in self.tiles: + north_west_collumn = Collumn(Point(x = tile.north_west.x-1, z = tile.north_west.z-1), tile.north_west) + north_east_collumn = Collumn(Point(x = tile.north_east.x, z = tile.north_east.z-1), Point(x = tile.north_east.x+1, z = tile.north_east.z)) + south_west_collumn = Collumn(Point(x = tile.south_west.x-1, z = tile.south_west.z), Point(x = tile.south_west.x, z = tile.south_west.z+1)) + south_east_collumn = Collumn(tile.south_east, Point(x = tile.south_east.x+1, z = tile.south_east.z+1)) + + if tile.north_vertice != None or tile.west_vertice != None: north_west_collumn.set_is_outer(True) + + if tile.north_vertice != None or tile.east_vertice != None: north_east_collumn.set_is_outer(True) + + if tile.south_vertice != None or tile.west_vertice != None: south_west_collumn.set_is_outer(True) + + if tile.south_vertice != None or tile.east_vertice != None: south_east_collumn.set_is_outer(True) + + collumns.extend([north_west_collumn, north_east_collumn, south_west_collumn, south_east_collumn]) + + return self._suppr_doubblons_collumns(collumns) + + def _suppr_doubblons_collumns(self, collumns : list[Collumn]): + for index,collumn in enumerate(collumns): + if index == len(collumns)-1: break + for compare in collumns[index+1:]: + if collumn.point1.position == compare.point1.position : + if compare.is_outer : collumn.set_is_outer(True) + collumns.remove(compare) + + return collumns + + \ No newline at end of file diff --git a/buildings/Roof.py b/buildings/Roof.py new file mode 100644 index 0000000..7036a87 --- /dev/null +++ b/buildings/Roof.py @@ -0,0 +1,15 @@ +import random as rd +from buildings.geometry.Polygon import Polygon + +class Roof: + def __init__(self,rdata, polygon : Polygon): + self.rdata = rdata + self.polygon = polygon + self.has_rembard = self.has_rembard() + + def build(self, editor, materials : list[str]): + self.polygon.fill(editor, materials[0]) + if self.has_rembard: self.polygon.fill_vertice(editor, materials[9],1) + + def has_rembard(self): + return rd.random() <= self.rdata["rembard"] diff --git a/buildings/TODO b/buildings/TODO new file mode 100644 index 0000000..43f622d --- /dev/null +++ b/buildings/TODO @@ -0,0 +1,20 @@ +Encadrement fenêtre +toit de balcon avec/sans pilliers +collumn style +rembard object +détails facade +rdc +toit (clim, chateau deau, pubs) +tiles 3d +textures object +opti textures +opti géométrique +opti gdpc +pilliers quand trop de fenêtres + pas de pilliers si tile trop petite +limitateur taille +facade lisses/ immeubles collés +matrices pré-distribués +angles 270 +bug entrée au milieu du O +bug entrée dans le pillier +center le building dans son area (ou pas) \ No newline at end of file diff --git a/buildings/elements/Balcony.py b/buildings/elements/Balcony.py new file mode 100644 index 0000000..42defca --- /dev/null +++ b/buildings/elements/Balcony.py @@ -0,0 +1,135 @@ +import random as rd +from gdpc import Editor, Block, geometry +from utils.functions import * +from utils.Enums import BALCONY_BORDER_RADIUS,COLLUMN_STYLE +from buildings.geometry.Point import Point +from buildings.geometry.Vertice import Vertice +from buildings.elements.Window import Window + +class Balcony: + def __init__(self, + rdata, + max_width : int, + windows : Window, + collumn_style : COLLUMN_STYLE): + self.rdata = rdata + self.windows = windows + self.max_width = max_width + self.collumn_style = collumn_style + self.length = self.get_len() + self.has_multiple = self.has_multiple() + self.has_details = self.has_details() + self.border_radius = self.has_border_radius() + self.follow_window = self.follow_window() + self.structure = self.get_structures() + self.editor, self.materials = None,None + + def build(self, editor : Editor, materials : list[str]): + self.editor = editor + self.materials = materials + for s in self.structure: + s.fill(editor, materials[0]) + self.build_rembard(s) + self.build_details(s) + self.build_border_radius(s) + + def build_rembard(self, s : Vertice): + geometry.placeCuboid(self.editor,(s.point1.x,1,-1),(s.point1.x,1,-self.length),Block(self.materials[3])) + geometry.placeCuboid(self.editor,(s.point2.x,1,-1),(s.point2.x,1,-self.length),Block(self.materials[3])) + geometry.placeCuboid(self.editor,(s.point1.x,1,-self.length),(s.point2.x,1,-self.length),Block(self.materials[3])) + + def build_details(self, s : Vertice): + if not self.has_details: return + geometry.placeCuboid(self.editor,(s.point1.x,0,-1),(s.point1.x,0,-self.length),Block(self.materials[4], {"facing": "east", "half": "top"})) + geometry.placeCuboid(self.editor,(s.point2.x,0,-1),(s.point2.x,0,-self.length),Block(self.materials[4], {"facing": "west", "half": "top"})) + geometry.placeCuboid(self.editor,(s.point1.x,0,-self.length),(s.point2.x,0,-self.length),Block(self.materials[4], {"facing": "south", "half": "top"})) + + def build_border_radius(self, s : Vertice): + if self.border_radius == BALCONY_BORDER_RADIUS.NONE: return + + geometry.placeCuboid(self.editor,(s.point1.x,0,-self.length),(s.point1.x,1,-self.length),Block("air")) + geometry.placeCuboid(self.editor,(s.point2.x,0,-self.length),(s.point2.x,1,-self.length),Block("air")) + self.editor.placeBlock((s.point1.x+1,1,-self.length+1), Block(self.materials[3])) + self.editor.placeBlock((s.point2.x-1,1,-self.length+1), Block(self.materials[3])) + + if self.has_details: + self.editor.placeBlock((s.point1.x,0,-self.length+1), Block(self.materials[4], {"facing": "south", "half": "top"})) + self.editor.placeBlock((s.point1.x+1,0,-self.length), Block(self.materials[4], {"facing": "east", "half": "top"})) + self.editor.placeBlock((s.point2.x,0,-self.length+1), Block(self.materials[4], {"facing": "south", "half": "top"})) + self.editor.placeBlock((s.point2.x-1,0,-self.length), Block(self.materials[4], {"facing": "west", "half": "top"})) + + if self.border_radius == BALCONY_BORDER_RADIUS.FULL: + self.editor.placeBlock((s.point1.x+1,0,-self.length+1), Block(self.materials[4], {"facing": "east", "half": "top"})) + self.editor.placeBlock((s.point2.x-1,0,-self.length+1), Block(self.materials[4], {"facing": "west", "half": "top"})) + + def get_structures(self) -> list[Vertice]: + # structures are the base shape of the balcony + attach_points = self.get_attach_points() + len_attach_points = len(attach_points)-1 + min_wid = self.rdata["size"]["min_width"] + min_gap = self.rdata["multiple"]["min_gap"] + growth_chance = self.rdata["growth"] + midpoint = len_attach_points//2 + x1,x2 = midpoint, len_attach_points - midpoint + + structures = [] + centered = True + while x1 > 0: + x1 -= 1 + x2 += 1 if centered else 0 + leng = attach_points[x2] - attach_points[x1] - 1 + + if x1 == 0: + if leng >= min_wid: self.append_structure(structures, x1, x2, attach_points, len_attach_points, centered) + break + if leng < min_wid: continue + + if growth_chance < rd.random(): + self.append_structure(structures, x1, x2, attach_points, len_attach_points, centered) + + if not self.has_multiple: break + else: + centered = False + if attach_points[x1]-min_wid < min_gap: break + gap = rd.randint(min_gap, attach_points[x1]-min_wid) + x2 = x1-gap + x1 = x2-min_wid+1 + + return structures + + def get_attach_points(self) -> list[int]: + # points where the structures can start/finish + padding = 0 if self.collumn_style.value < 2 else 1 # collumn_style < 2 = no outer collumns + points = [i + padding for i in range(self.max_width)] + if self.follow_window: + pad = self.windows.padding + for w in self.windows.windows: + for i in range(pad+w.x1, pad+w.x2+1): + points.remove(i) + + return points + + def create_structure(self, x1 : int, x2 : int) -> Vertice: + return Vertice(Point(x = x1), Point(x = x2,z = -self.length)) + + def append_structure(self, structures : list[Vertice], x1 : int, x2 : int, attach_points : list[int], len_attach_points : int, centered : bool): + structures.append(self.create_structure(attach_points[x1], attach_points[x2])) + if not centered: + structures.append(self.create_structure(attach_points[len_attach_points-x1], attach_points[len_attach_points-x2])) + + def follow_window(self) -> bool: + return not self.windows.ypadding > 3 + + def has_multiple(self) -> bool: + if self.max_width < self.rdata["multiple"]["min_width"]: return False + return self.rdata["multiple"]["proba"] >= rd.random() + + def has_details(self) -> bool: + return self.rdata["details"] >= rd.random() + + def has_border_radius(self) -> bool: + if self.length < 2: return BALCONY_BORDER_RADIUS.NONE + return select_random(self.rdata["border_radius"], BALCONY_BORDER_RADIUS) + + def get_len(self) -> int: + return rd.randint(self.rdata["size"]["min_len"], self.rdata["size"]["max_len"]) \ No newline at end of file diff --git a/buildings/elements/Collumn.py b/buildings/elements/Collumn.py new file mode 100644 index 0000000..40e644d --- /dev/null +++ b/buildings/elements/Collumn.py @@ -0,0 +1,14 @@ +from buildings.geometry.Rectangle import Rectangle +from buildings.geometry.Point import Point + +class Collumn(Rectangle): + def __init__(self, point1 : Point, point2 : Point, is_outer : bool = False) : + Rectangle.__init__(self, point1, point2) + self.is_outer = is_outer + + def set_is_outer(self, is_outer : bool): + self.is_outer = is_outer + + def __repr__(self): + return super().__repr__() + f"\nIs outer : {self.is_outer}\n\n" + \ No newline at end of file diff --git a/buildings/elements/FacadeDetails.py b/buildings/elements/FacadeDetails.py new file mode 100644 index 0000000..069963e --- /dev/null +++ b/buildings/elements/FacadeDetails.py @@ -0,0 +1,19 @@ +from buildings.geometry.Vertice import Vertice + +class FacadeDetails: + def __init__(self,rdata , zones : list[Vertice]): + self.zones = zones + self.sizes = self.get_sizes() + + def get_sizes(self) -> list[tuple[int,int,int]]: + # foreach different zone sizes in self.zones, we will gen different details + sizes = [] + center_for_symetry = len(self.zones) // 2 + for zone in self.zones: + size = zone.point2.position - zone.point1.position + if size not in sizes : + sizes.append(size) + + return sizes + + \ No newline at end of file diff --git a/buildings/elements/FacadedetailsElt/Buttons.py b/buildings/elements/FacadedetailsElt/Buttons.py new file mode 100644 index 0000000..3436a95 --- /dev/null +++ b/buildings/elements/FacadedetailsElt/Buttons.py @@ -0,0 +1,3 @@ +class Buttons: + def __init__(self): + pass \ No newline at end of file diff --git a/buildings/elements/FacadedetailsElt/InterFloor.py b/buildings/elements/FacadedetailsElt/InterFloor.py new file mode 100644 index 0000000..e69de29 diff --git a/buildings/elements/FacadedetailsElt/Moldings.py b/buildings/elements/FacadedetailsElt/Moldings.py new file mode 100644 index 0000000..e69de29 diff --git a/buildings/elements/FacadedetailsElt/Pillar.py b/buildings/elements/FacadedetailsElt/Pillar.py new file mode 100644 index 0000000..e69de29 diff --git a/buildings/elements/Window.py b/buildings/elements/Window.py new file mode 100644 index 0000000..5a2739e --- /dev/null +++ b/buildings/elements/Window.py @@ -0,0 +1,150 @@ +import random as rd +import math +from gdpc import Editor, Block, geometry, Transform +from utils.Enums import WINDOW_BORDER_RADIUS +from utils.functions import * +from buildings.geometry.Point import Point +from buildings.geometry.Vertice import Vertice +from buildings.elements.WindowElt.Glass import Glass + +class Window: + def __init__(self, + rdata, + max_width : int, + max_height : int, + facade_len : int, + facade_height : int): + self.rdata = rdata + self.width, self.height = self.get_size(max_width, max_height) + self.is_grounded = self.is_grounded() + self.is_alternate = self.is_alternate() + self.border_radius = self.border_radius() + self.has_multiple = self.has_multiple_windows() + self.has_vertical_crossbar, self.has_horizontal_crossbar = self.has_crossbars() + self.padding, self.ypadding = self.get_padding(facade_len, facade_height) + self.windows = self.get_windows() + self.editor, self.materials = None,None + + def build(self, editor : Editor, materials : list[str]): + self.editor = editor + self.materials = materials + with editor.pushTransform(Transform((self.padding,self.ypadding,0))): + for g in self.windows: + leng = len(g) + g.build(editor, materials[1], materials[2]) + self.build_crossbars(g.x1, g.x2, leng) + if leng > 1: self.build_border_radius(g.x1, g.x2) + + def build_crossbars(self, x1 : int, x2 : int, len : int): + if self.has_vertical_crossbar and self.height+1 >= self.rdata["crossbars"]["min_height_for_vertical_crossbar"]: + y = self.height//2 + geometry.placeCuboid(self.editor,(x1,y,0),(x2,y,0),Block(self.materials[3])) + if self.has_horizontal_crossbar and len >= self.rdata["crossbars"]["min_width_for_horizontal_crossbar"]: + x = len//2 + geometry.placeCuboid(self.editor,(x1+x,0,0),(x2-x,self.height,0),Block(self.materials[3], {"up" : "true"})) + + def build_border_radius(self, x1 : int, x2 : int): + if self.border_radius != WINDOW_BORDER_RADIUS.NONE: + self.editor.placeBlock((x1,self.height,0),Block(self.materials[4], {"facing": "west", "half": "top"})) + self.editor.placeBlock((x2,self.height,0),Block(self.materials[4], {"facing": "east", "half": "top"})) + if self.border_radius == WINDOW_BORDER_RADIUS.TOP_AND_BOTTOM: + self.editor.placeBlock((x1,0,0),Block(self.materials[4], {"facing": "west"})) + self.editor.placeBlock((x2,0,0),Block(self.materials[4], {"facing": "east"})) + + def get_windows(self) -> list[Glass]: + windows = [] + if not self.has_multiple: windows = [Glass(0,self.width-1,[self.create_window(0, self.width)])] + else: windows = self.get_multiple_windows() + if self.is_alternate: self.alternate(windows) + + return windows + + def get_multiple_windows(self) -> list[Glass]: + windows = [] + slices = rd.randint(3, self.width//self.rdata["size"]["min_width"]) + mid = math.ceil(slices/2) + windows_count = mid + inter_count = slices - windows_count + window_size = rd.randint(self.rdata["size"]["min_width"], (self.width-inter_count) // windows_count) + inter_size = (self.width - window_size*windows_count) // inter_count + + is_even= slices % 2 == 0 + is_window, gap = True, 0 + remainder = self.width - (window_size*windows_count + inter_size*inter_count) + + if windows_count % 2 == 1 and inter_count % 2 == 1: + inter_count -= 1 + remainder += inter_size + is_even = not is_even + + for i in range(1,slices+1): + wsize,isize = window_size, inter_size + if is_even and i == mid: wsize, isize = wsize*2, isize*2 + if i == mid: wsize, isize = wsize + remainder, isize + remainder + + if is_window: + windows.append(Glass(gap, gap+wsize-1,[self.create_window(gap, wsize)])) + gap += wsize + else : + gap += isize + + is_window = not is_window + + return windows + + def alternate(self, windows : list[Glass]): + for g in windows: + g.reset_groups() + leng = len(g) + mid = g.x1 + leng//2 + + is_block, is_even = False, leng % 2 == 0 + for x in range(g.x1,g.x2+1): + if is_even and x == mid: is_block = not is_block # to keep symetry + if is_block: g.group2.append(self.create_window(x)) + else : g.group1.append(self.create_window(x)) + is_block = not is_block + + def create_window(self, x1 : int, length : int = None) -> Vertice: + x2 = x1 if length is None else x1 + length -1 + return Vertice(Point(x = x1), Point(x2,self.height)) + + def has_multiple_windows(self): + if self.width > self.rdata["size"]["max_width"]: return True + elif self.width >= self.rdata["multiple"]["min_width"]: + return self.rdata["multiple"]["proba"] >= rd.random() + else : return False + + def is_alternate(self): + # if the window alternate between glass_blocks and glass_panes + return self.rdata["alternate"] >= rd.random() + + + def get_size(self, max_width : int ,max_height : int) -> tuple[int,int]: + return ( + rd.randint(self.rdata["size"]["min_width"],max_width), + rd.randint(self.rdata["size"]["min_height"],max_height) + ) + + def get_padding(self, facade_len : int, facade_height : int) -> tuple[int,int]: + padding,ypadding = 0,0 + if not self.is_grounded: ypadding = (facade_height - self.height)//2 + + # correction to avoid asymetry + padding = (facade_len - self.width)//2 + self.width = facade_len - padding*2 + + return (padding, ypadding) + + def is_grounded(self): + # if the window is grounded or if there is a padding between the window and the ground + return self.rdata["grounded"] >= rd.random() + + def has_crossbars(self): + # if the window has crossbars + data = self.rdata["crossbars"] + + return (data["vertical_crossbar"] >= rd.random(), data["horizontal_crossbar"] >= rd.random()) + + def border_radius(self): + return select_random(self.rdata["border_radius"], WINDOW_BORDER_RADIUS) \ No newline at end of file diff --git a/buildings/elements/WindowElt/Glass.py b/buildings/elements/WindowElt/Glass.py new file mode 100644 index 0000000..e9f40ee --- /dev/null +++ b/buildings/elements/WindowElt/Glass.py @@ -0,0 +1,22 @@ +from gdpc import Editor +from buildings.geometry.Vertice import Vertice + +class Glass: + def __init__(self, x1 : int, x2 : int, group1 : list[Vertice], group2 : list[Vertice] = None): + self.x1, self.x2 = x1, x2 + self.group1, self.group2 = group1, group2 + + + def build(self, editor : Editor, material1 : str, material2 : str): + for elt in self.group1: + elt.fill(editor, material1) + if self.group2 is None: return + for elt in self.group2: + elt.fill(editor, material2) + + def reset_groups(self): + self.group1, self.group2 = [], [] + + def __len__(self): + return self.x2 - self.x1 + 1 + \ No newline at end of file diff --git a/buildings/geometry/Point.py b/buildings/geometry/Point.py new file mode 100644 index 0000000..7300d8b --- /dev/null +++ b/buildings/geometry/Point.py @@ -0,0 +1,20 @@ +class Point: + def __init__(self, x : int = 0, y : int = 0, z : int = 0, p : tuple[int,int,int] = None): + if p != None: x,y,z = p + self.x = x + self.y = y + self.z = z + self.position = (x,y,z) + + def set_position(self, x : int = None, y : int = None, z : int = None, p : tuple[int,int,int] = None): + if p != None: x,y,z = p + self.x = x if x != None else self.x + self.y = y if y != None else self.y + self.z = z if z != None else self.z + self.position = (self.x,self.y,self.z) + + def __repr__(self): + return f"Point({self.position})" + + def copy(self) -> 'Point': + return Point(self.x, self.y, self.z) \ No newline at end of file diff --git a/buildings/geometry/Polygon.py b/buildings/geometry/Polygon.py new file mode 100644 index 0000000..92dfbc3 --- /dev/null +++ b/buildings/geometry/Polygon.py @@ -0,0 +1,107 @@ +from utils.Enums import DIRECTION +from gdpc import Editor, Block, geometry, Transform +from buildings.geometry.Tile import Tile +from buildings.geometry.Point import Point +from buildings.geometry.Rectangle import Rectangle +from buildings.geometry.Vertice import Vertice + +class Polygon: + def __init__(self, size: tuple[int,int]): + self.size = size + self.shape = [] + self.vertices = [] + + def fill(self, editor : Editor, material : str, y : int = 0, y2 : int = None): + if y2 == None: y2 = 0 + for rect in self.shape: + with editor.pushTransform(Transform((0,y,0))): + rect.fill(editor, material, y2) + + def fill_vertice(self, editor : Editor, material : str, y : int, y2 : int = None): + if y2 == None: y2 = 0 + for vertice in self.vertices: + with editor.pushTransform(Transform((0,y,0))): + vertice.fill(editor, material, y2) + + def compress(self, tiles : list[Tile], vertices : list[Vertice]): + remaining_tiles = tiles.copy() + while len(remaining_tiles) > 0: + start = remaining_tiles[0] + neightbor = start.get_neighbor(DIRECTION.WEST) + row = [] + + # Find western border + while neightbor: + start = neightbor + neightbor = start.get_neighbor(DIRECTION.WEST) + + # Find eastern border + while True: + row.append(start) + remaining_tiles.remove(start) + neightbor = start.get_neighbor(DIRECTION.EAST) + if not neightbor: break + start = neightbor + + # Find northern border + north_row = self._find_row_border(row.copy(), DIRECTION.NORTH, remaining_tiles) + # Find southern border + south_row = self._find_row_border(row.copy(), DIRECTION.SOUTH, remaining_tiles) + + area = Rectangle(north_row[0].north_west, south_row[-1].south_east) + self.shape.append(area) + + remaining_vertices = vertices.copy() + current = remaining_vertices.pop() + while len(remaining_vertices) > 0: + neighbors = current.get_neighbors() + has_next1 = self._has_next(neighbors[0], current.facing, remaining_vertices) + has_next2 = self._has_next(neighbors[1], current.facing, remaining_vertices) + + if has_next1: + current = Vertice(has_next1.point1.copy(), current.point2.copy(), current.facing) + elif has_next2: + current = Vertice(current.point1.copy(), has_next2.point2.copy(), current.facing) + else: + self.vertices.append(current) + current = remaining_vertices.pop() + + if len(remaining_vertices) == 0: self.vertices.append(current) + + def set_vertices_and_neighbors(self, tiles : list[Tile], vertices : list[Vertice], height : int): + for tile in tiles: + targets = tile.get_neighbors_coords() + for vertice_num,target in enumerate(targets): + has_neighbor = self._has_neighbor(target, tiles) + if not has_neighbor: + vertice = tile.get_vertice(vertice_num) + vertices.append(vertice) + tile.set_vertice(DIRECTION(vertice_num), vertice, height) + else : + tile.set_neighbor(vertice_num, has_neighbor) + + def _find_row_border(self, line : list[Tile], direction : str, remaining_tiles : list[Tile]) -> list[Tile]: + while True: + new_line = [] + for tile in line: + neightbor = tile.get_neighbor(direction) + if neightbor not in remaining_tiles: return line + new_line.append(neightbor) + for tile in new_line: remaining_tiles.remove(tile) + line = new_line + + def _has_neighbor(self, target : Point, tiles : list[Tile]) -> bool|Tile: + for tile in tiles: + if tile.pos.position == target.position: + return tile + return False + + def _has_next(self, target : Point, facing : str, remaining_vertices : list[Vertice]) -> bool|Vertice: + for vertice in remaining_vertices: + if vertice.facing == facing: + if vertice.point1.position == target.position or vertice.point2.position == target.position: + remaining_vertices.remove(vertice) + return vertice + return False + + \ No newline at end of file diff --git a/buildings/geometry/Rectangle.py b/buildings/geometry/Rectangle.py new file mode 100644 index 0000000..47a7a4f --- /dev/null +++ b/buildings/geometry/Rectangle.py @@ -0,0 +1,23 @@ +from gdpc import Editor, Block, geometry +from buildings.geometry.Point import Point + +class Rectangle: + def __init__(self, point1 : Point, point2 : Point): + self.point1 = point1 + self.point2 = point2 + + def get_position(self): + return (self.point1.position, self.point2.position) + + def get_height(self): + return self.point2.y - self.point1.y + + def fill(self,editor : Editor, material : str, y : int = None, xpadding : int = 0, zpadding : int = 0): + if self.point2.x - self.point1.x < 2*xpadding: xpadding = 0 + if self.point2.z - self.point1.z < 2*zpadding: zpadding = 0 + if y is None: y = self.point2.y + + geometry.placeCuboid(editor, (self.point1.x+xpadding, 0, self.point1.z+zpadding), (self.point2.x-xpadding, y, self.point2.z-zpadding), Block(material)) + + def __repr__(self): + return f"{type(self).__name__}\n1 : {str(self.point1)},\n2 : {str(self.point2)}" \ No newline at end of file diff --git a/buildings/geometry/Tile.py b/buildings/geometry/Tile.py new file mode 100644 index 0000000..e1b6174 --- /dev/null +++ b/buildings/geometry/Tile.py @@ -0,0 +1,94 @@ +from gdpc import Editor, Block, geometry +from utils.Enums import DIRECTION +from buildings.geometry.Point import Point +from buildings.geometry.Vertice import Vertice + +class Tile: + def __init__(self, size : int, position : tuple[int, int]): + self.size = size + x,z = position + leng = self.size-1 + self.pos = Point(x = x, z = z) + + self.has_vertice = False + + self.north_west = self.pos + self.north_east = Point(x = self.pos.x + leng, z =self.pos.z) + self.south_west = Point(x = self.pos.x, z = self.pos.z + leng) + self.south_east = Point(x = self.pos.x + leng, z = self.pos.z + leng) + + self.west_neighbor = None + self.east_neighbor = None + self.north_neighbor = None + self.south_neighbor = None + + self.west_vertice = None + self.east_vertice = None + self.north_vertice = None + self.south_vertice = None + + def fill(self, editor : Editor, material : str, y : int = 0) -> list[Point]: + geometry.placeCuboid(editor, (self.pos.x, 0, self.pos.z), (self.pos.x+self.size-1, y, self.pos.z+self.size-1), Block(material)) + + def get_neighbors_coords(self): + return [Point(x = self.pos.x, z = self.pos.z - self.size), # north + Point(x = self.pos.x + self.size, z = self.pos.z), # east + Point(x = self.pos.x, z = self.pos.z + self.size), # south + Point(x = self.pos.x - self.size, z = self.pos.z)] # west + + + def get_neighbor(self, direction) -> Point: + match(direction): + case DIRECTION.WEST: + return self.west_neighbor + case DIRECTION.EAST: + return self.east_neighbor + case DIRECTION.NORTH: + return self.north_neighbor + case DIRECTION.SOUTH: + return self.south_neighbor + + def set_neighbor(self, direction, neighbor : 'Tile'): + match(direction): + case DIRECTION.WEST: + self.west_neighbor = neighbor + case DIRECTION.EAST: + self.east_neighbor = neighbor + case DIRECTION.NORTH: + self.north_neighbor = neighbor + case DIRECTION.SOUTH: + self.south_neighbor = neighbor + + def get_vertice(self,vertice : int|DIRECTION) -> Vertice: + # gives the corresponding vertice : + # 0 = north, 1 = east, 2 = south, 3 = west + match(vertice): + case 0 : + return Vertice(self.north_west.copy(), self.north_east.copy(), DIRECTION.NORTH) + case 1 : + return Vertice(self.north_east.copy(), self.south_east.copy(), DIRECTION.EAST) + case 2 : + return Vertice(self.south_west.copy(), self.south_east.copy(), DIRECTION.SOUTH) + case 3 : + return Vertice(self.north_west.copy(), self.south_west.copy(), DIRECTION.WEST) + case DIRECTION.WEST : + return self.west_vertice + case DIRECTION.EAST : + return self.east_vertice + case DIRECTION.NORTH : + return self.north_vertice + case DIRECTION.SOUTH : + return self.south_vertice + + def set_vertice(self, direction : DIRECTION, vertice : Vertice, height : int): + self.has_vertice = True + vertice.point2.set_position(y = height) + match(direction): + case DIRECTION.WEST : + self.west_vertice = vertice + case DIRECTION.EAST : + self.east_vertice = vertice + case DIRECTION.NORTH : + self.north_vertice = vertice + case DIRECTION.SOUTH : + self.south_vertice = vertice \ No newline at end of file diff --git a/buildings/geometry/Vertice.py b/buildings/geometry/Vertice.py new file mode 100644 index 0000000..ca26622 --- /dev/null +++ b/buildings/geometry/Vertice.py @@ -0,0 +1,24 @@ +from utils.Enums import DIRECTION +from buildings.geometry.Point import Point +from buildings.geometry.Rectangle import Rectangle + +class Vertice(Rectangle): + def __init__(self, point1 : Point, point2 : Point, facing : DIRECTION = None): + Rectangle.__init__(self, point1, point2) + self.facing = facing + + def get_neighbors(self): + match self.facing: + case DIRECTION.NORTH | DIRECTION.SOUTH: + return [Point(x = self.point1.x - 1, z = self.point1.z), + Point(x = self.point2.x + 1, z = self.point2.z)] + case DIRECTION.EAST | DIRECTION.WEST: + return [Point(x = self.point1.x, z = self.point1.z - 1), + Point(x = self.point2.x, z = self.point2.z + 1)] + + def __len__(self): + return self.point2.x - self.point1.x + self.point2.z - self.point1.z + 1 + + def __repr__(self): + return super().__repr__() + f"\nFacing : {self.facing} \n\n" + \ No newline at end of file diff --git a/buildings/shapes.json b/buildings/shapes.json new file mode 100644 index 0000000..4cf2882 --- /dev/null +++ b/buildings/shapes.json @@ -0,0 +1,59 @@ +[ + { + "id": 0, + "name": "basic_shape", + "matrice":[[1]] + }, + { + "id": 1, + "name": "long_shape", + "matrice":[[0,1]] + }, + { + "id": 2, + "name": "double_long_shape", + "matrice":[[1,0,1]] + }, + { + "id": 3, + "name": "L_shape", + "matrice":[[1,0], + [1,1]] + }, + { + "id": 4, + "name": "U_shape", + "matrice":[[1,0,1], + [1,1,1]] + }, + { + "id": 5, + "name": "H_shape", + "matrice":[[1,0,1], + [1,1,1], + [1,0,1]] + }, + { + "id": 6, + "name": "X_shape", + "matrice":[[0,1,0], + [1,1,1], + [0,1,0]] + }, + { + "id": 7, + "name": "O_shape", + "matrice":[[1,1,1], + [1,0,1], + [1,1,1]] + }, + { + "id": 8, + "name": "E_shape", + "matrice":[[1,1,1], + [1,0,0], + [1,1,1], + [1,0,0], + [1,1,1]] + } +] \ No newline at end of file diff --git a/main.py b/main.py index 29cb455..56e4e09 100644 --- a/main.py +++ b/main.py @@ -1,22 +1,45 @@ -from gdpc import Editor, Block, geometry +from gdpc import Editor, Block, geometry, Transform import networks.curve as curve import numpy as np +from utils.JsonReader import JsonReader +from utils.YamlReader import YamlReader +from buildings.Building import Building + +from utils.functions import * editor = Editor(buffering=True) +# get every differents buildings shapes +f = JsonReader('buildings\shapes.json') +shapes = f.data + +# get the random data for the buildings +y = YamlReader('params.yml') +random_data = y.data + +#move your editor to the position you wanna build on +transform = Transform((0,-60,110),rotation = 0) +editor.transform.push(transform) + +# clear the area you build on +geometry.placeCuboid(editor, (-5,0,-8), (25,100,25), Block("air")) + +# create a building at the relative position 0,0 with 20 blocks length and 20 blocks width, with a normal shape and 10 floors +building = Building(random_data["buildings"], (0, 0), (20,20), shapes[0]['matrice'], 10) +# build it with your custom materials +building.build(editor, ["stone_bricks","glass_pane","glass","cobblestone_wall","stone_brick_stairs","oak_planks","white_concrete","cobblestone","stone_brick_slab","iron_bars"]) + + + + + + # # Get a block block = editor.getBlock((0,48,0)) -# # Place a block -editor.placeBlock((-5, -58, 0), Block("stone")) - # # Build a cube # geometry.placeCuboid(editor, (458, 92, 488), (468, 99, 471), Block("oak_planks")) -curve = curve.Curve([(396, 132, 740), (435, 138, 730), - (443, 161, 758), (417, 73, 729)]) -curve.compute_curve() - -#for point in curve.computed_points: -# print(point) -# editor.placeBlock(point, Block("stone")) +# curve = curve.Curve([(396, 132, 740), (435, 138, 730), +# (443, 161, 758), (417, 73, 729)]) +# curve.compute_curve() \ No newline at end of file diff --git a/metro/Metro_Line.py b/metro/Metro_Line.py new file mode 100644 index 0000000..6068d90 --- /dev/null +++ b/metro/Metro_Line.py @@ -0,0 +1,92 @@ +from math import sqrt, atan2 + + +class Position: + def __init__(self, x: int = 0, y: int = 0): + self.x = x + self.y = y + + def __add__(self, other: "Position") -> "Position": + return Position(self.x + other.x, self.y + other.y) + + def __sub__(self, other: "Position") -> "Position": + return Position(self.x - other.x, self.y - other.y) + + def __mul__(self, other: float) -> "Position": + return Position(int(self.x * other), int(self.y * other)) + + def __truediv__(self, other: float) -> "Position": + return Position(int(self.x / other), int(self.y / other)) + + def __str__(self): + return f"({self.x}, {self.y})" + + def __eq__(self, other: "Position"): + return self.x == other.x and self.y == other.y + + def distance_to(self, other: "Position") -> float: + return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2) + + def norm(self) -> float: + return sqrt(self.x ** 2 + self.y ** 2) + + def angle_to(self, other: "Position") -> float: + return atan2(self.y - other.y, other.x - self.x) + + +class Station: + """ + This class represents the position and link of a metro station. + """ + + def __init__(self, pos: Position, orientation: float, name: str = "Station"): + """ + Constructor of Station. + + :param pos: Position x and y of the station + :param orientation: The orientation of the station in radian (The angle is where the station is facing next) + :param name: The name of the station + """ + self.name = name + self.orientation = orientation + self.pos = pos + self.last_station = None + self.next_station = None + + def distance_to(self, station: "Station") -> float: + """ + Calculate the distance between two stations. + + :param station: The station to calculate the distance to + :return: The distance between two stations + """ + return self.pos.distance_to(station.pos) + + +class Metro_Line: + """ + This class represents the metro line. + """ + + def __init__(self, name: str = "Metro line A"): + """ + Constructor of Metro_Line. + + :param name: The name of the metro line + """ + self.name = name + self.stations = [] + + def add_station(self, station: Station): + """ + Add a station to the metro map. + + :param station: The station to be added + """ + self.stations.append(station) + if len(self.stations) > 1: + self.stations[-2].next_station = station + station.last_station = self.stations[-2] + + +__all__ = ["Metro_Line", "Station", "Position"] diff --git a/metro/metro_line_map.py b/metro/metro_line_map.py new file mode 100644 index 0000000..42c3892 --- /dev/null +++ b/metro/metro_line_map.py @@ -0,0 +1,404 @@ +from Metro_Line import * +from math import pi, cos, sin, sqrt, atan2, inf +from pygame import Surface +import pygame + +from metro.Metro_Line import Position + + +def cubic_bezier(time: float, join1: Position, control_point1: Position, control_point2: Position, + join2: Position) -> Position: + """ + Calculate the position of a point on a cubic Bézier curve at a given time + + Formula used : B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1 + 3(1-t) * t^2 * P2 + t^3 * P3 + + :param time: The time at which to calculate the position + :param join1: The first join point of the curve + :param control_point1: The first control point of the curve + :param control_point2: The second control point of the curve + :param join2: The second join point of the curve + :return: The position of the point on the curve at the given time + """ + return (join1 * ((1 - time) ** 3) + + control_point1 * 3 * ((1 - time) ** 2) * time + + control_point2 * 3 * (1 - time) * (time ** 2) + + join2 * (time ** 3)) + + +def cubic_bezier_derivative(time: float, join1: Position, control_point1: Position, control_point2: Position, + join2: Position) -> Position: + """ + Calculate the first derivative of a cubic Bézier curve at a given time + + Formula used : B'(t) = 3(1-t)^2 * (P1 - P0) + 6(1-t) * t * (P2 - P1) + 3t^2 * (P3 - P2) + + :param time: The time at which to calculate the derivative + :param join1: The first join point of the curve + :param control_point1: The first control point of the curve + :param control_point2: The second control point of the curve + :param join2: The second join point of the curve + :return: The derivative of the curve at the given time + """ + return ((control_point1 - join1) * 3 * ((1 - time) ** 2) + + (control_point2 - control_point1) * 6 * (1 - time) * time + + (join2 - control_point2) * 3 * (time ** 2)) + + +def cubic_bezier_second_derivative(time: float, join1: Position, control_point1: Position, control_point2: Position, + join2: Position) -> Position: + """ + Calculate the second derivative of a cubic Bézier curve at a given time + + Formula used : B''(t) = 6(1-t) * (P2 - 2P1 + P0) + 6t * (P3 - 2P2 + P1) + + :param time: The time at which to calculate the second derivative + :param join1: The first join point of the curve + :param control_point1: The first control point of the curve + :param control_point2: The second control point of the curve + :param join2: The second join point of the curve + :return: The second derivative of the curve at the given time + """ + return ((control_point2 - control_point1 * 2 + join1) * 6 * (1 - time) + + (join2 - control_point2 * 2 + control_point1) * 6 * time) + + +def bezier_curve(control_points, num_points) -> tuple[list[Position], list[Position], list[Position]]: + """ + Generate a Bézier curve from a list of control points + + :param control_points: The control points of the curve + :param num_points: The number of points to generate + :return: A tuple containing the points of the curve, the derivative of the curve, + and the second derivative of the curve + """ + points = [] + derivative = [] + second_derivative = [] + for t in range(num_points + 1): + points.append(cubic_bezier(t / num_points, *control_points)) + derivative.append(cubic_bezier_derivative(t / num_points, *control_points)) + second_derivative.append(cubic_bezier_second_derivative(t / num_points, *control_points)) + return points, derivative, second_derivative + + +def osculating_circle(points: list[Position], derivative: list[Position], second_derivative: list[Position]) \ + -> list[tuple[int, Position]]: + """ + Calculate the osculating circle at each point of a curve + An osculating circle is the circle that best approximates the curve at a given point + + Source : https://en.wikipedia.org/wiki/Osculating_circle + + :param points: The points of the curve + :param derivative: The derivative of the curve + :param second_derivative: The second derivative of the curve + :return: A list of tuples, each containing the radius and center of each osculating circle + """ + circle = [] + for i in range(len(points)): + curvature = (abs(derivative[i].x * second_derivative[i].y - derivative[i].y * second_derivative[i].x) + / ((derivative[i].x ** 2 + derivative[i].y ** 2) ** 1.5)) + if curvature != 0: + radius = 1 / curvature + normal = derivative[i].norm() + cross_product = derivative[i].x * second_derivative[i].y - derivative[i].y * second_derivative[i].x + if cross_product > 0: + center = points[i] + Position(-derivative[i].y * radius / normal, derivative[i].x * radius / normal) + else: + center = points[i] + Position(derivative[i].y * radius / normal, -derivative[i].x * radius / normal) + circle.append((int(radius), center)) + return circle + + +def merge_similar_circles(circles: list[tuple[int, Position]], radius_threshold: float, center_threshold: float) \ + -> list[tuple[int, Position]]: + """ + Merge similar osculating circles + + :param circles: The osculating circles to merge + :param radius_threshold: The maximum difference in radius for two circles to be considered similar + :param center_threshold: The maximum distance between the centers of two circles to be considered similar + :return: The merged osculating circles + """ + merged_circles = [] + i = 0 + while i < len(circles) - 1: + radius1, center1 = circles[i] + radius2, center2 = circles[i + 1] + if abs(radius1 - radius2) <= radius_threshold and center1.distance_to(center2) <= center_threshold: + merged_radius = (radius1 + radius2) // 2 + merged_center = Position((center1.x + center2.x) // 2, (center1.y + center2.y) // 2) + merged_circles.append((merged_radius, merged_center)) + i += 2 + else: + merged_circles.append(circles[i]) + i += 1 + if i < len(circles): + merged_circles.append(circles[i]) + + if len(merged_circles) == len(circles): + return merged_circles + else: + return merge_similar_circles(merged_circles, radius_threshold, center_threshold) + + +def circle_intersection(circle1: tuple[int, Position], circle2: tuple[int, Position]) -> list[Position]: + distance = circle1[1].distance_to(circle2[1]) + + if (distance > circle1[0] + circle2[0] or distance < abs(circle1[0] - circle2[0]) + or (distance == 0 and circle1[0] == circle2[0])): + return [] + + distance_line_circle = (circle1[0] ** 2 - circle2[0] ** 2 + distance ** 2) / (2 * distance) + distance_line_intersec_point = sqrt(circle1[0] ** 2 - distance_line_circle ** 2) + p = circle1[1] + (circle2[1] - circle1[1]) * distance_line_circle / distance + + return [Position(int(p.x + distance_line_intersec_point * (circle2[1].y - circle1[1].y) / distance), + int(p.y - distance_line_intersec_point * (circle2[1].x - circle1[1].x) / distance)), + Position(int(p.x - distance_line_intersec_point * (circle2[1].y - circle1[1].y) / distance), + int(p.y + distance_line_intersec_point * (circle2[1].x - circle1[1].x) / distance))] + + +def closest_to_curve(points: list[Position], curve_points: list[Position]) -> Position: + closest_point = Position() + distance = inf + for point in points: + for curve_point in curve_points: + distance_point_curve = point.distance_to(curve_point) + if distance_point_curve < distance: + distance = distance_point_curve + closest_point = point + return closest_point + + +def midpoint_circle_segment(circle: tuple[int, Position], start_point: Position, end_point: Position, curve_points: list[Position]) -> list[ + Position]: + points = [] + + start_angle = circle[1].angle_to(start_point) + end_angle = circle[1].angle_to(end_point) + + if start_angle < 0: + start_angle += 2 * pi + if end_angle < 0: + end_angle += 2 * pi + if start_angle > end_angle: + start_angle, end_angle = end_angle, start_angle + + middle_angle = (start_angle+end_angle)/2 + middle_point = circle[1] + Position(int(circle[0]*cos(middle_angle)), -int(circle[0]*sin(middle_angle))) + is_outside_point = closest_to_curve([middle_point, circle[1]], curve_points) == middle_point + + x0, y0 = circle[1].x, circle[1].y + x = circle[0] + y = 0 + err = 0 + + while x >= y: + for (x1, y1) in [(x0 + x, y0 + y), (x0 + y, y0 + x), (x0 - y, y0 + x), (x0 - x, y0 + y), + (x0 - x, y0 - y), (x0 - y, y0 - x), (x0 + y, y0 - x), (x0 + x, y0 - y)]: + angle = atan2(y0 - y1, x1 - x0) + if angle < 0: + angle += 2*pi + if is_outside_point: + if start_angle <= angle <= end_angle: + points.append(Position(int(x1), int(y1))) + else: + if angle <= start_angle or end_angle <= angle: + points.append(Position(int(x1), int(y1))) + + if err <= 0: + y += 1 + err += 2 * y + 1 + if err > 0: + x -= 1 + err -= 2 * x + 1 + + return points + + +def calculate_control_points(station, next_station, curve_factor) -> tuple[Position, Position]: + """ + Calculate the control points for a Bézier curve between two stations + + :param station: The first station + :param next_station: The second station + :param curve_factor: The factor to multiply the distance between stations to create the control points + :return: A tuple containing the control points for the curve + """ + distance = station.distance_to(next_station) + + control_point_pos = station.pos + Position(cos(station.orientation) * distance * curve_factor, + -sin(station.orientation) * distance * curve_factor) + + control_point_next_pos = next_station.pos + Position( + cos(next_station.orientation + pi) * distance * curve_factor, + - sin(next_station.orientation + pi) * distance * curve_factor) + + return control_point_pos, control_point_next_pos + + +def metro_line_osculating_circles(metro: Metro_Line, curve_factor: float = 0.5, num_points_factor: float = 1 / 20) -> ( + tuple)[list[tuple[int, Position]], list[Position]]: + """ + Calculate the osculating circles of a metro line + + :param metro: The metro line to calculate the osculating circles of + :param curve_factor: How much the control points should be offset from the stations + :param num_points_factor: How many points to generate for each segment of the curve + :return: A tuple containing the osculating circles and the points of the metro line curve + """ + print(f"[METRO LINE] Calculating osculating circles of the metro line {metro.name}") + circles = [] + points_list = [] + for i in range(len(metro.stations) - 1): + print(f"[METRO LINE] Calculating between {metro.stations[i].name} and {metro.stations[i].next_station.name}") + station = metro.stations[i] + + distance = station.distance_to(station.next_station) + + control_point_pos, control_point_next_pos = calculate_control_points(station, station.next_station, + curve_factor) + + points, derivatives, second_derivatives = bezier_curve( + [station.pos, control_point_pos, control_point_next_pos, station.next_station.pos], + int(distance * num_points_factor)) + + osculating_circles = osculating_circle(points, derivatives, second_derivatives) + merged_circles = merge_similar_circles(osculating_circles, 50, 50) + print( + f"[METRO LINE] {len(osculating_circles) - len(merged_circles)} out of {len(osculating_circles)} circles deleted !") + circles.extend(merged_circles) + points_list.extend(points) + print(f"[METRO LINE] Osculating circles done") + return circles, points_list + + +# --- DRAW PART --- + +def draw_osculating_circle(circle: list[tuple[int, Position]], surface: Surface): + """ + :param circle: The osculating circles to draw + :param surface: The surface on which to draw the circles + """ + for radius, center in circle: + pygame.draw.circle(surface, (255, 0, 0), (center.x, center.y), int(radius), 1) + pygame.draw.circle(surface, (0, 0, 255), (center.x, center.y), 10) + + +def draw_station(station: Station, surface: Surface): + """ + :param station: The station to draw + :param surface: The surface on which to draw the station + """ + pygame.draw.circle(surface, (255, 255, 255), (station.pos.x, station.pos.y), 10) + + +def draw_points(points: list[Position], surface): + """ + :param points: The points to draw + :param surface: The surface on which to draw the points + """ + for point in points: + pygame.draw.circle(surface, (40, 255, 40), (point.x, point.y), 5) + + +def draw_point(point: Position, surface): + pygame.draw.circle(surface, (40, 255, 40), (point.x, point.y), 5) + + +def draw_pixels(points: list[Position], surface): + for point in points: + surface.set_at((point.x, point.y), (0, 255, 255)) + + +def draw_metro_line(metro: Metro_Line, surface: Surface, show_points: bool = True): + """ + :param metro: The metro line to draw + :param surface: The surface on which to draw the metro line + :param show_points: Whether to show the points of the curve + """ + for i in range(len(metro.stations) - 1): + station = metro.stations[i] + draw_station(station, surface) + draw_station(station.next_station, surface) + + circles, points = metro_line_osculating_circles(metro) + draw_osculating_circle(circles, surface) + for i in range(1, len(circles) - 1): + intersect_point_circle_before = closest_to_curve(circle_intersection(circles[i - 1], circles[i]), points) + intersect_point_circle_after = closest_to_curve(circle_intersection(circles[i], circles[i + 1]), points) + if intersect_point_circle_before == Position(): + continue + intersect_point_circle_before = circles[i - 1][1] + else: + draw_point(intersect_point_circle_before, surface) + + if intersect_point_circle_after == Position(): + continue + intersect_point_circle_after = circles[i + 1][1] + else: + draw_point(intersect_point_circle_after, surface) + + points_midpoint = midpoint_circle_segment(circles[i], intersect_point_circle_before, + intersect_point_circle_after, points) + draw_pixels(points_midpoint, surface) + + if len(points) != 0: + intersect_point_circle_before = points[0] + intersect_point_circle_after = closest_to_curve(circle_intersection(circles[0], circles[1]), points) + points_midpoint = midpoint_circle_segment(circles[0], intersect_point_circle_before, + intersect_point_circle_after, points) + draw_pixels(points_midpoint, surface) + + intersect_point_circle_before = points[-1] + intersect_point_circle_after = closest_to_curve(circle_intersection(circles[-1], circles[-2]), points) + points_midpoint = midpoint_circle_segment(circles[-1], intersect_point_circle_before, + intersect_point_circle_after, points) + draw_pixels(points_midpoint, surface) + + +def interface(): + """ + Interface for creating a metro line + + Control : + + - Up arrow ↑ Create a station facing north + + - Down arrow ↓ Create a station facing south + + - Left arrow ← Create a station facing west + + - Right arrow → Create a station facing east + """ + metro = Metro_Line('A') + surface = pygame.display.set_mode((1000, 1000)) + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + return + + if event.type == pygame.KEYDOWN: + angle = 0 + if event.key == pygame.K_UP: + angle = pi / 2 + elif event.key == pygame.K_DOWN: + angle = -pi / 2 + elif event.key == pygame.K_LEFT: + angle = pi + x, y = pygame.mouse.get_pos() + metro.add_station(Station(Position(x, y), angle, str(len(metro.stations)))) + draw_metro_line(metro, surface) + + pygame.display.flip() + + +def main(): + interface() + + +if __name__ == "__main__": + main() diff --git a/params.yml b/params.yml new file mode 100644 index 0000000..5ce03f5 --- /dev/null +++ b/params.yml @@ -0,0 +1,91 @@ +# contains all random variables +buildings: + tile_size: + min_tile_size: 3 + max_tile_size: 12 + + foundations: + collumn_style : + # proportion of each style + none: 1 + inner: 5 + outer: 1 + both: 1 + floor: + min_height: 4 + max_height: 7 + + facade: + + windows: + size: + min_height: 2 + max_height: 6 + min_width: 1 + max_width: 12 + crossbars: + min_height_for_vertical_crossbar: 3 + vertical_crossbar: 0.25 + min_width_for_horizontal_crossbar: 3 + horizontal_crossbar: 0.25 + grounded: 0.5 + # alternate between block and pane + alternate: 0.5 + multiple: + # min size and probability of multiple windows on the same vertice + min_width: 5 + proba: 0.5 + border_radius: + # proportion of each style + none: 2 + top: 1 + top_and_bottom: 1 + + balcony: + proba : 0.25 + growth: 0.5 # [growth]% chance to have min_width + 1 balcony length, [growth**2]% chance to have min_width + 2 balcony length, etc + size: + min_len : 1 + max_len : 3 + min_width : 3 + multiple: + # probability to have multiple balcony IF POSSIBLE + # this feature need a very large facade + proba: 1 + min_width: 5 + min_gap: 1 + details: 0.35 + border_radius: + # proportion of each style + none: 6 + # no difference if there is no details + medium: 1 + full: 1 + + inter_floor: + proba: 0.5 + border_style: + # bloc used to fill the corner of the interfloor + none: 1 + slab: 2 + stairs: 2 + + entrance: + centered: 0.8 + different_facade: 0.75 + size: + min_height: 5 + max_height: 9 + door: + size: + min_height: 2 + max_height: 4 + min_width: 1 + max_width: 3 + padding: + max: 2 + max_top: 2 + + roof: + rembard: 0.5 + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1ef7339..c763a55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,8 @@ gdpc==7.1.0 -matplotlib==3.8.2 numpy==1.26.4 -scipy==1.13.0 +Pillow==10.3.0 +pygame==2.5.2 +scipy==1.13.1 +skan==0.11.1 +skimage==0.0 +pyyaml==6.0.1 diff --git a/utils/Enums.py b/utils/Enums.py new file mode 100644 index 0000000..a40fcbe --- /dev/null +++ b/utils/Enums.py @@ -0,0 +1,28 @@ +from enum import Enum + +class DIRECTION(Enum): + NORTH = 0 + EAST = 1 + SOUTH = 2 + WEST = 3 + +class COLLUMN_STYLE(Enum): + NONE = 0 + INNER = 1 + OUTER = 2 + BOTH = 3 + +class WINDOW_BORDER_RADIUS(Enum): + NONE = 0 + TOP = 1 + TOP_AND_BOTTOM = 2 + +class BALCONY_BORDER_RADIUS(Enum): + NONE = 0 + MEDIUM = 1 + FULL = 2 + +class INTER_FLOOR_BORDER(Enum): + NONE = 0 + SLAB = 1 + STAIRS = 2 \ No newline at end of file diff --git a/utils/JsonReader.py b/utils/JsonReader.py new file mode 100644 index 0000000..c2b462c --- /dev/null +++ b/utils/JsonReader.py @@ -0,0 +1,11 @@ +import json + +class JsonReader: + def __init__(self, json_file): + self.data = self._load_json(json_file) + + def _load_json(self, json_file : str): + f = open(json_file) + js = json.load(f) + + return js \ No newline at end of file diff --git a/utils/YamlReader.py b/utils/YamlReader.py new file mode 100644 index 0000000..4b2e883 --- /dev/null +++ b/utils/YamlReader.py @@ -0,0 +1,11 @@ +import yaml + +class YamlReader: + def __init__(self, yaml_file): + self.data = self._load_yaml(yaml_file) + + def _load_yaml(self, yaml_file : str): + with open(yaml_file, 'r') as stream: + data_loaded = yaml.safe_load(stream) + + return data_loaded \ No newline at end of file diff --git a/utils/functions.py b/utils/functions.py new file mode 100644 index 0000000..aec47f8 --- /dev/null +++ b/utils/functions.py @@ -0,0 +1,6 @@ +from enum import Enum +import random as rd + +def select_random(rdata : dict, enum : Enum) -> Enum: + # select a random value of the dict according to his coef and return the corresponding value in the enum + return enum[rd.choice([elt for elt,num in rdata.items() for _ in range(num)]).upper()] \ No newline at end of file diff --git a/world_maker/Block.py b/world_maker/Block.py new file mode 100644 index 0000000..33733ee --- /dev/null +++ b/world_maker/Block.py @@ -0,0 +1,34 @@ +from gdpc import Editor, Block, geometry +from gdpc.lookup import * +import numpy as np +import math + +SOLID_NATURAL_BLOCKS = SOILS | STONES | ORES | LIQUIDS + +class Block: + def __init__(self, coordinates:tuple, name:str): + self.coordinates = coordinates + self.name = name + self.neighbors = [] + self.surface = None + + + def addNeighbors(self, neighbors:list[Block]): + for neighbor in neighbors: + self.neighbors.append(neighbor) + + def isSurface(self): + if self.surface == None: + if str(self.name) in SOLID_NATURAL_BLOCKS: + for neighbor in self.neighbors: + if str(neighbor.name) not in SOLID_NATURAL_BLOCKS: + self.surface = True + return True + if len(self.neighbors) != 0: + self.surface = False + return False + else: + self.surface = False + return False + else: + return self.surface \ No newline at end of file diff --git a/world_maker/City.py b/world_maker/City.py new file mode 100644 index 0000000..8611be7 --- /dev/null +++ b/world_maker/City.py @@ -0,0 +1,153 @@ +from District import District, Road +from Position import Position +from PIL import Image +import random +from data_analysis import handle_import_image +from typing import Union + + +class City: + """ + Attributes: + districts (list): The list of districts in the city. + map_data (list): The 2D list representing the map of the city. + height_map (list): The 2D list representing the height map of the city. + """ + + def __init__(self): + """ + The constructor for the City class. + """ + self.districts = [] + self.map_data = [] + self.height_map = [] + self.init_maps() + + def init_maps(self): + """ + Initialize the maps of the city. It reads the heightmap and watermap images and converts them into 2D lists. + """ + heightmap = Image.open('./data/heightmap.png').convert('L') + watermap = Image.open('./data/watermap.png').convert('L') + width, height = heightmap.size + self.map_data = [[-1 if watermap.getpixel((x, y)) > 0 else 0 for x in range(width)] for y in range(height)] + self.height_map = [[heightmap.getpixel((x, y)) for x in range(width)] for y in range(height)] + watermap.close() + heightmap.close() + + def add_district(self, center: Position, district_type: str = ""): + """ + Add a new district to the city. + + :param district_type: + :param center: The center position of the new district. + """ + self.districts.append(District(len(self.districts) + 1, center, district_type)) + self.map_data[center.y][center.x] = len(self.districts) + + def is_expend_finished(self): + """ + Check if the expansion of all districts in the city is finished. + + :return: True if the expansion is finished, False otherwise. + """ + for district in self.districts: + if len(district.area_expend_from_point) > 0: + return False + return True + + def choose_expend_point(self, point: Position, index_district: int): + """ + Choose a point to expand a district based on the distance between the center of the district and the point itself. + + :param point: The point to be expanded. + :param index_district: The index of the district to be expanded. + """ + min_distance = point.distance_to(self.districts[index_district].center_expend) + index_district_chosen = index_district + for index in range(index_district + 1, len(self.districts)): + if point in self.districts[index].area_expend: + distance = point.distance_to(self.districts[index].center_expend) + if distance < min_distance: + min_distance = distance + self.districts[index_district_chosen].area_expend.remove(point) + index_district_chosen = index + else: + self.districts[index].area_expend.remove(point) + self.districts[index_district_chosen].area_expend_from_point.append(point) + self.districts[index_district_chosen].area_expend.remove(point) + self.map_data[point.y][point.x] = index_district_chosen + 1 + + def update_expend_district(self): + """ + Update the expansion points of all districts in the city. + """ + for district in self.districts: + if len(district.area_expend_from_point) > 0: + district.update_expend_points(district.area_expend_from_point[0], self.map_data, self.height_map) + for district in self.districts: + for point in district.area_expend: + self.choose_expend_point(point, district.tile_id - 1) + + def loop_expend_district(self): + """ + Loop the expansion of all districts in the city until all districts are fully expanded. + """ + print("[City] Start expanding districts...") + while not self.is_expend_finished(): + self.update_expend_district() + print("[City] Finished expanding districts.") + + def district_draw_map(self): + """ + Draw the map of the city with different colors for each district. + """ + width, height = len(self.map_data[0]), len(self.map_data) + img = Image.new('RGB', (width, height)) + colors = {id_district: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + for id_district in range(1, len(self.districts) + 1)} + + for y in range(height): + for x in range(width): + if self.map_data[y][x] <= 0: + img.putpixel((x, y), (0, 0, 0)) + else: + img.putpixel((x, y), colors[self.map_data[y][x]]) + + img.save('./data/district.png') + print("[City] District map created.") + + def draw_roads(self, image: Union[str, Image], size: int = 1) -> Image: + """ + Draw the roads of the city on the image. + + :param size: + :param image: The image to draw the roads on. + """ + image = handle_import_image(image) + for district in self.districts: + district.draw_roads(image, size) + return image + + def district_generate_road(self) -> list[Road]: + """ + Generate the roads of the city for each district. + + :return: The list of roads of the city. + """ + roads = [] + for district in self.districts: + district.generate_roads(self.map_data) + roads.extend(district.roads) + return roads + + +if __name__ == '__main__': + city = City() + for i in range(10): + city.add_district(Position(random.randint(0, 400), random.randint(0, 400))) + city.loop_expend_district() + city.district_draw_map() + city.district_generate_road() + image = city.draw_roads(Image.new('RGB', (401, 401)),4) + image.save('./data/roadmap.png') diff --git a/world_maker/District.py b/world_maker/District.py new file mode 100644 index 0000000..4a2f887 --- /dev/null +++ b/world_maker/District.py @@ -0,0 +1,235 @@ +from Position import Position +from typing import Union +from random import randint +from PIL import Image + +class Road: + def __init__(self, position: Position, id_height: int, id_width: int, border: bool = False): + self.position: Position = position + self.north: Union[Road, None] = None + self.south: Union[Road, None] = None + self.east: Union[Road, None] = None + self.west: Union[Road, None] = None + self.id_height = id_height + self.id_width = id_width + self.border = border + + +class District: + """ + The District class represents a district that can be expanded. + + Attributes: + center_expend (Position): The center position from which the district expands. + area (list): The list of positions that are part of the district. + area_expend_from_point (list): The list of positions from which the district can expand. + area_expend (list): The list of positions to which the district will maybe expand. + """ + + def __init__(self, tile_id: int, center: Position, district_type: str = ""): + """ + The constructor for the District class. + + :param tile_id: Unique id of the district (Must be greater than 0) + :param center: The center position from which the district expands. + :param district_type: The type of the district (Forest, City, Mountain, Villa) + """ + if tile_id <= 0: + raise ValueError("Tile id must be greater than 0") + self.tile_id = tile_id + self.type = district_type + self.center_expend = center + self.area_expend_from_point = [center] + self.area_expend = [] + self.roads: list[Road] = [] + self.roads_expend = [] + + def verify_point(self, point: Position, point_new: Position, map_data: list[list[int]], + height_map: list[list[int]]): + """ + Verify if a new point can be added to a district extend area list. + + :param point: The current point. + :param point_new: The new point to be verified. + :param map_data: The 2D list representing the map. + :param height_map: The 2D list representing the height map. + :return: True if the new point can be added, False otherwise. + """ + return (0 <= point_new.x < len(map_data[0]) and + 0 <= point_new.y < len(map_data) and + map_data[point_new.y][point_new.x] == 0 and + (self.type == "Mountain" or + abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2)) + + def is_point_inside(self, point: Position, map_data) -> bool: + """ + Check if a point is inside the district. + + :param point: The point to be checked. + :return: True if the point is inside the district, False otherwise. + """ + if not (0 <= point.x < len(map_data[0]) and 0 <= point.y < len(map_data)): + return False + return map_data[point.y][point.x] == self.tile_id + + def is_position_in_area_expend(self, position: Position) -> bool: + """ + Check if a position is inside the district. + + :param position: The position to be checked. + :return: True if the position is inside the district, False otherwise. + """ + for point in self.area_expend: + if point == position: + return True + return False + + def update_expend_points(self, point: Position, map_data: list[list[int]], height_map: list[list[int]]): + """ + Update the points to which the district can expand. + + :param point: The current point. + :param map_data: The 2D list representing the map. + :param height_map: The 2D list representing the height map. + """ + for pos in [Position(1, 0), Position(-1, 0), Position(0, 1), Position(0, -1)]: + if self.verify_point(point, point + pos, map_data, height_map): + if not self.is_position_in_area_expend(point + pos): + self.area_expend.append(point + pos) + self.area_expend_from_point.remove(point) + + def move_point_to_area(self, point: Position, vector: Position, map_data) -> Position: + while not self.is_point_inside(point + vector, map_data): + point += vector + return point + vector + + def get_road_from_point(self, point: Position) -> Union[Road, None]: + """ + Get the road that contains a specific point. + + :param point: The point to be checked. + :return: The road that contains the point. + """ + for road in self.roads: + if point == road.position: + return road + return None + + def get_road_expend_from_point(self, point: Position) -> Union[Road, None]: + """ + Get the road that contains a specific point. + + :param point: The point to be checked. + :return: The road that contains the point. + """ + for road in self.roads_expend: + if point == road.position: + return road + return None + + def generate_roads(self, map_data, random_range=(20, 40)): + width = {0: self.center_expend.x} + height = {0: self.center_expend.y} + self.roads_expend = [Road(self.center_expend, 0, 0)] + self.roads = [self.roads_expend[0]] + while len(self.roads_expend) > 0: + road = self.roads_expend.pop(0) + print(road.position) + for id_width in [-1, 1]: + if road.id_width + id_width not in width: + width[road.id_width + id_width] = width[road.id_width] + randint(random_range[0], + random_range[1]) * id_width + road_new = Road(Position(width[road.id_width + id_width], road.position.y), + road.id_height, road.id_width + id_width) + if self.is_point_inside(road_new.position, map_data): + road_search = self.get_road_from_point(road_new.position) + road_expend_search = self.get_road_expend_from_point(road_new.position) + if road_search is not None: + road_new = road_search + + if id_width == -1: + road.west = road_new + road_new.east = road + else: + road.east = road_new + road_new.west = road + + if road_search is None: + self.roads.append(road_new) + self.roads_expend.append(road_new) + else: + self.roads[self.roads.index(road_search)] = road_new + if road_expend_search is not None: + self.roads_expend[self.roads_expend.index(road_expend_search)] = road_new + else: + point_new = self.move_point_to_area(road_new.position, Position(-id_width, 0), map_data) + road_new = Road(point_new, road.id_height, road.id_width + id_width, True) + if id_width == -1: + road.west = road_new + road_new.east = road + else: + road.east = road_new + road_new.west = road + self.roads.append(road_new) + + for id_height in [-1, 1]: + if road.id_height + id_height not in height: + height[road.id_height + id_height] = height[road.id_height] + randint(random_range[0], + random_range[1]) * id_height + road_new = Road(Position(road.position.x, height[road.id_height + id_height]), + road.id_height + id_height, road.id_width) + if self.is_point_inside(road_new.position, map_data): + road_search = self.get_road_from_point(road_new.position) + road_expend_search = self.get_road_expend_from_point(road_new.position) + if road_search is not None: + road_new = road_search + + if id_height == -1: + road.north = road_new + road_new.south = road + else: + road.south = road_new + road_new.north = road + + if road_search is None: + self.roads.append(road_new) + self.roads_expend.append(road_new) + else: + self.roads[self.roads.index(road_search)] = road_new + if road_expend_search is not None: + self.roads_expend[self.roads_expend.index(road_expend_search)] = road_new + else: + pass + point_new = self.move_point_to_area(road_new.position, Position(0, -id_height), map_data) + road_new = Road(point_new, road.id_height + id_height, road.id_width, True) + if id_height == -1: + road.north = road_new + road_new.south = road + else: + road.south = road_new + road_new.north = road + self.roads.append(road_new) + + def draw_roads(self, image: Image, size: int = 1): + for road in self.roads: + image.putpixel((road.position.x, road.position.y), (255, 255, 255)) + if road.north is not None: + for y in range(road.position.y, road.north.position.y): + image = draw_square(image, Position(road.position.x, y), size) + if road.south is not None: + for y in range(road.position.y, road.south.position.y): + image = draw_square(image, Position(road.position.x, y), size) + if road.east is not None: + for x in range(road.position.x, road.east.position.x): + image = draw_square(image, Position(x, road.position.y), size) + if road.west is not None: + for x in range(road.position.x, road.west.position.x): + image = draw_square(image, Position(x, road.position.y), size) + + +def draw_square(image, center: Position, size: int) -> Image: + for x in range(center.x - size, center.x + size): + for y in range(center.y - size, center.y + size): + if 0 <= x < image.width and 0 <= y < image.height: + image.putpixel((x, y), (255, 255, 255)) + return image diff --git a/world_maker/Position.py b/world_maker/Position.py new file mode 100644 index 0000000..a488037 --- /dev/null +++ b/world_maker/Position.py @@ -0,0 +1,37 @@ +from math import sqrt, atan2 + + +class Position: + def __init__(self, x: int = 0, y: int = 0): + self.x = x + self.y = y + + def __add__(self, other: "Position") -> "Position": + return Position(self.x + other.x, self.y + other.y) + + def __sub__(self, other: "Position") -> "Position": + return Position(self.x - other.x, self.y - other.y) + + def __mul__(self, other: float) -> "Position": + return Position(int(self.x * other), int(self.y * other)) + + def __truediv__(self, other: float) -> "Position": + return Position(int(self.x / other), int(self.y / other)) + + def __str__(self): + return f"({self.x}, {self.y})" + + def __eq__(self, other: "Position"): + return self.x == other.x and self.y == other.y + + def get_tuple(self) -> tuple[int, int]: + return self.x, self.y + + def distance_to(self, other: "Position") -> float: + return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2) + + def norm(self) -> float: + return sqrt(self.x ** 2 + self.y ** 2) + + def angle_to(self, other: "Position") -> float: + return atan2(self.y - other.y, other.x - self.x) diff --git a/world_maker/Skeleton.py b/world_maker/Skeleton.py new file mode 100644 index 0000000..6b4d1ce --- /dev/null +++ b/world_maker/Skeleton.py @@ -0,0 +1,215 @@ +import numpy as np +#import skan +from skimage.morphology import skeletonize +from skan.csr import skeleton_to_csgraph +from collections import Counter +from PIL import Image +import random + + +class Skeleton: + def __init__(self, data: np.ndarray = None): + self.lines = [] + self.intersections = [] + self.centers = [] + self.coordinates = [] + self.graph = None + if data is not None: + self.set_skeleton(data) + + def set_skeleton(self, data: np.ndarray): + print("[Skeleton] Start skeletonization...") + binary_skeleton = skeletonize(data, method="lee") + + graph, coordinates = skeleton_to_csgraph(binary_skeleton) + self.graph = graph.tocoo() + + # List of lists. Inverted coordinates. + coordinates = list(coordinates) + # print(coordinates) + for i in range(len(coordinates)): + coordinates[i] = list(coordinates[i]) + # print(coordinates) + + for i in range(len(coordinates[0])): + # print((coordinates[0][i], coordinates[1][i], coordinates[2][i])) + self.coordinates.append((coordinates[0][i], coordinates[1][i], coordinates[2][i])) + print("[Skeleton] Skeletonization completed.") + + def find_next_elements(self, key: str) -> list: + """Find the very nearest elements""" + + line = [] + + values = np.array(self.graph.row) + indices = np.where(values == key)[0] + + for i in range(len(indices)): + if self.graph.row[indices[i]] == key: + line.append(self.graph.col[indices[i]]) + return line + + def find_line(self, key: str): + next_keys = self.find_next_elements(key) + + if len(next_keys) >= 3: # Intersections. + return next_keys + + if len(next_keys) == 2 or len(next_keys) == 1: # In line or endpoints. + line = [key] + line.insert(0, next_keys[0]) + if len(next_keys) == 2: + line.insert(len(line), next_keys[1]) + + next_keys = line[0], line[-1] + + while len(next_keys) == 2 or len(next_keys) == 1: + extremity = [] + for key in next_keys: + next_keys = self.find_next_elements(key) + + if len(next_keys) <= 2: + # Add the neighbors that is not already in the line. + for element in next_keys: + if element not in line: + extremity.append(element) + line.append(element) + + if len(next_keys) >= 3: + # Add the intersection only. + extremity.append(key) + + next_keys = [] + for key in extremity: + ends = self.find_next_elements(key) + if len(ends) == 2: + next_keys.append(key) + return line + + def parse_graph(self, parse_orphan: bool = False): + print("[Skeleton] Start parsing the graph", ("with orphans" if parse_orphan else "") + "...") + for key, value in sorted( + Counter(self.graph.row).items(), key=lambda kv: kv[1], reverse=True + ): + # Start from the biggest intersections. + if value != 2: # We don't want to be in the middle of a line. + line = self.find_line(key) + + # We have now all the connected points if it's an + # intersection. We need to find the line. + + if value != 1: + # It's not an endpoint. + self.centers.append(key) + self.intersections.append(line) + for i in line: + line = self.find_line(i) + + if i in line: + # The key is inside the result : it's a line. + already_inside = False + for l in self.lines: + # Verification if not already inside. + if Counter(l) == Counter(line): + already_inside = True + # print(line, "inside", lines) + + if not already_inside: + self.lines.append(line) + else: + # The key is not inside the result, it's an + # intersection directly connected to the key. + line = [key, i] + already_inside = False + for l in self.lines: + # Verification if not already inside. + if Counter(l) == Counter(line): + already_inside = True + # print(line, "inside", lines) + + if not already_inside: + self.lines.append(line) + elif value == 2 and parse_orphan: + line = self.find_line(key) + already_inside = False + for l in self.lines: + # Verification if not already inside. + if Counter(l) == Counter(line): + already_inside = True + + if not already_inside: + self.lines.append(line) + print("[Skeleton] Graph parsing completed.") + + def map(self) -> Image: + """ + + Generate an image to visualize 2D path of the skeleton. + + Returns: + image: 2D path of the skeleton on top of the heightmap. + """ + print("[Skeleton] Start mapping the skeleton...") + # editor = Editor() + + # buildArea = editor.getBuildArea() + # buildRect = buildArea.toRect() + # xzStart = buildRect.begin + # xzDistance = (max(buildRect.end[0], buildRect.begin[0]) - min(buildRect.end[0], buildRect.begin[0]), + # max(buildRect.end[1], buildRect.begin[1]) - min(buildRect.end[1], buildRect.begin[1])) + + heightmap = Image.open("data/heightmap.png").convert('RGB') + # roadsArea = Image.new("L", xzDistance, 0) + # width, height = heightmap.size + + # Lines + for i in range(len(self.lines)): + r, g, b = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + + for j in range(len(self.lines[i])): + z = self.coordinates[self.lines[i][j]][0] + # y = self.coordinates[self.lines[i][j]][1] + x = self.coordinates[self.lines[i][j]][2] + + heightmap.putpixel( + ( + int(z), + int(x), + ), + (r + j, g + j, b + j), + ) + + # roadsArea.putpixel( + # ( + # int(z), + # int(x), + # ), + # (255), + # ) + + # Centers + for i in range(len(self.centers)): + # print(self.coordinates[self.centers[i]]) + heightmap.putpixel( + (int(self.coordinates[self.centers[i]][0]), int(self.coordinates[self.centers[i]][2])), + (255, 255, 0), + ) + + # roadsArea.putpixel( + # (int(self.coordinates[self.centers[i]][0]), int(self.coordinates[self.centers[i]][2])), + # (255), + # ) + + # # Intersections + # for i in range(len(self.intersections)): + # intersection = [] + # for j in range(len(self.intersections[i])): + # intersection.append(self.coordinates[self.intersections[i][j]]) + + # for i in range(len(intersection)): + # heightmap.putpixel( + # (int(self.intersections[i][2]), int(self.intersections[i][0])), + # (255, 0, 255), + # ) + print("[Skeleton] Mapping completed.") + return heightmap # , roadsArea diff --git a/world_maker/World.py b/world_maker/World.py new file mode 100644 index 0000000..c771332 --- /dev/null +++ b/world_maker/World.py @@ -0,0 +1,233 @@ +from gdpc import Editor, geometry, lookup +import numpy as np +from PIL import Image +from Block import Block + +waterBiomes = [ + "minecraft:ocean", + "minecraft:deep_ocean", + "minecraft:warm_ocean", + "minecraft:lukewarm_ocean", + "minecraft:deep_lukewarm_ocean", + "minecraft:cold_ocean", + "minecraft:deep_cold_ocean", + "minecraft:frozen_ocean", + "minecraft:deep_frozen_ocean", + "minecraft:mushroom_fieds", + "minecraft:river", + "minecraft:frozen_river", +] + +waterBlocks = [ + "minecraft:water", +] + + +class World: + def __init__(self): + + editor = Editor(buffering=True) + buildArea = editor.getBuildArea() + + self.coordinates_min = [min(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + self.coordinates_max = [max(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + + self.length_x = self.coordinates_max[0] - self.coordinates_min[0] + 1 + self.length_y = self.coordinates_max[1] - self.coordinates_min[1] + 1 + self.length_z = self.coordinates_max[2] - self.coordinates_min[2] + 1 + + self.volume = [[[None for _ in range(self.length_z)] for _ in range(self.length_y)] for _ in + range(self.length_x)] + + def isInVolume(self, coordinates): + if (self.coordinates_min[0] <= coordinates[0] <= self.coordinates_max[0] and + self.coordinates_min[1] <= coordinates[1] <= self.coordinates_max[1] and + self.coordinates_min[2] <= coordinates[2] <= self.coordinates_max[2]): + return True + return False + + def addBlocks(self, blocks: list[Block]): + """ + Add block or list of block to the volume. + """ + + for block in blocks: + if self.isInVolume(block.coordinates): + self.volume[block.coordinates[0] - self.coordinates_min[0]][ + block.coordinates[1] - self.coordinates_min[1]][ + block.coordinates[2] - self.coordinates_min[2]] = block + + def removeBlock(self, volumeCoordinates): + """ + Add block or list of block to the volume. + """ + + self.volume[volumeCoordinates[0]][volumeCoordinates[1]][volumeCoordinates[2]] = None + + def getBlockFromCoordinates(self, coordinates): + """ + Use already created volume to get block data. + """ + + editor = Editor(buffering=True) + if self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] == None: + self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] = Block((coordinates[0], coordinates[1], coordinates[2]), + editor.getBlock((coordinates[0], coordinates[1], + coordinates[2])).id) + + return self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] + + def getNeighbors(self, Block): + for i in range(-1, 2): + for j in range(-1, 2): + for k in range(-1, 2): + if not (i == 0 and j == 0 and k == 0): + coordinates = (Block.coordinates[0] + i, Block.coordinates[1] + j, Block.coordinates[2] + k) + if self.isInVolume(coordinates): + Block.addNeighbors([self.getBlockFromCoordinates(coordinates)]) + + def setVolume(self): + """ + Scan the world with no optimization. Not tested on large areas. + """ + + editor = Editor(buffering=True) + + for x in range(self.coordinates_min[0], self.coordinates_max[0] + 1): + for y in range(self.coordinates_min[1], self.coordinates_max[1] + 1): + for z in range(self.coordinates_min[2], self.coordinates_max[2] + 1): + self.addBlocks([Block((x, y, z), editor.getBlock((x, y, z)).id)]) + + def getData(self): + """ + Generate all needed datas for the generator : heightmap, watermap, and preset the volume with data from the heightmap. + """ + + editor = Editor() + buildArea = editor.getBuildArea() + buildRect = buildArea.toRect() + + xzStart = buildRect.begin + print("[World]", '('+str(xzStart[0])+', '+str(xzStart[1])+')', "xzStart") + xzDistance = (max(buildRect.end[0], buildRect.begin[0]) - min(buildRect.end[0], buildRect.begin[0]), + max(buildRect.end[1], buildRect.begin[1]) - min(buildRect.end[1], buildRect.begin[1])) + watermap = Image.new("L", xzDistance, 0) + heightmap = Image.new("RGBA", xzDistance, 0) + treesmap = Image.new("RGBA", xzDistance, 0) + + slice = editor.loadWorldSlice(buildRect) + + heightmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING_NO_LEAVES"], dtype=np.uint8)) + treesmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING"], dtype=np.uint8)) + + for x in range(0, xzDistance[0]): + for z in range(0, xzDistance[1]): + y = heightmapData[x][z] - 1 + yTree = treesmapData[x][z] - 1 + + biome = slice.getBiome((x, y, z)) + block = slice.getBlock((x, y, z)) + maybeATree = slice.getBlock((x, yTree, z)) + + if maybeATree.id in lookup.TREES: + treesmap.putpixel((x, z), (yTree, yTree, yTree)) + + if block.id not in lookup.TREES: + heightmap.putpixel((x, z), (y, y, y)) + else: + height = 0 + number = 0 + for i in range(-1, 2): + for j in range(-1, 2): + if i != 0 or j != 0: + if (0 <= x + i < xzDistance[0]) and (0 <= z + j < xzDistance[1]): + k = heightmapData[x + i][z + j] - 1 + + # print('getData for tree', xzStart[0] + x + i, k, xzStart[1] + z + j) + + blockNeighbor = slice.getBlock((x + i, k, z + j)) + if blockNeighbor.id not in lookup.TREES: + height += k + number += 1 + if number != 0: + average = round(height / number) + # print(average, "average") + heightmap.putpixel((x, z), (average, average, average)) + + if (biome in waterBiomes) or (block.id in waterBlocks): + watermap.putpixel((x, z), 255) + else: + watermap.putpixel((x, z), 0) + + self.addBlocks([Block((xzStart[0] + x, 100, xzStart[1] + z), block)]) # y set to 100 for 2D + + return heightmap, watermap, treesmap + + def propagate(self, coordinates, scanned=[]): + i = 0 + editor = Editor(buffering=True) + if self.isInVolume(coordinates): + Block = self.getBlockFromCoordinates(coordinates) + self.getNeighbors(Block) + for neighbor in Block.neighbors: + if neighbor not in scanned: + scanned.append(neighbor) + self.getNeighbors(neighbor) + if neighbor.isSurface(): + self.propagate(neighbor.coordinates, scanned) + + def volumeTo3DBinaryImage(self): + binaryImage = [] + for x in range(self.length_x): + binaryImage.append([]) + for y in range(self.length_y): + binaryImage[x].append([]) + for z in range(self.length_z): + if self.volume[x][y][z] != None: + binaryImage[x][y].append(True) + else: + binaryImage[x][y].append(False) + + return np.array(binaryImage) + + def maskVolume(self, mask): + """ + + Delete unusable area of the volume to not let it be use by the skeletonize, based on a filtered image that act as a mask. + + Args: + mask (image): white or black image : combined watermap smoothed and sobel smoothed. + """ + editor = Editor() + buildArea = editor.getBuildArea() + buildRect = buildArea.toRect() + + xzStart = buildRect.begin + xzDistance = (max(buildRect.end[0], buildRect.begin[0]) - min(buildRect.end[0], buildRect.begin[0]), + max(buildRect.end[1], buildRect.begin[1]) - min(buildRect.end[1], buildRect.begin[1])) + + mask = Image.open(mask) + + slice = editor.loadWorldSlice(buildRect) + + heightmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING_NO_LEAVES"], dtype=np.uint8)) + + for x in range(0, xzDistance[0]): + for z in range(0, xzDistance[1]): + y = heightmapData[x][z] - 1 + if mask.getpixel((x, z)) == 255: + self.removeBlock((x, 100, z)) # y set to 100 for 2D + + def simplifyVolume(self): + array = self.volumeTo3DBinaryImage() + # array = ndimage.binary_dilation(array, iterations=15) + + return array + + +if __name__ == "__main__": + w = World() + w.getData() \ No newline at end of file diff --git a/world_maker/data/.gitkeep b/world_maker/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/world_maker/data/building.png b/world_maker/data/building.png new file mode 100644 index 0000000..48e51dc Binary files /dev/null and b/world_maker/data/building.png differ diff --git a/world_maker/data/district.png b/world_maker/data/district.png new file mode 100644 index 0000000..fc00abf Binary files /dev/null and b/world_maker/data/district.png differ diff --git a/world_maker/data/heightmap.png b/world_maker/data/heightmap.png new file mode 100644 index 0000000..4ec6160 Binary files /dev/null and b/world_maker/data/heightmap.png differ diff --git a/world_maker/data/highwaymap.png b/world_maker/data/highwaymap.png new file mode 100644 index 0000000..0a5673e Binary files /dev/null and b/world_maker/data/highwaymap.png differ diff --git a/world_maker/data/roadmap.png b/world_maker/data/roadmap.png new file mode 100644 index 0000000..841ca64 Binary files /dev/null and b/world_maker/data/roadmap.png differ diff --git a/world_maker/data/roadmap2.png b/world_maker/data/roadmap2.png new file mode 100644 index 0000000..c084d6e Binary files /dev/null and b/world_maker/data/roadmap2.png differ diff --git a/world_maker/data/skeleton_highway.png b/world_maker/data/skeleton_highway.png new file mode 100644 index 0000000..2ecb893 Binary files /dev/null and b/world_maker/data/skeleton_highway.png differ diff --git a/world_maker/data/smooth_sobel_watermap.png b/world_maker/data/smooth_sobel_watermap.png new file mode 100644 index 0000000..20ff75c Binary files /dev/null and b/world_maker/data/smooth_sobel_watermap.png differ diff --git a/world_maker/data/sobelmap.png b/world_maker/data/sobelmap.png new file mode 100644 index 0000000..c608899 Binary files /dev/null and b/world_maker/data/sobelmap.png differ diff --git a/world_maker/data/treemap.png b/world_maker/data/treemap.png new file mode 100644 index 0000000..bf7f01f Binary files /dev/null and b/world_maker/data/treemap.png differ diff --git a/world_maker/data/watermap.png b/world_maker/data/watermap.png new file mode 100644 index 0000000..c900dbb Binary files /dev/null and b/world_maker/data/watermap.png differ diff --git a/world_maker/data_analysis.py b/world_maker/data_analysis.py new file mode 100644 index 0000000..56e061f --- /dev/null +++ b/world_maker/data_analysis.py @@ -0,0 +1,234 @@ +import World +from PIL import Image, ImageFilter +import numpy as np +from scipy import ndimage +from Skeleton import Skeleton +from typing import Union + + +def get_data(world: World): + print("[Data Analysis] Generating data...") + heightmap, watermap, treemap = world.getData() + heightmap.save('./data/heightmap.png') + watermap.save('./data/watermap.png') + treemap.save('./data/treemap.png') + print("[Data Analysis] Data generated.") + return heightmap, watermap, treemap + + +def handle_import_image(image: Union[str, Image]) -> Image: + if isinstance(image, str): + return Image.open(image) + return image + + +def filter_negative(image: Image) -> Image: + """ + Invert the colors of an image. + + Args: + image (image): image to filter + """ + return Image.fromarray(np.invert(np.array(image))) + + +def filter_sobel(image: Union[str, Image]) -> Image: + """ + Edge detection algorithms from an image. + + Args: + image (image): image to filter + """ + + # Open the image + image = handle_import_image(image).convert('RGB') + + img = np.array(image).astype(np.uint8) + + # Apply gray scale + gray_img = np.round( + 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2] + ).astype(np.uint8) + + # Sobel Operator + h, w = gray_img.shape + # define filters + horizontal = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) # s2 + vertical = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]) # s1 + + # define images with 0s + newhorizontalImage = np.zeros((h, w)) + newverticalImage = np.zeros((h, w)) + newgradientImage = np.zeros((h, w)) + + # offset by 1 + for i in range(1, h - 1): + for j in range(1, w - 1): + horizontalGrad = ( + (horizontal[0, 0] * gray_img[i - 1, j - 1]) + + (horizontal[0, 1] * gray_img[i - 1, j]) + + (horizontal[0, 2] * gray_img[i - 1, j + 1]) + + (horizontal[1, 0] * gray_img[i, j - 1]) + + (horizontal[1, 1] * gray_img[i, j]) + + (horizontal[1, 2] * gray_img[i, j + 1]) + + (horizontal[2, 0] * gray_img[i + 1, j - 1]) + + (horizontal[2, 1] * gray_img[i + 1, j]) + + (horizontal[2, 2] * gray_img[i + 1, j + 1]) + ) + + newhorizontalImage[i - 1, j - 1] = abs(horizontalGrad) + + verticalGrad = ( + (vertical[0, 0] * gray_img[i - 1, j - 1]) + + (vertical[0, 1] * gray_img[i - 1, j]) + + (vertical[0, 2] * gray_img[i - 1, j + 1]) + + (vertical[1, 0] * gray_img[i, j - 1]) + + (vertical[1, 1] * gray_img[i, j]) + + (vertical[1, 2] * gray_img[i, j + 1]) + + (vertical[2, 0] * gray_img[i + 1, j - 1]) + + (vertical[2, 1] * gray_img[i + 1, j]) + + (vertical[2, 2] * gray_img[i + 1, j + 1]) + ) + + newverticalImage[i - 1, j - 1] = abs(verticalGrad) + + # Edge Magnitude + mag = np.sqrt(pow(horizontalGrad, 2.0) + pow(verticalGrad, 2.0)) + newgradientImage[i - 1, j - 1] = mag + + image = Image.fromarray(newgradientImage) + image = image.convert("L") + + return image + + +def filter_smooth(image: Union[str, Image], radius: int = 3): + """ + :param image: white and black image representing the derivative of the terrain (sobel), where black is flat and white is very steep. + :param radius: Radius of the Gaussian blur. + + Returns: + image: black or white image, with black as flat areas to be skeletonized + """ + + image = handle_import_image(image) + + # image = image.filter(ImageFilter.SMOOTH_MORE) + # image = image.filter(ImageFilter.SMOOTH_MORE) + # image = image.filter(ImageFilter.SMOOTH_MORE) + image = image.convert('L') + image = image.filter(ImageFilter.GaussianBlur(radius)) + array = np.array(image) + + bool_array = array > 7 + + # bool_array = ndimage.binary_opening(bool_array, structure=np.ones((3,3)), iterations=1) + # bool_array = ndimage.binary_closing(bool_array, structure=np.ones((3,3)), iterations=1) + # bool_array = ndimage.binary_opening(bool_array, structure=np.ones((5,5)), iterations=1) + # bool_array = ndimage.binary_closing(bool_array, structure=np.ones((5,5)), iterations=1) + # bool_array = ndimage.binary_opening(bool_array, structure=np.ones((7,7)), iterations=1) + # bool_array = ndimage.binary_closing(bool_array, structure=np.ones((7,7)), iterations=1) + + return Image.fromarray(bool_array) + + +def subtract_map(image: Union[str, Image], substractImage: Union[str, Image]) -> Image: + image = handle_import_image(image) + substractImage = handle_import_image(substractImage).convert('L') + + array_heightmap = np.array(image) + array_substractImage = np.array(substractImage) + + mask = array_substractImage == 255 + array_heightmap[mask] = 0 + + return Image.fromarray(array_heightmap) + + +def group_map(image1: Union[str, Image], image2: Union[str, Image]) -> Image: + image1 = handle_import_image(image1).convert('L') + image2 = handle_import_image(image2).convert('L') + + array1 = np.array(image1) + array2 = np.array(image2) + + mask = array1 == 255 + array2[mask] = 255 + + return Image.fromarray(array2) + + +def filter_smooth_array(array: np.ndarray, radius: int = 3) -> np.ndarray: + image = Image.fromarray(array) + smooth_image = filter_smooth(image, radius) + array = np.array(smooth_image) + return array + + +def filter_remove_details(image: Union[str, Image], n: int = 20) -> Image: + image = handle_import_image(image) + array = np.array(image) + for _ in range(n): + array = ndimage.binary_dilation(array, iterations=4) + array = ndimage.binary_erosion(array, iterations=5) + array = filter_smooth_array(array, 2) + array = ndimage.binary_erosion(array, iterations=3) + image = Image.fromarray(array) + return image + + +def highway_map() -> Image: + print("[Data Analysis] Generating highway map...") + smooth_sobel = filter_smooth("./data/sobelmap.png", 1) + negative_smooth_sobel = filter_negative(smooth_sobel) + negative_smooth_sobel_water = subtract_map(negative_smooth_sobel, './data/watermap.png') + array_sobel_water = np.array(negative_smooth_sobel_water) + array_sobel_water = ndimage.binary_erosion(array_sobel_water, iterations=12) + array_sobel_water = ndimage.binary_dilation(array_sobel_water, iterations=5) + array_sobel_water = filter_smooth_array(array_sobel_water, 5) + array_sobel_water = ndimage.binary_erosion(array_sobel_water, iterations=20) + array_sobel_water = filter_smooth_array(array_sobel_water, 6) + image = Image.fromarray(array_sobel_water) + image_no_details = filter_remove_details(image, 15) + image_no_details.save('./data/highwaymap.png') + print("[Data Analysis] Highway map generated.") + return image_no_details + + +def create_volume(surface: np.ndarray, heightmap: np.ndarray, make_it_flat: bool = False) -> np.ndarray: + volume = np.full((len(surface), 255, len(surface[0])), False) + for z in range(len(surface)): + for x in range(len(surface[0])): + if not make_it_flat: + volume[x][heightmap[z][x]][z] = surface[z][x] + else: + volume[x][0][z] = surface[z][x] + return volume + + +def convert_2D_to_3D(image: Union[str, Image], make_it_flat: bool = False) -> np.ndarray: + image = handle_import_image(image) + heightmap = Image.open('./data/heightmap.png').convert('L') + heightmap = np.array(heightmap) + surface = np.array(image) + volume = create_volume(surface, heightmap, make_it_flat) + return volume + + +def skeleton_highway_map(image: Union[str, Image] = './data/highwaymap.png'): + image_array = convert_2D_to_3D(image, True) + skeleton = Skeleton(image_array) + skeleton.parse_graph(True) + heightmap_skeleton = skeleton.map() + heightmap_skeleton.save('./data/skeleton_highway.png') + + +def smooth_sobel_water() -> Image: + watermap = handle_import_image("./data/watermap.png") + watermap = filter_negative(filter_remove_details(filter_negative(watermap), 5)) + sobel = handle_import_image("./data/sobelmap.png") + sobel = filter_remove_details(filter_smooth(sobel, 1), 2) + group = group_map(watermap, sobel) + group = filter_negative(group) + group.save('./data/smooth_sobel_watermap.png') + return group diff --git a/world_maker/pack_rectangle.py b/world_maker/pack_rectangle.py new file mode 100644 index 0000000..879ae8c --- /dev/null +++ b/world_maker/pack_rectangle.py @@ -0,0 +1,115 @@ +from PIL import Image +import numpy as np +from typing import Union +from data_analysis import handle_import_image + +class Rectangle: + def __init__(self, width, height): + self.width = width + self.height = height + + +class Bin: + def __init__(self, grid): + self.grid = grid + self.rectangles = [] + + def place_rectangle(self, rectangle): + best_spot = None + best_spot_empty_area = float('inf') + + for i in range(len(self.grid[0]) - rectangle.width + 1): # Swap usage of x and y + for j in range(len(self.grid) - rectangle.height + 1): + if self.can_place(rectangle, i, j): + empty_area = self.calculate_empty_area(rectangle, i, j) + if empty_area < best_spot_empty_area: + best_spot = (i, j) + best_spot_empty_area = empty_area + + if best_spot is not None: + self.rectangles.append((best_spot, (best_spot[0]+rectangle.width, best_spot[1]+rectangle.height))) + self.update_grid(rectangle, *best_spot) + return True + + return False + + def calculate_empty_area(self, rectangle, x, y): + empty_area = 0 + for rect_x in range(x, x + rectangle.width): + for rect_y in range(y, y + rectangle.height): + if self.grid[rect_y][rect_x]: # Swap usage of x and y + empty_area += 1 + return empty_area + + def can_place(self, rectangle, x, y): + for rect_x in range(x, x + rectangle.width): + for rect_y in range(y, y + rectangle.height): + if not self.grid[rect_y][rect_x]: # Swap usage of x and y + return False + return True + + def update_grid(self, rectangle, x, y): + for rect_x in range(x, x + rectangle.width): + for rect_y in range(y, y + rectangle.height): + self.grid[rect_y][rect_x] = False # Swap usage of x and y + + +def pack_rectangles(rectangles, grid): + rectangles = sorted(rectangles, key=lambda r: r.width * r.height, reverse=True) + bins = [Bin(grid)] + + for rectangle in rectangles: + for bin in bins: + if bin.place_rectangle(rectangle): + break + else: # No break, meaning rectangle couldn't be placed in any bin + new_bin = Bin(grid) + if new_bin.place_rectangle(rectangle): + bins.append(new_bin) + else: + return False # If a rectangle can't be placed even in a new bin, return False + + return True # If all rectangles can be placed, return True + + +import random + + +def generate_rectangle(max_width, max_height): + width = random.randint(6, 20) + height = random.randint(6, 20) + return Rectangle(width, height) + + +def pack_rectangles(grid): + max_width = len(grid[0]) + max_height = len(grid) + bin = Bin(grid) + + while True: + rectangle = generate_rectangle(max_width // 2, max_height // 2) + if not bin.place_rectangle(rectangle): + break # Stop when a rectangle can't be placed + print(len(bin.rectangles)) + return bin.rectangles # Return the list of rectangles that were placed + + +def draw_rectangles(rectangles, grid): + image = Image.new('RGB', (len(grid[0]), len(grid)), (0, 0, 0)) + for rectangle in rectangles: + start, end = rectangle + for x in range(start[0], end[0]): + for y in range(start[1], end[1]): + image.putpixel((x, y), (144, 255, 144)) + return image + + +def generate_building(image: Union[str, Image] = './data/roadmap2.png'): + image = handle_import_image(image).convert('L') + grid = np.array(image) + rectangles = pack_rectangles(grid) + draw_rectangles(rectangles, grid).save('./data/building.png') + return rectangles + +if __name__ == '__main__': + generate_building() diff --git a/world_maker/world_maker.py b/world_maker/world_maker.py new file mode 100644 index 0000000..c86e021 --- /dev/null +++ b/world_maker/world_maker.py @@ -0,0 +1,22 @@ +import World +from PIL import Image +from data_analysis import get_data, highway_map, filter_sobel, skeleton_highway_map, smooth_sobel_water, subtract_map +from City import City +from Position import Position +from random import randint + +if __name__ == '__main__': + #world = World.World() + #heightmap, watermap, treemap = get_data(world) + #filter_sobel("./data/heightmap.png").save('./data/sobelmap.png') + smooth_sobel_water = smooth_sobel_water() + #skeleton_highway_map(highway_map()) + city = City() + for i in range(10): + city.add_district(Position(randint(0, 400), randint(0, 400))) + city.loop_expend_district() + city.district_draw_map() + city.district_generate_road() + road = city.draw_roads(Image.new('RGB', (401, 401)), 4) + road.save('./data/roadmap.png') + subtract_map(smooth_sobel_water, road).save('./data/roadmap2.png')