Merge branch 'main' into main

This commit is contained in:
Xeon0X
2024-06-15 15:05:47 +02:00
committed by GitHub
52 changed files with 3063 additions and 14 deletions

2
.gitignore vendored
View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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)

View 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"])

View 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"

View 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

View File

@@ -0,0 +1,3 @@
class Buttons:
def __init__(self):
pass

View 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)

View 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

View 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)

View 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

View 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)}"

View 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

View 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
View 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
View File

@@ -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
View 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
View 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
View 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

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View 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

View 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()

View 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')