From 32485d86bc85cc4060c7abdf2dfd9e49449c4887 Mon Sep 17 00:00:00 2001 From: Xeon0X Date: Thu, 13 Jun 2024 18:34:10 +0200 Subject: [PATCH] Everything cleaned and tested --- main.py | 33 +++++++++++-------- networks/geometry/Circle.py | 32 +++++++++--------- networks/geometry/Point2D.py | 17 +++++++--- networks/geometry/Point3D.py | 8 ++--- networks/geometry/Polyline.py | 30 +++++++++++++++-- networks/geometry/Segment2D.py | 55 +++++++++++++++++-------------- networks/geometry/Segment3D.py | 42 +++++++++++------------ networks/geometry/point_tools.py | 1 - output_image.png | Bin 5757 -> 4384 bytes 9 files changed, 130 insertions(+), 88 deletions(-) diff --git a/main.py b/main.py index 190bd5d..6ea285c 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ -from networks.geometry.Enums import LINE_OVERLAP, LINE_THICKNESS_MODE +from Enums import LINE_OVERLAP, LINE_THICKNESS_MODE, ROTATION from PIL import Image, ImageDraw import matplotlib.pyplot as plt from networks.geometry.Point3D import Point3D @@ -20,7 +20,6 @@ from buildings.Building import Building import random from networks.roads import Road as Road -from networks.geometry.Enums import ROTATION from networks.roads.intersections import Intersection as Intersection from networks.geometry.point_tools import curved_corner_by_curvature, curved_corner_by_distance @@ -319,27 +318,33 @@ image = Image.new('RGB', (width, height), 'white') draw = ImageDraw.Draw(image) -for i in range(len(p.coordinates)-1): - if p.coordinates[i] != None: - s = Segment2D(Point2D(p.coordinates[i].x, p.coordinates[i].y), Point2D( - p.coordinates[i+1].x, p.coordinates[i+1].y)) +for i in range(len(p.output_points)-1): + print("iiii", i) + if p.output_points[i] != None: + s = Segment2D(Point2D(p.output_points[i].x, p.output_points[i].y), Point2D( + p.output_points[i+1].x, p.output_points[i+1].y)) s.segment_thick(ww, LINE_THICKNESS_MODE.MIDDLE) - print(s.coordinates) - for j in range(len(s.coordinates)-1): + + for j in range(len(s.points_thick)-1): + print("j", j) # editor.placeBlock( # s.coordinates[j].coordinate, Block("cyan_concrete")) - draw.point((s.coordinates[j].x+w, - w-s.coordinates[j].y), fill='red') + draw.point((s.points_thick[j].x+w, + w-s.points_thick[j].y), fill='red') + print(s.points_thick[j]) for i in range(len(center)): + print("iiii", i) if center[i]: - circle = Circle(center[i], radius[i]-ww/2+1, radius[i]+ww/2+1) - for j in range(len(circle.coordinates)-1): + circle = Circle(center[i]) + circle.circle_thick(radius[i]-ww/2+1, radius[i]+ww/2+1) + for j in range(len(circle.points_thick)-1): # editor.placeBlock( # (circle.coordinates[j].x, y, circle.coordinates[j].y), Block("white_concrete")) - draw.point((circle.coordinates[j].x+w, - w-circle.coordinates[j].y), fill='black') + draw.point((circle.points_thick[j].x+w, + w-circle.points_thick[j].y), fill='black') + print(circle.points_thick[j]) image.save('output_image.png') diff --git a/networks/geometry/Circle.py b/networks/geometry/Circle.py index 1c17a7a..c6f78a2 100644 --- a/networks/geometry/Circle.py +++ b/networks/geometry/Circle.py @@ -8,14 +8,14 @@ class Circle: self.center = center self.radius = None - self.coordinates = [] + self.points = [] self.inner = None self.outer = None - self.coordinates_thick = [] + self.points_thick = [] self.spaced_radius = None - self.spaced_coordinates = [] + self.spaced_points = [] def __repr__(self): return f"Circle(center: {self.center}, radius: {self.radius}, spaced_radius: {self.spaced_radius}, inner: {self.inner}, outer: {self.outer})" @@ -28,10 +28,10 @@ class Circle: y = 0 error = 2-2*radius while (True): - self.coordinates.append(Point2D(center.x-x, center.y+y)) - self.coordinates.append(Point2D(center.x-y, center.y-x)) - self.coordinates.append(Point2D(center.x+x, center.y-y)) - self.coordinates.append(Point2D(center.x+y, center.y+x)) + self.points.append(Point2D(center.x-x, center.y+y)) + self.points.append(Point2D(center.x-y, center.y-x)) + self.points.append(Point2D(center.x+x, center.y-y)) + self.points.append(Point2D(center.x+y, center.y+x)) r = error if (r <= y): y += 1 @@ -96,7 +96,7 @@ class Circle: else: xi -= 1 erri += 2 * (y - xi + 1) - return self.coordinates_thick + return self.points_thick def circle_spaced(self, number: int, radius: int) -> List[Point2D]: """Get evenly spaced coordinates of the circle. @@ -113,25 +113,25 @@ class Circle: self.spaced_radius = radius center = self.center - self.spaced_coordinates = [ + self.spaced_points = [ Point2D(cos(2 * pi / number * i) * radius, sin(2 * pi / number * i) * radius) for i in range(0, number + 1) ] - for i in range(len(self.spaced_coordinates)): - self.spaced_coordinates[i] = Point2D( - self.spaced_coordinates[i].x + center.x, - self.spaced_coordinates[i].y + center.y + for i in range(len(self.spaced_points)): + self.spaced_points[i] = Point2D( + self.spaced_points[i].x + center.x, + self.spaced_points[i].y + center.y ).round() - return self.spaced_coordinates + return self.spaced_points def _x_line(self, x1, x2, y): while x1 <= x2: - self.coordinates_thick.append(Point2D(x1, y)) + self.points_thick.append(Point2D(x1, y)) x1 += 1 def _y_line(self, x, y1, y2): while y1 <= y2: - self.coordinates_thick.append(Point2D(x, y1)) + self.points_thick.append(Point2D(x, y1)) y1 += 1 diff --git a/networks/geometry/Point2D.py b/networks/geometry/Point2D.py index 4d2b49a..b2bedff 100644 --- a/networks/geometry/Point2D.py +++ b/networks/geometry/Point2D.py @@ -8,7 +8,7 @@ class Point2D: def __init__(self, x: int, y: int): self.x = x self.y = y - self.coordinate = (self.x, self.y) + self.coordinates = (self.x, self.y) def copy(self): return Point2D(self.x, self.y) @@ -167,8 +167,8 @@ class Point2D: """ if xy2 is None: xy2 = xy1.coordinate + np.array([1, 0]) - v0 = np.array(xy1.coordinate) - np.array(self.coordinate) - v1 = np.array(xy2.coordinate) - np.array(self.coordinate) + v0 = np.array(xy1.coordinate) - np.array(self.coordinates) + v1 = np.array(xy2.coordinate) - np.array(self.coordinates) angle = atan2(np.linalg.det([v0, v1]), np.dot(v0, v1)) return np.degrees(angle) @@ -176,17 +176,24 @@ class Point2D: def round(self, ndigits: int = None) -> "Point2D": self.x = round(self.x, ndigits) self.y = round(self.y, ndigits) - self.coordinate = (self.x, self.y) + self.coordinates = (self.x, self.y) return self def distance(self, point: "Point2D") -> int: return sqrt((point.x - self.x) ** 2 + (point.y - self.y) ** 2) + @staticmethod + def collinear(p0: "Point2D", p1: "Point2D", p2: "Point2D") -> bool: + # https://stackoverflow.com/questions/9608148/python-script-to-determine-if-x-y-coordinates-are-colinear-getting-some-e + x1, y1 = p1.x - p0.x, p1.y - p0.y + x2, y2 = p2.x - p0.x, p2.y - p0.y + return abs(x1 * y2 - x2 * y1) < 1e-12 + @staticmethod def to_vectors(points: List["Point3D"]) -> List[np.array]: vectors = [] for point in points: - vectors.append(np.array(point.coordinate)) + vectors.append(np.array(point.coordinates)) if (len(vectors) == 1): return vectors[0] diff --git a/networks/geometry/Point3D.py b/networks/geometry/Point3D.py index 03474de..7873612 100644 --- a/networks/geometry/Point3D.py +++ b/networks/geometry/Point3D.py @@ -8,7 +8,7 @@ class Point3D: self.x = x self.y = y self.z = z - self.coordinate = (x, y, z) + self.coordinates = (x, y, z) def copy(self): return Point3D(self.x, self.y, self.z) @@ -29,7 +29,7 @@ class Point3D: Returns: Point3D: The nearest point, and if multiple, the first in the list. - >>> print(Point3D(0, 0, 0).nearest((Point3D(-10, 10, 5), Point3D(10, 10, 1)))) + >>> Point3D(0, 0, 0).nearest((Point3D(-10, 10, 5), Point3D(10, 10, 1))) Point3D(x: 10, y: 10, z: 1) """ return min(points, key=lambda point: self.distance(point)) @@ -64,7 +64,7 @@ class Point3D: self.x = round(self.x, ndigits) self.y = round(self.y, ndigits) self.z = round(self.z, ndigits) - self.coordinate = (self.x, self.y, self.z) + self.coordinates = (self.x, self.y, self.z) return self def distance(self, point: "Point3D"): @@ -74,7 +74,7 @@ class Point3D: def to_vectors(points: List["Point3D"]): vectors = [] for point in points: - vectors.append(np.array(point.coordinate)) + vectors.append(np.array(point.coordinates)) if (len(vectors) == 1): return vectors[0] diff --git a/networks/geometry/Polyline.py b/networks/geometry/Polyline.py index 145fb32..6988140 100644 --- a/networks/geometry/Polyline.py +++ b/networks/geometry/Polyline.py @@ -18,8 +18,7 @@ class Polyline: >>> Polyline((Point2D(0, 0), Point2D(0, 10), Point2D(50, 10), Point2D(20, 20))) """ - self.coordinates = points - self.points = Point2D.to_vectors(points) + self.points = Point2D.to_vectors(self._remove_collinear_points(points)) self.length_polyline = len(points) if self.length_polyline < 4: @@ -34,12 +33,15 @@ class Polyline: self.radii = [None] * self.length_polyline # r self.centers = [None] * self.length_polyline # c + self.connections = [None] * self.length_polyline self._compute_requirements() self._compute_alpha_radii() self._alpha_assign(0, self.length_polyline-1) + self.output_points = points + def __repr__(self): return str(self.alpha_radii) @@ -51,6 +53,9 @@ class Polyline: return self.radii def get_centers(self): + if self.radii == [None] * self.length_polyline: + raise ValueError("No radii found. Run get_radii before.") + for i in range(1, self.length_polyline-1): bisector = (self.unit_vectors[i] - self.unit_vectors[i-1]) / ( np.linalg.norm(self.unit_vectors[i] - self.unit_vectors[i-1])) @@ -61,6 +66,15 @@ class Polyline: self.centers[i] = Point2D(array[0], array[1]).round() return self.centers + def get_arcs(self): + for i in range(1, self.length_polyline-1): + point_1 = self.points[i] - \ + self.alpha_radii[i] * self.unit_vectors[i-1] + point_2 = self.points[i] + \ + self.alpha_radii[i] * self.unit_vectors[i] + self.connections[i] = (point_1, point_2) + return self.connections + def _alpha_assign(self, start_index: int, end_index: int): """ The alpha-assign procedure assigning radii based on a polyline. @@ -131,3 +145,15 @@ class Polyline: def _compute_alpha_radii(self): self.alpha_radii[0] = 0 self.alpha_radii[self.length_polyline-1] = 0 + + @staticmethod + def _remove_collinear_points(points): + output_points = [points[0]] + + for i in range(1, len(points) - 1): + if not Point2D.collinear( + points[i-1], points[i], points[i+1]): + output_points.append(points[i]) + + output_points.append(points[-1]) + return output_points diff --git a/networks/geometry/Segment2D.py b/networks/geometry/Segment2D.py index 15a1067..46cc70c 100644 --- a/networks/geometry/Segment2D.py +++ b/networks/geometry/Segment2D.py @@ -8,14 +8,14 @@ class Segment2D: def __init__(self, start: Point2D, end: Point2D): self.start = start self.end = end - self.coordinates = [] - self.coordinates_thick = [] + self.points = [] + self.points_thick = [] self.thickness = None def __repr__(self): - return str(self.coordinates) + return str(self.points) - def segment(self, overlap: LINE_OVERLAP = LINE_OVERLAP.NONE, is_computing_thickness: bool = False) -> List[Point2D]: + def segment(self, start: Point2D = None, end: Point2D = None, overlap: LINE_OVERLAP = LINE_OVERLAP.NONE, _is_computing_thickness: bool = False) -> List[Point2D]: """Modified Bresenham draw (line) with optional overlap. From: https://github.com/ArminJo/Arduino-BlueDisplay/blob/master/src/LocalGUI/ThickLine.hpp @@ -24,12 +24,17 @@ class Segment2D: start (Point2D): Start point of the segment. end (Point2D): End point of the segment. overlap (LINE_OVERLAP): Overlap draws additional pixel when changing minor direction. For standard bresenham overlap, choose LINE_OVERLAP_NONE. Can also be LINE_OVERLAP_MAJOR or LINE_OVERLAP_MINOR. + _is_computing_thickness (bool, optionnal): Used by segment_thick. Don't touch. >>> Segment2D(Point2D(0, 0), Point2D(10, 15)) """ - start = self.start.copy() - end = self.end.copy() + if start == None or end == None: + start = self.start.copy() + end = self.end.copy() + else: + start = start.copy() + end = end.copy() # Direction delta_x = end.x - start.x @@ -50,7 +55,7 @@ class Segment2D: delta_2x = 2*delta_x delta_2y = 2*delta_y - self._add_coordinates(start, is_computing_thickness) + self._add_points(start, _is_computing_thickness) if (delta_x > delta_y): error = delta_2y - delta_x @@ -58,33 +63,33 @@ class Segment2D: start.x += step_x if (error >= 0): if (overlap == LINE_OVERLAP.MAJOR): - self._add_coordinates(start, is_computing_thickness) + self._add_points(start, _is_computing_thickness) start.y += step_y if (overlap == LINE_OVERLAP.MINOR): - self._add_coordinates( - Point2D(start.copy().x - step_x, start.copy().y), is_computing_thickness) + self._add_points( + Point2D(start.copy().x - step_x, start.copy().y), _is_computing_thickness) error -= delta_2x error += delta_2y - self._add_coordinates(start, is_computing_thickness) + self._add_points(start, _is_computing_thickness) else: error = delta_2x - delta_y while (start.y != end.y): start.y += step_y if (error >= 0): if (overlap == LINE_OVERLAP.MAJOR): - self._add_coordinates(start, is_computing_thickness) + self._add_points(start, _is_computing_thickness) start.x += step_x if (overlap == LINE_OVERLAP.MINOR): - self._add_coordinates( - Point2D(start.copy().x, start.copy().y - step_y), is_computing_thickness) + self._add_points( + Point2D(start.copy().x, start.copy().y - step_y), _is_computing_thickness) error -= delta_2y error += delta_2x - self._add_coordinates(start, is_computing_thickness) + self._add_points(start, _is_computing_thickness) - if not is_computing_thickness: - return self.coordinates + if not _is_computing_thickness: + return self.points def segment_thick(self, thickness: int, thickness_mode: LINE_THICKNESS_MODE) -> List[Point2D]: """Bresenham with thickness. @@ -150,7 +155,7 @@ class Segment2D: error += delta_2x self.segment( - start, end, LINE_OVERLAP.NONE, is_computing_thickness=True) + start, end, overlap=LINE_OVERLAP.NONE, _is_computing_thickness=True) error = delta_2x - delta_x for i in range(thickness, 1, -1): @@ -165,7 +170,7 @@ class Segment2D: error += delta_2y self.segment( - start, end, overlap, is_computing_thickness=True) + start, end, overlap=overlap, _is_computing_thickness=True) else: if swap: @@ -186,7 +191,7 @@ class Segment2D: error += delta_2x self.segment( - start, end, LINE_OVERLAP.NONE, is_computing_thickness=True) + start, end, overlap=LINE_OVERLAP.NONE, _is_computing_thickness=True) error = delta_2x - delta_y for i in range(thickness, 1, -1): @@ -201,9 +206,9 @@ class Segment2D: error += delta_2x self.segment( - start, end, overlap, is_computing_thickness=True) + start, end, overlap=overlap, _is_computing_thickness=True) - return self.coordinates + return self.points def perpendicular(self, distance: int) -> List[Point2D]: """Compute perpendicular points from both side of the segment placed at start level. @@ -232,8 +237,8 @@ class Segment2D: np.round((self.start.y + self.end.y) / 2.0).astype(int), ) - def _add_coordinates(self, coordinates, is_computing_thickness): + def _add_points(self, points, is_computing_thickness): if is_computing_thickness: - self.coordinates_thick.append(coordinates.copy()) + self.points_thick.append(points.copy()) else: - self.coordinates.append(coordinates.copy()) + self.points.append(points.copy()) diff --git a/networks/geometry/Segment3D.py b/networks/geometry/Segment3D.py index 89fbe96..e5dac0f 100644 --- a/networks/geometry/Segment3D.py +++ b/networks/geometry/Segment3D.py @@ -7,22 +7,22 @@ class Segment3D: def __init__(self, start: Point3D, end: Point3D): self.start = start self.end = end - self.coordinates = [] + self.output_points = [] def __repr__(self): - return str(self.coordinates) + return str(self.output_points) - def discrete_coordinates(self, overlap: bool = False): + def segment(self, overlap: bool = False): """Calculate a segment between two points in 3D space. 3d Bresenham algorithm. From: https://www.geeksforgeeks.org/bresenhams-algorithm-for-3-d-line-drawing/ Args: - overlap (bool, optional): If False, remove unnecessary coordinates connecting to other coordinates side by side, leaving only a diagonal connection. Defaults to False. + overlap (bool, optional): If False, remove unnecessary points connecting to other points side by side, leaving only a diagonal connection. Defaults to False. >>> Segment3D(Point3D(0, 0, 0), Point3D(10, 10, 15)) """ - self.coordinates.append(start.copy()) + self.output_points.append(start.copy()) dx = abs(self.end.x - self.start.x) dy = abs(self.end.y - self.start.y) dz = abs(self.end.z - self.start.z) @@ -45,18 +45,18 @@ class Segment3D: p2 = 2 * dz - dx while start.x != end.x: start.x += xs - self.coordinates.append(start.copy()) + self.output_points.append(start.copy()) if p1 >= 0: start.y += ys if not overlap: - if self.coordinates[-1].y != start.y: - self.coordinates.append(start.copy()) + if self.output_points[-1].y != start.y: + self.output_points.append(start.copy()) p1 -= 2 * dx if p2 >= 0: start.z += zs if not overlap: - if self.coordinates[-1].z != start.z: - self.coordinates.append(start.copy()) + if self.output_points[-1].z != start.z: + self.output_points.append(start.copy()) p2 -= 2 * dx p1 += 2 * dy p2 += 2 * dz @@ -67,18 +67,18 @@ class Segment3D: p2 = 2 * dz - dy while start.y != end.y: start.y += ys - self.coordinates.append(start.copy()) + self.output_points.append(start.copy()) if p1 >= 0: start.x += xs if not overlap: - if self.coordinates[-1].x != start.x: - self.coordinates.append(start.copy()) + if self.output_points[-1].x != start.x: + self.output_points.append(start.copy()) p1 -= 2 * dy if p2 >= 0: start.z += zs if not overlap: - if self.coordinates[-1].z != start.z: - self.coordinates.append(start.copy()) + if self.output_points[-1].z != start.z: + self.output_points.append(start.copy()) p2 -= 2 * dy p1 += 2 * dx p2 += 2 * dz @@ -89,22 +89,22 @@ class Segment3D: p2 = 2 * dx - dz while start.z != end.z: start.z += zs - self.coordinates.append(start.copy()) + self.output_points.append(start.copy()) if p1 >= 0: start.y += ys if not overlap: - if self.coordinates[-1].y != start.y: - self.coordinates.append(start.copy()) + if self.output_points[-1].y != start.y: + self.output_points.append(start.copy()) p1 -= 2 * dz if p2 >= 0: start.x += xs if not overlap: - if self.coordinates[-1].x != start.x: - self.coordinates.append(start.copy()) + if self.output_points[-1].x != start.x: + self.output_points.append(start.copy()) p2 -= 2 * dz p1 += 2 * dy p2 += 2 * dx - return self.coordinates + return self.output_points def middle_point(self): return (np.round((self.start.x + self.end.x) / 2.0).astype(int), diff --git a/networks/geometry/point_tools.py b/networks/geometry/point_tools.py index 1ca56bb..9f4f400 100644 --- a/networks/geometry/point_tools.py +++ b/networks/geometry/point_tools.py @@ -1,6 +1,5 @@ from math import sqrt, cos, pi, sin import numpy as np -from networks.geometry.segment_tools import discrete_segment, middle_point, parallel def segments_intersection(line0, line1, full_line=True): diff --git a/output_image.png b/output_image.png index df9422f8fe707286e718084b7324378f49052d62..8d29fc56b30b312307102127f8e6eb63a3fcbdea 100644 GIT binary patch literal 4384 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 V<;Noh!jlh?bN0})b!P&9|1V9IXSo0X literal 5757 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