Merge pull request #2 from NichiHachi/main
Custom district generator and non-functional Metro (sadge)
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -157,4 +157,4 @@ cython_debug/
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
.idea/
|
||||
|
||||
92
metro/Metro_Line.py
Normal file
92
metro/Metro_Line.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from math import sqrt, atan2
|
||||
|
||||
|
||||
class Position:
|
||||
def __init__(self, x: int = 0, y: int = 0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __add__(self, other: "Position") -> "Position":
|
||||
return Position(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other: "Position") -> "Position":
|
||||
return Position(self.x - other.x, self.y - other.y)
|
||||
|
||||
def __mul__(self, other: float) -> "Position":
|
||||
return Position(int(self.x * other), int(self.y * other))
|
||||
|
||||
def __truediv__(self, other: float) -> "Position":
|
||||
return Position(int(self.x / other), int(self.y / other))
|
||||
|
||||
def __str__(self):
|
||||
return f"({self.x}, {self.y})"
|
||||
|
||||
def __eq__(self, other: "Position"):
|
||||
return self.x == other.x and self.y == other.y
|
||||
|
||||
def distance_to(self, other: "Position") -> float:
|
||||
return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
|
||||
|
||||
def norm(self) -> float:
|
||||
return sqrt(self.x ** 2 + self.y ** 2)
|
||||
|
||||
def angle_to(self, other: "Position") -> float:
|
||||
return atan2(self.y - other.y, other.x - self.x)
|
||||
|
||||
|
||||
class Station:
|
||||
"""
|
||||
This class represents the position and link of a metro station.
|
||||
"""
|
||||
|
||||
def __init__(self, pos: Position, orientation: float, name: str = "Station"):
|
||||
"""
|
||||
Constructor of Station.
|
||||
|
||||
:param pos: Position x and y of the station
|
||||
:param orientation: The orientation of the station in radian (The angle is where the station is facing next)
|
||||
:param name: The name of the station
|
||||
"""
|
||||
self.name = name
|
||||
self.orientation = orientation
|
||||
self.pos = pos
|
||||
self.last_station = None
|
||||
self.next_station = None
|
||||
|
||||
def distance_to(self, station: "Station") -> float:
|
||||
"""
|
||||
Calculate the distance between two stations.
|
||||
|
||||
:param station: The station to calculate the distance to
|
||||
:return: The distance between two stations
|
||||
"""
|
||||
return self.pos.distance_to(station.pos)
|
||||
|
||||
|
||||
class Metro_Line:
|
||||
"""
|
||||
This class represents the metro line.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "Metro line A"):
|
||||
"""
|
||||
Constructor of Metro_Line.
|
||||
|
||||
:param name: The name of the metro line
|
||||
"""
|
||||
self.name = name
|
||||
self.stations = []
|
||||
|
||||
def add_station(self, station: Station):
|
||||
"""
|
||||
Add a station to the metro map.
|
||||
|
||||
:param station: The station to be added
|
||||
"""
|
||||
self.stations.append(station)
|
||||
if len(self.stations) > 1:
|
||||
self.stations[-2].next_station = station
|
||||
station.last_station = self.stations[-2]
|
||||
|
||||
|
||||
__all__ = ["Metro_Line", "Station", "Position"]
|
||||
404
metro/metro_line_map.py
Normal file
404
metro/metro_line_map.py
Normal file
@@ -0,0 +1,404 @@
|
||||
from Metro_Line import *
|
||||
from math import pi, cos, sin, sqrt, atan2, inf
|
||||
from pygame import Surface
|
||||
import pygame
|
||||
|
||||
from metro.Metro_Line import Position
|
||||
|
||||
|
||||
def cubic_bezier(time: float, join1: Position, control_point1: Position, control_point2: Position,
|
||||
join2: Position) -> Position:
|
||||
"""
|
||||
Calculate the position of a point on a cubic Bézier curve at a given time
|
||||
|
||||
Formula used : B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1 + 3(1-t) * t^2 * P2 + t^3 * P3
|
||||
|
||||
:param time: The time at which to calculate the position
|
||||
:param join1: The first join point of the curve
|
||||
:param control_point1: The first control point of the curve
|
||||
:param control_point2: The second control point of the curve
|
||||
:param join2: The second join point of the curve
|
||||
:return: The position of the point on the curve at the given time
|
||||
"""
|
||||
return (join1 * ((1 - time) ** 3)
|
||||
+ control_point1 * 3 * ((1 - time) ** 2) * time
|
||||
+ control_point2 * 3 * (1 - time) * (time ** 2)
|
||||
+ join2 * (time ** 3))
|
||||
|
||||
|
||||
def cubic_bezier_derivative(time: float, join1: Position, control_point1: Position, control_point2: Position,
|
||||
join2: Position) -> Position:
|
||||
"""
|
||||
Calculate the first derivative of a cubic Bézier curve at a given time
|
||||
|
||||
Formula used : B'(t) = 3(1-t)^2 * (P1 - P0) + 6(1-t) * t * (P2 - P1) + 3t^2 * (P3 - P2)
|
||||
|
||||
:param time: The time at which to calculate the derivative
|
||||
:param join1: The first join point of the curve
|
||||
:param control_point1: The first control point of the curve
|
||||
:param control_point2: The second control point of the curve
|
||||
:param join2: The second join point of the curve
|
||||
:return: The derivative of the curve at the given time
|
||||
"""
|
||||
return ((control_point1 - join1) * 3 * ((1 - time) ** 2)
|
||||
+ (control_point2 - control_point1) * 6 * (1 - time) * time
|
||||
+ (join2 - control_point2) * 3 * (time ** 2))
|
||||
|
||||
|
||||
def cubic_bezier_second_derivative(time: float, join1: Position, control_point1: Position, control_point2: Position,
|
||||
join2: Position) -> Position:
|
||||
"""
|
||||
Calculate the second derivative of a cubic Bézier curve at a given time
|
||||
|
||||
Formula used : B''(t) = 6(1-t) * (P2 - 2P1 + P0) + 6t * (P3 - 2P2 + P1)
|
||||
|
||||
:param time: The time at which to calculate the second derivative
|
||||
:param join1: The first join point of the curve
|
||||
:param control_point1: The first control point of the curve
|
||||
:param control_point2: The second control point of the curve
|
||||
:param join2: The second join point of the curve
|
||||
:return: The second derivative of the curve at the given time
|
||||
"""
|
||||
return ((control_point2 - control_point1 * 2 + join1) * 6 * (1 - time)
|
||||
+ (join2 - control_point2 * 2 + control_point1) * 6 * time)
|
||||
|
||||
|
||||
def bezier_curve(control_points, num_points) -> tuple[list[Position], list[Position], list[Position]]:
|
||||
"""
|
||||
Generate a Bézier curve from a list of control points
|
||||
|
||||
:param control_points: The control points of the curve
|
||||
:param num_points: The number of points to generate
|
||||
:return: A tuple containing the points of the curve, the derivative of the curve,
|
||||
and the second derivative of the curve
|
||||
"""
|
||||
points = []
|
||||
derivative = []
|
||||
second_derivative = []
|
||||
for t in range(num_points + 1):
|
||||
points.append(cubic_bezier(t / num_points, *control_points))
|
||||
derivative.append(cubic_bezier_derivative(t / num_points, *control_points))
|
||||
second_derivative.append(cubic_bezier_second_derivative(t / num_points, *control_points))
|
||||
return points, derivative, second_derivative
|
||||
|
||||
|
||||
def osculating_circle(points: list[Position], derivative: list[Position], second_derivative: list[Position]) \
|
||||
-> list[tuple[int, Position]]:
|
||||
"""
|
||||
Calculate the osculating circle at each point of a curve
|
||||
An osculating circle is the circle that best approximates the curve at a given point
|
||||
|
||||
Source : https://en.wikipedia.org/wiki/Osculating_circle
|
||||
|
||||
:param points: The points of the curve
|
||||
:param derivative: The derivative of the curve
|
||||
:param second_derivative: The second derivative of the curve
|
||||
:return: A list of tuples, each containing the radius and center of each osculating circle
|
||||
"""
|
||||
circle = []
|
||||
for i in range(len(points)):
|
||||
curvature = (abs(derivative[i].x * second_derivative[i].y - derivative[i].y * second_derivative[i].x)
|
||||
/ ((derivative[i].x ** 2 + derivative[i].y ** 2) ** 1.5))
|
||||
if curvature != 0:
|
||||
radius = 1 / curvature
|
||||
normal = derivative[i].norm()
|
||||
cross_product = derivative[i].x * second_derivative[i].y - derivative[i].y * second_derivative[i].x
|
||||
if cross_product > 0:
|
||||
center = points[i] + Position(-derivative[i].y * radius / normal, derivative[i].x * radius / normal)
|
||||
else:
|
||||
center = points[i] + Position(derivative[i].y * radius / normal, -derivative[i].x * radius / normal)
|
||||
circle.append((int(radius), center))
|
||||
return circle
|
||||
|
||||
|
||||
def merge_similar_circles(circles: list[tuple[int, Position]], radius_threshold: float, center_threshold: float) \
|
||||
-> list[tuple[int, Position]]:
|
||||
"""
|
||||
Merge similar osculating circles
|
||||
|
||||
:param circles: The osculating circles to merge
|
||||
:param radius_threshold: The maximum difference in radius for two circles to be considered similar
|
||||
:param center_threshold: The maximum distance between the centers of two circles to be considered similar
|
||||
:return: The merged osculating circles
|
||||
"""
|
||||
merged_circles = []
|
||||
i = 0
|
||||
while i < len(circles) - 1:
|
||||
radius1, center1 = circles[i]
|
||||
radius2, center2 = circles[i + 1]
|
||||
if abs(radius1 - radius2) <= radius_threshold and center1.distance_to(center2) <= center_threshold:
|
||||
merged_radius = (radius1 + radius2) // 2
|
||||
merged_center = Position((center1.x + center2.x) // 2, (center1.y + center2.y) // 2)
|
||||
merged_circles.append((merged_radius, merged_center))
|
||||
i += 2
|
||||
else:
|
||||
merged_circles.append(circles[i])
|
||||
i += 1
|
||||
if i < len(circles):
|
||||
merged_circles.append(circles[i])
|
||||
|
||||
if len(merged_circles) == len(circles):
|
||||
return merged_circles
|
||||
else:
|
||||
return merge_similar_circles(merged_circles, radius_threshold, center_threshold)
|
||||
|
||||
|
||||
def circle_intersection(circle1: tuple[int, Position], circle2: tuple[int, Position]) -> list[Position]:
|
||||
distance = circle1[1].distance_to(circle2[1])
|
||||
|
||||
if (distance > circle1[0] + circle2[0] or distance < abs(circle1[0] - circle2[0])
|
||||
or (distance == 0 and circle1[0] == circle2[0])):
|
||||
return []
|
||||
|
||||
distance_line_circle = (circle1[0] ** 2 - circle2[0] ** 2 + distance ** 2) / (2 * distance)
|
||||
distance_line_intersec_point = sqrt(circle1[0] ** 2 - distance_line_circle ** 2)
|
||||
p = circle1[1] + (circle2[1] - circle1[1]) * distance_line_circle / distance
|
||||
|
||||
return [Position(int(p.x + distance_line_intersec_point * (circle2[1].y - circle1[1].y) / distance),
|
||||
int(p.y - distance_line_intersec_point * (circle2[1].x - circle1[1].x) / distance)),
|
||||
Position(int(p.x - distance_line_intersec_point * (circle2[1].y - circle1[1].y) / distance),
|
||||
int(p.y + distance_line_intersec_point * (circle2[1].x - circle1[1].x) / distance))]
|
||||
|
||||
|
||||
def closest_to_curve(points: list[Position], curve_points: list[Position]) -> Position:
|
||||
closest_point = Position()
|
||||
distance = inf
|
||||
for point in points:
|
||||
for curve_point in curve_points:
|
||||
distance_point_curve = point.distance_to(curve_point)
|
||||
if distance_point_curve < distance:
|
||||
distance = distance_point_curve
|
||||
closest_point = point
|
||||
return closest_point
|
||||
|
||||
|
||||
def midpoint_circle_segment(circle: tuple[int, Position], start_point: Position, end_point: Position, curve_points: list[Position]) -> list[
|
||||
Position]:
|
||||
points = []
|
||||
|
||||
start_angle = circle[1].angle_to(start_point)
|
||||
end_angle = circle[1].angle_to(end_point)
|
||||
|
||||
if start_angle < 0:
|
||||
start_angle += 2 * pi
|
||||
if end_angle < 0:
|
||||
end_angle += 2 * pi
|
||||
if start_angle > end_angle:
|
||||
start_angle, end_angle = end_angle, start_angle
|
||||
|
||||
middle_angle = (start_angle+end_angle)/2
|
||||
middle_point = circle[1] + Position(int(circle[0]*cos(middle_angle)), -int(circle[0]*sin(middle_angle)))
|
||||
is_outside_point = closest_to_curve([middle_point, circle[1]], curve_points) == middle_point
|
||||
|
||||
x0, y0 = circle[1].x, circle[1].y
|
||||
x = circle[0]
|
||||
y = 0
|
||||
err = 0
|
||||
|
||||
while x >= y:
|
||||
for (x1, y1) in [(x0 + x, y0 + y), (x0 + y, y0 + x), (x0 - y, y0 + x), (x0 - x, y0 + y),
|
||||
(x0 - x, y0 - y), (x0 - y, y0 - x), (x0 + y, y0 - x), (x0 + x, y0 - y)]:
|
||||
angle = atan2(y0 - y1, x1 - x0)
|
||||
if angle < 0:
|
||||
angle += 2*pi
|
||||
if is_outside_point:
|
||||
if start_angle <= angle <= end_angle:
|
||||
points.append(Position(int(x1), int(y1)))
|
||||
else:
|
||||
if angle <= start_angle or end_angle <= angle:
|
||||
points.append(Position(int(x1), int(y1)))
|
||||
|
||||
if err <= 0:
|
||||
y += 1
|
||||
err += 2 * y + 1
|
||||
if err > 0:
|
||||
x -= 1
|
||||
err -= 2 * x + 1
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def calculate_control_points(station, next_station, curve_factor) -> tuple[Position, Position]:
|
||||
"""
|
||||
Calculate the control points for a Bézier curve between two stations
|
||||
|
||||
:param station: The first station
|
||||
:param next_station: The second station
|
||||
:param curve_factor: The factor to multiply the distance between stations to create the control points
|
||||
:return: A tuple containing the control points for the curve
|
||||
"""
|
||||
distance = station.distance_to(next_station)
|
||||
|
||||
control_point_pos = station.pos + Position(cos(station.orientation) * distance * curve_factor,
|
||||
-sin(station.orientation) * distance * curve_factor)
|
||||
|
||||
control_point_next_pos = next_station.pos + Position(
|
||||
cos(next_station.orientation + pi) * distance * curve_factor,
|
||||
- sin(next_station.orientation + pi) * distance * curve_factor)
|
||||
|
||||
return control_point_pos, control_point_next_pos
|
||||
|
||||
|
||||
def metro_line_osculating_circles(metro: Metro_Line, curve_factor: float = 0.5, num_points_factor: float = 1 / 20) -> (
|
||||
tuple)[list[tuple[int, Position]], list[Position]]:
|
||||
"""
|
||||
Calculate the osculating circles of a metro line
|
||||
|
||||
:param metro: The metro line to calculate the osculating circles of
|
||||
:param curve_factor: How much the control points should be offset from the stations
|
||||
:param num_points_factor: How many points to generate for each segment of the curve
|
||||
:return: A tuple containing the osculating circles and the points of the metro line curve
|
||||
"""
|
||||
print(f"[METRO LINE] Calculating osculating circles of the metro line {metro.name}")
|
||||
circles = []
|
||||
points_list = []
|
||||
for i in range(len(metro.stations) - 1):
|
||||
print(f"[METRO LINE] Calculating between {metro.stations[i].name} and {metro.stations[i].next_station.name}")
|
||||
station = metro.stations[i]
|
||||
|
||||
distance = station.distance_to(station.next_station)
|
||||
|
||||
control_point_pos, control_point_next_pos = calculate_control_points(station, station.next_station,
|
||||
curve_factor)
|
||||
|
||||
points, derivatives, second_derivatives = bezier_curve(
|
||||
[station.pos, control_point_pos, control_point_next_pos, station.next_station.pos],
|
||||
int(distance * num_points_factor))
|
||||
|
||||
osculating_circles = osculating_circle(points, derivatives, second_derivatives)
|
||||
merged_circles = merge_similar_circles(osculating_circles, 50, 50)
|
||||
print(
|
||||
f"[METRO LINE] {len(osculating_circles) - len(merged_circles)} out of {len(osculating_circles)} circles deleted !")
|
||||
circles.extend(merged_circles)
|
||||
points_list.extend(points)
|
||||
print(f"[METRO LINE] Osculating circles done")
|
||||
return circles, points_list
|
||||
|
||||
|
||||
# --- DRAW PART ---
|
||||
|
||||
def draw_osculating_circle(circle: list[tuple[int, Position]], surface: Surface):
|
||||
"""
|
||||
:param circle: The osculating circles to draw
|
||||
:param surface: The surface on which to draw the circles
|
||||
"""
|
||||
for radius, center in circle:
|
||||
pygame.draw.circle(surface, (255, 0, 0), (center.x, center.y), int(radius), 1)
|
||||
pygame.draw.circle(surface, (0, 0, 255), (center.x, center.y), 10)
|
||||
|
||||
|
||||
def draw_station(station: Station, surface: Surface):
|
||||
"""
|
||||
:param station: The station to draw
|
||||
:param surface: The surface on which to draw the station
|
||||
"""
|
||||
pygame.draw.circle(surface, (255, 255, 255), (station.pos.x, station.pos.y), 10)
|
||||
|
||||
|
||||
def draw_points(points: list[Position], surface):
|
||||
"""
|
||||
:param points: The points to draw
|
||||
:param surface: The surface on which to draw the points
|
||||
"""
|
||||
for point in points:
|
||||
pygame.draw.circle(surface, (40, 255, 40), (point.x, point.y), 5)
|
||||
|
||||
|
||||
def draw_point(point: Position, surface):
|
||||
pygame.draw.circle(surface, (40, 255, 40), (point.x, point.y), 5)
|
||||
|
||||
|
||||
def draw_pixels(points: list[Position], surface):
|
||||
for point in points:
|
||||
surface.set_at((point.x, point.y), (0, 255, 255))
|
||||
|
||||
|
||||
def draw_metro_line(metro: Metro_Line, surface: Surface, show_points: bool = True):
|
||||
"""
|
||||
:param metro: The metro line to draw
|
||||
:param surface: The surface on which to draw the metro line
|
||||
:param show_points: Whether to show the points of the curve
|
||||
"""
|
||||
for i in range(len(metro.stations) - 1):
|
||||
station = metro.stations[i]
|
||||
draw_station(station, surface)
|
||||
draw_station(station.next_station, surface)
|
||||
|
||||
circles, points = metro_line_osculating_circles(metro)
|
||||
draw_osculating_circle(circles, surface)
|
||||
for i in range(1, len(circles) - 1):
|
||||
intersect_point_circle_before = closest_to_curve(circle_intersection(circles[i - 1], circles[i]), points)
|
||||
intersect_point_circle_after = closest_to_curve(circle_intersection(circles[i], circles[i + 1]), points)
|
||||
if intersect_point_circle_before == Position():
|
||||
continue
|
||||
intersect_point_circle_before = circles[i - 1][1]
|
||||
else:
|
||||
draw_point(intersect_point_circle_before, surface)
|
||||
|
||||
if intersect_point_circle_after == Position():
|
||||
continue
|
||||
intersect_point_circle_after = circles[i + 1][1]
|
||||
else:
|
||||
draw_point(intersect_point_circle_after, surface)
|
||||
|
||||
points_midpoint = midpoint_circle_segment(circles[i], intersect_point_circle_before,
|
||||
intersect_point_circle_after, points)
|
||||
draw_pixels(points_midpoint, surface)
|
||||
|
||||
if len(points) != 0:
|
||||
intersect_point_circle_before = points[0]
|
||||
intersect_point_circle_after = closest_to_curve(circle_intersection(circles[0], circles[1]), points)
|
||||
points_midpoint = midpoint_circle_segment(circles[0], intersect_point_circle_before,
|
||||
intersect_point_circle_after, points)
|
||||
draw_pixels(points_midpoint, surface)
|
||||
|
||||
intersect_point_circle_before = points[-1]
|
||||
intersect_point_circle_after = closest_to_curve(circle_intersection(circles[-1], circles[-2]), points)
|
||||
points_midpoint = midpoint_circle_segment(circles[-1], intersect_point_circle_before,
|
||||
intersect_point_circle_after, points)
|
||||
draw_pixels(points_midpoint, surface)
|
||||
|
||||
|
||||
def interface():
|
||||
"""
|
||||
Interface for creating a metro line
|
||||
|
||||
Control :
|
||||
|
||||
- Up arrow ↑ Create a station facing north
|
||||
|
||||
- Down arrow ↓ Create a station facing south
|
||||
|
||||
- Left arrow ← Create a station facing west
|
||||
|
||||
- Right arrow → Create a station facing east
|
||||
"""
|
||||
metro = Metro_Line('A')
|
||||
surface = pygame.display.set_mode((1000, 1000))
|
||||
while True:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
return
|
||||
|
||||
if event.type == pygame.KEYDOWN:
|
||||
angle = 0
|
||||
if event.key == pygame.K_UP:
|
||||
angle = pi / 2
|
||||
elif event.key == pygame.K_DOWN:
|
||||
angle = -pi / 2
|
||||
elif event.key == pygame.K_LEFT:
|
||||
angle = pi
|
||||
x, y = pygame.mouse.get_pos()
|
||||
metro.add_station(Station(Position(x, y), angle, str(len(metro.stations))))
|
||||
draw_metro_line(metro, surface)
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
|
||||
def main():
|
||||
interface()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
gdpc==7.1.0
|
||||
matplotlib==3.8.2
|
||||
numpy==1.26.4
|
||||
pygame==2.5.2
|
||||
scipy==1.13.0
|
||||
|
||||
34
world_maker/Block.py
Normal file
34
world_maker/Block.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from gdpc import Editor, Block, geometry
|
||||
from gdpc.lookup import *
|
||||
import numpy as np
|
||||
import math
|
||||
|
||||
SOLID_NATURAL_BLOCKS = SOILS | STONES | ORES | LIQUIDS
|
||||
|
||||
class Block:
|
||||
def __init__(self, coordinates:tuple, name:str):
|
||||
self.coordinates = coordinates
|
||||
self.name = name
|
||||
self.neighbors = []
|
||||
self.surface = None
|
||||
|
||||
|
||||
def addNeighbors(self, neighbors:list[Block]):
|
||||
for neighbor in neighbors:
|
||||
self.neighbors.append(neighbor)
|
||||
|
||||
def isSurface(self):
|
||||
if self.surface == None:
|
||||
if str(self.name) in SOLID_NATURAL_BLOCKS:
|
||||
for neighbor in self.neighbors:
|
||||
if str(neighbor.name) not in SOLID_NATURAL_BLOCKS:
|
||||
self.surface = True
|
||||
return True
|
||||
if len(self.neighbors) != 0:
|
||||
self.surface = False
|
||||
return False
|
||||
else:
|
||||
self.surface = False
|
||||
return False
|
||||
else:
|
||||
return self.surface
|
||||
126
world_maker/City.py
Normal file
126
world_maker/City.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from District import District, CustomDistrict, VoronoiDistrict
|
||||
from Position import Position
|
||||
from PIL import Image
|
||||
import random
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Add a new district to the city.
|
||||
|
||||
:param center: The center position of the new district.
|
||||
"""
|
||||
self.districts.append(CustomDistrict(len(self.districts) + 1, center))
|
||||
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 i in range(index_district + 1, len(self.districts)):
|
||||
if point in self.districts[i].area_expend:
|
||||
distance = point.distance_to(self.districts[i].center_expend)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
self.districts[index_district_chosen].area_expend.remove(point)
|
||||
index_district_chosen = i
|
||||
else:
|
||||
self.districts[i].area_expend.remove(point)
|
||||
self.districts[index_district_chosen].area.append(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.
|
||||
"""
|
||||
loop_count = 0
|
||||
while not self.is_expend_finished():
|
||||
self.update_expend_district()
|
||||
loop_count += 1
|
||||
if loop_count % 100 == 0:
|
||||
print("[City] Loop count: ", loop_count)
|
||||
|
||||
def custom_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 = {i: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
|
||||
for i 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/custom_district.png')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
city = City()
|
||||
for i in range(10):
|
||||
city.add_district(Position(random.randint(0, 600), random.randint(0, 600)))
|
||||
city.loop_expend_district()
|
||||
city.custom_district_draw_map()
|
||||
90
world_maker/District.py
Normal file
90
world_maker/District.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from Position import Position
|
||||
|
||||
|
||||
class District:
|
||||
"""
|
||||
The District class represents a district in the world.
|
||||
A district can be characterized by its type and its unique id.
|
||||
|
||||
Attributes:
|
||||
tile_id (int): The unique id of the district.
|
||||
type (str): The type of the district. Can be "Forest", "City", "Mountain" or "Villa".
|
||||
"""
|
||||
|
||||
def __init__(self, tile_id: int):
|
||||
"""
|
||||
The constructor for the District class.
|
||||
|
||||
:param tile_id: Unique id of the district (Must be greater than 0)
|
||||
"""
|
||||
if tile_id <= 0:
|
||||
raise ValueError("Tile id must be greater than 0")
|
||||
self.tile_id = tile_id
|
||||
self.type = "" #Forest, City, Montain, Villa
|
||||
|
||||
|
||||
def verify_point(point: Position, point_new: Position, map_data: list[list[int]], height_map: list[list[int]]):
|
||||
"""
|
||||
Function to 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
|
||||
abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2)
|
||||
|
||||
|
||||
class CustomDistrict(District):
|
||||
"""
|
||||
The CustomDistrict 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):
|
||||
"""
|
||||
The constructor for the CustomDistrict class.
|
||||
|
||||
:param tile_id: Unique id of the district (Must be greater than 0)
|
||||
:param center: The center position from which the district expands.
|
||||
"""
|
||||
super().__init__(tile_id)
|
||||
self.center_expend = center
|
||||
self.area = [center]
|
||||
self.area_expend_from_point = [center]
|
||||
self.area_expend = []
|
||||
|
||||
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 verify_point(point, point + pos, map_data, height_map):
|
||||
if point + pos not in self.area_expend:
|
||||
self.area_expend.append(point + pos)
|
||||
self.area_expend_from_point.remove(point)
|
||||
|
||||
|
||||
class Edge: #I'm Edging rn
|
||||
def __init__(self, point1, point2):
|
||||
self.point1 = point1
|
||||
self.point2 = point2
|
||||
|
||||
|
||||
class VoronoiDistrict(District):
|
||||
def __init__(self, tile_id: int, center: Position):
|
||||
super().__init__(tile_id)
|
||||
self.center = center
|
||||
self.edges = []
|
||||
37
world_maker/Position.py
Normal file
37
world_maker/Position.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from math import sqrt, atan2
|
||||
|
||||
|
||||
class Position:
|
||||
def __init__(self, x: int = 0, y: int = 0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __add__(self, other: "Position") -> "Position":
|
||||
return Position(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other: "Position") -> "Position":
|
||||
return Position(self.x - other.x, self.y - other.y)
|
||||
|
||||
def __mul__(self, other: float) -> "Position":
|
||||
return Position(int(self.x * other), int(self.y * other))
|
||||
|
||||
def __truediv__(self, other: float) -> "Position":
|
||||
return Position(int(self.x / other), int(self.y / other))
|
||||
|
||||
def __str__(self):
|
||||
return f"({self.x}, {self.y})"
|
||||
|
||||
def __eq__(self, other: "Position"):
|
||||
return self.x == other.x and self.y == other.y
|
||||
|
||||
def get_tuple(self) -> tuple[int, int]:
|
||||
return self.x, self.y
|
||||
|
||||
def distance_to(self, other: "Position") -> float:
|
||||
return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
|
||||
|
||||
def norm(self) -> float:
|
||||
return sqrt(self.x ** 2 + self.y ** 2)
|
||||
|
||||
def angle_to(self, other: "Position") -> float:
|
||||
return atan2(self.y - other.y, other.x - self.x)
|
||||
237
world_maker/World.py
Normal file
237
world_maker/World.py
Normal file
@@ -0,0 +1,237 @@
|
||||
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(xzStart, "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
|
||||
|
||||
print('getData', xzStart[0] + x, y, xzStart[1] + z)
|
||||
|
||||
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=[]):
|
||||
print('propagate', coordinates)
|
||||
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()
|
||||
BIN
world_maker/data/custom_district.png
Normal file
BIN
world_maker/data/custom_district.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
BIN
world_maker/data/heightmap.png
Normal file
BIN
world_maker/data/heightmap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
world_maker/data/treemap.png
Normal file
BIN
world_maker/data/treemap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
BIN
world_maker/data/watermap.png
Normal file
BIN
world_maker/data/watermap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
17
world_maker/world_maker.py
Normal file
17
world_maker/world_maker.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import World
|
||||
|
||||
|
||||
def get_data(world: World):
|
||||
heightmap, watermap, treemap = world.getData()
|
||||
heightmap.save('./data/heightmap.png')
|
||||
watermap.save('./data/watermap.png')
|
||||
treemap.save('./data/treemap.png')
|
||||
|
||||
|
||||
def main():
|
||||
world = World.World()
|
||||
get_data(world)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user