Merge branch 'main' of https://github.com/Eclairsombre/GDMC-2024
2
.gitignore
vendored
@@ -157,4 +157,4 @@ cython_debug/
|
|||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# 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
|
# 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.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
.idea/
|
||||||
|
|||||||
43
buildings/Building.py
Normal file
@@ -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))
|
||||||
|
|
||||||
114
buildings/Entrance.py
Normal file
@@ -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
|
||||||
|
|
||||||
89
buildings/Facade.py
Normal file
@@ -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]))
|
||||||
|
|
||||||
150
buildings/Foundations.py
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
15
buildings/Roof.py
Normal file
@@ -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"]
|
||||||
20
buildings/TODO
Normal file
@@ -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)
|
||||||
135
buildings/elements/Balcony.py
Normal file
@@ -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"])
|
||||||
14
buildings/elements/Collumn.py
Normal file
@@ -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"
|
||||||
|
|
||||||
19
buildings/elements/FacadeDetails.py
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
3
buildings/elements/FacadedetailsElt/Buttons.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class Buttons:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
0
buildings/elements/FacadedetailsElt/InterFloor.py
Normal file
0
buildings/elements/FacadedetailsElt/Moldings.py
Normal file
0
buildings/elements/FacadedetailsElt/Pillar.py
Normal file
150
buildings/elements/Window.py
Normal file
@@ -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)
|
||||||
22
buildings/elements/WindowElt/Glass.py
Normal file
@@ -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
|
||||||
|
|
||||||
20
buildings/geometry/Point.py
Normal file
@@ -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)
|
||||||
107
buildings/geometry/Polygon.py
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
23
buildings/geometry/Rectangle.py
Normal file
@@ -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)}"
|
||||||
94
buildings/geometry/Tile.py
Normal file
@@ -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
|
||||||
24
buildings/geometry/Vertice.py
Normal file
@@ -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"
|
||||||
|
|
||||||
59
buildings/shapes.json
Normal file
@@ -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]]
|
||||||
|
}
|
||||||
|
]
|
||||||
45
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 networks.curve as curve
|
||||||
import numpy as np
|
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)
|
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
|
# # Get a block
|
||||||
block = editor.getBlock((0,48,0))
|
block = editor.getBlock((0,48,0))
|
||||||
|
|
||||||
# # Place a block
|
|
||||||
editor.placeBlock((-5, -58, 0), Block("stone"))
|
|
||||||
|
|
||||||
# # Build a cube
|
# # Build a cube
|
||||||
# geometry.placeCuboid(editor, (458, 92, 488), (468, 99, 471), Block("oak_planks"))
|
# geometry.placeCuboid(editor, (458, 92, 488), (468, 99, 471), Block("oak_planks"))
|
||||||
|
|
||||||
curve = curve.Curve([(396, 132, 740), (435, 138, 730),
|
# curve = curve.Curve([(396, 132, 740), (435, 138, 730),
|
||||||
(443, 161, 758), (417, 73, 729)])
|
# (443, 161, 758), (417, 73, 729)])
|
||||||
curve.compute_curve()
|
# curve.compute_curve()
|
||||||
|
|
||||||
#for point in curve.computed_points:
|
|
||||||
# print(point)
|
|
||||||
# editor.placeBlock(point, Block("stone"))
|
|
||||||
92
metro/Metro_Line.py
Normal file
@@ -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"]
|
||||||
404
metro/metro_line_map.py
Normal file
@@ -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()
|
||||||
91
params.yml
Normal file
@@ -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
|
||||||
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
gdpc==7.1.0
|
gdpc==7.1.0
|
||||||
matplotlib==3.8.2
|
|
||||||
numpy==1.26.4
|
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
|
||||||
|
|||||||
28
utils/Enums.py
Normal file
@@ -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
|
||||||
11
utils/JsonReader.py
Normal file
@@ -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
|
||||||
11
utils/YamlReader.py
Normal file
@@ -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
|
||||||
6
utils/functions.py
Normal file
@@ -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()]
|
||||||
34
world_maker/Block.py
Normal file
@@ -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
|
||||||
153
world_maker/City.py
Normal file
@@ -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')
|
||||||
235
world_maker/District.py
Normal file
@@ -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
|
||||||
37
world_maker/Position.py
Normal file
@@ -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)
|
||||||
215
world_maker/Skeleton.py
Normal file
@@ -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
|
||||||
233
world_maker/World.py
Normal file
@@ -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()
|
||||||
0
world_maker/data/.gitkeep
Normal file
BIN
world_maker/data/building.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
world_maker/data/district.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
world_maker/data/heightmap.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
world_maker/data/highwaymap.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
world_maker/data/roadmap.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
world_maker/data/roadmap2.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
world_maker/data/skeleton_highway.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
world_maker/data/smooth_sobel_watermap.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
world_maker/data/sobelmap.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
world_maker/data/treemap.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
world_maker/data/watermap.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
234
world_maker/data_analysis.py
Normal file
@@ -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
|
||||||
115
world_maker/pack_rectangle.py
Normal file
@@ -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()
|
||||||
22
world_maker/world_maker.py
Normal file
@@ -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')
|
||||||