diff --git a/main.py b/main.py index a0d761e..28309d7 100644 --- a/main.py +++ b/main.py @@ -1,31 +1,33 @@ -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 -from networks.geometry.Segment3D import Segment3D -from networks.geometry.Segment2D import Segment2D -from networks.geometry.Circle import Circle -from networks.geometry.Polyline import Polyline -from networks.geometry.Point2D import Point2D -import networks.roads.lines.Line as Line -import networks.roads.lanes.Lane as Lane -from gdpc import Editor, Block, geometry -import networks.geometry.curve_tools as curve_tools -import networks.geometry.Strip as Strip -import networks.geometry.segment_tools as segment_tools -import numpy as np import json -from buildings.Building import Building import random +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +from gdpc import Block, Editor, geometry +from PIL import Image, ImageDraw + +import networks.geometry.curve_tools as curve_tools +import networks.geometry.segment_tools as segment_tools +import networks.geometry.Strip as Strip +import networks.roads.lanes.Lane as Lane +import networks.roads.lines.Line as Line +from buildings.Building import Building +from Enums import LINE_OVERLAP, LINE_THICKNESS_MODE, ROTATION +from networks.geometry.Circle import Circle +from networks.geometry.Point2D import Point2D +from networks.geometry.Point3D import Point3D +from networks.geometry.point_tools import ( + curved_corner_by_curvature, + curved_corner_by_distance, +) +from networks.geometry.Polyline import Polyline +from networks.geometry.Segment2D import Segment2D +from networks.geometry.Segment3D import Segment3D from networks.roads import Road as Road from networks.roads.intersections import Intersection as Intersection -from networks.geometry.point_tools import curved_corner_by_curvature, curved_corner_by_distance - - -import matplotlib matplotlib.use('Agg') @@ -286,18 +288,26 @@ block_list = ["blue_concrete", "red_concrete", "green_concrete", # p = Polyline((Point2D(-1225, 468), Point2D(-1138, 481), # Point2D(-1188, 451), Point2D(-1176, 409), Point2D(-1179, 399))) -w = 200 +w = 100 -n_points = 20 +n_points = 8 min_val, max_val = -w, w random_points = [Point2D(random.randint(min_val, max_val), random.randint( min_val, max_val)) for _ in range(n_points)] +print(random_points) +print("\n\n") + # random_points = (Point2D(-75, -75), Point2D(0, -75), Point2D(75, 75), # Point2D(75, -50), Point2D(-50, 50), Point2D(0, 0)) +# random_points = random_points[0].optimized_path(random_points) + +# random_points = [Point2D(-40, -56), Point2D(-94, 92), Point2D(19, -47), Point2D( +# 100, 59), Point2D(-85, -73), Point2D(-33, -9), Point2D(57, -25), Point2D(51, -34)] + random_points = random_points[0].optimized_path(random_points) p = Polyline(random_points) @@ -316,10 +326,8 @@ image = Image.new('RGB', (width, height), 'black') draw = ImageDraw.Draw(image) -print(p.output_points) - for i in range(len(p.output_points)-1): - if p.output_points[i] != None: + if p.output_points[i] != 0: 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) @@ -331,40 +339,40 @@ for i in range(len(p.output_points)-1): w-s.points_thick[j].y), fill='grey') -for i in range(2, len(p.get_arcs_intersections())-2): +# for i in range(2, len(p.get_arcs_intersections())-2): - s = Segment2D(Point2D(p.acrs_intersections[i][0].x, p.acrs_intersections[i][0].y), Point2D( - p.acrs_intersections[i-1][-1].x, p.acrs_intersections[i-1][-1].y)) - s.segment_thick(ww, LINE_THICKNESS_MODE.MIDDLE) +# s = Segment2D(Point2D(p.acrs_intersections[i][0].x, p.acrs_intersections[i][0].y), Point2D( +# p.acrs_intersections[i-1][-1].x, p.acrs_intersections[i-1][-1].y)) +# s.segment_thick(ww, LINE_THICKNESS_MODE.MIDDLE) - for j in range(len(s.points_thick)-1): - # editor.placeBlock( - # s.coordinates[j].coordinate, Block("cyan_concrete")) - draw.point((s.points_thick[j].x+w, - w-s.points_thick[j].y), fill='white') - draw.point((p.acrs_intersections[i][0].x+w, - w-p.acrs_intersections[i][0].y), fill='blue') - draw.point((p.acrs_intersections[i][-1].x+w, - w-p.acrs_intersections[i][-1].y), fill='red') +# for j in range(len(s.points_thick)-1): +# # editor.placeBlock( +# # s.coordinates[j].coordinate, Block("cyan_concrete")) +# draw.point((s.points_thick[j].x+w, +# w-s.points_thick[j].y), fill='green') +# draw.point((p.acrs_intersections[i][0].x+w, +# w-p.acrs_intersections[i][0].y), fill='green') +# draw.point((p.acrs_intersections[i][-1].x+w, +# w-p.acrs_intersections[i][-1].y), fill='green') -for i in range(len(center)): - if center[i]: - circle = Circle(center[i]) - circle.circle_thick(round(radius[i]-ww/2), round(radius[i]+ww/2)) - for j in range(len(circle.points_thick)-1): - if circle.points_thick[j].is_in_triangle(p.acrs_intersections[i][0], p.acrs_intersections[i][1], p.acrs_intersections[i][2]): - # editor.placeBlock( - # (circle.coordinates[j].x, y, circle.coordinates[j].y), Block("white_concrete")) - draw.point((circle.points_thick[j].x+w, - w-circle.points_thick[j].y), fill='white') - circle.circle(radius[i]) - for j in range(len(circle.points)-1): - if circle.points[j].is_in_triangle(p.acrs_intersections[i][0], p.acrs_intersections[i][1], p.acrs_intersections[i][2]): - # editor.placeBlock( - # (circle.coordinates[j].x, y, circle.coordinates[j].y), Block("white_concrete")) - draw.point((circle.points[j].x+w, - w-circle.points[j].y), fill='purple') +# for i in range(len(center)): +# if center[i]: +# circle = Circle(center[i]) +# circle.circle_thick(round(radius[i]-ww/2), round(radius[i]+ww/2)) +# for j in range(len(circle.points_thick)-1): +# if circle.points_thick[j].is_in_triangle(p.acrs_intersections[i][0], p.acrs_intersections[i][1], p.acrs_intersections[i][2]): +# # editor.placeBlock( +# # (circle.coordinates[j].x, y, circle.coordinates[j].y), Block("white_concrete")) +# draw.point((circle.points_thick[j].x+w, +# w-circle.points_thick[j].y), fill='green') +# circle.circle(radius[i]) +# for j in range(len(circle.points)-1): +# if circle.points[j].is_in_triangle(p.acrs_intersections[i][0], p.acrs_intersections[i][1], p.acrs_intersections[i][2]): +# # editor.placeBlock( +# # (circle.coordinates[j].x, y, circle.coordinates[j].y), Block("white_concrete")) +# draw.point( +# (circle.points[j].x+w, w-circle.points[j].y), fill='green') s1 = Segment2D(Point2D(p.acrs_intersections[1][0].x, p.acrs_intersections[1][0].y), Point2D( p.output_points[0].x, p.output_points[0].y)) @@ -372,7 +380,7 @@ s1.segment_thick(ww, LINE_THICKNESS_MODE.MIDDLE) for j in range(len(s1.points_thick)-1): draw.point((s1.points_thick[j].x+w, - w-s1.points_thick[j].y), fill='white') + w-s1.points_thick[j].y), fill='grey') s1 = Segment2D(Point2D(p.acrs_intersections[-2][2].x, p.acrs_intersections[-2][2].y), Point2D( p.output_points[-1].x, p.output_points[-1].y)) @@ -380,10 +388,34 @@ s1.segment_thick(ww, LINE_THICKNESS_MODE.MIDDLE) for j in range(len(s1.points_thick)-1): draw.point((s1.points_thick[j].x+w, - w-s1.points_thick[j].y), fill='white') + w-s1.points_thick[j].y), fill='grey') + +for i in range(0, len(p.arcs)): + for j in range(len(p.arcs[i])): + draw.point((p.arcs[i][j].x+w, w-p.arcs[i][j].y), fill='white') + + +for i in range(1, len(p.segments)-1): + for j in range(len(p.segments[i].segment())): + draw.point((p.segments[i].points[j].x+w, + w-p.segments[i].points[j].y), fill='white') + +for i in range(1, len(p.centers)-1): + draw.point((p.centers[i].x+w, w-p.centers[i].y), fill='red') + draw.point((p.acrs_intersections[i][0].x+w, + w-p.acrs_intersections[i][0].y), fill='blue') + draw.point((p.acrs_intersections[i][1].x+w, + w-p.acrs_intersections[i][1].y), fill='purple') + draw.point((p.acrs_intersections[i][2].x+w, + w-p.acrs_intersections[i][2].y), fill='blue') + image.save('output_image.png') +# s = Segment2D(Point2D(-88, -12), Point2D(9, 75)) +# s.segment_thick(3, LINE_THICKNESS_MODE.MIDDLE) +# print(s.points) + # s = Segment2D(Point2D(0, 0), Point2D(10, 10)).perpendicular(10) # print(s) diff --git a/networks/geometry/Circle.py b/networks/geometry/Circle.py index caff87c..cda5585 100644 --- a/networks/geometry/Circle.py +++ b/networks/geometry/Circle.py @@ -1,21 +1,24 @@ -from networks.geometry.Point2D import Point2D -from math import cos, sin, pi +from math import cos, pi, sin from typing import List +import numpy as np + +from networks.geometry.Point2D import Point2D + class Circle: def __init__(self, center: Point2D): self.center = center self.radius = None - self.points = [] + self.points: List[Point2D] = [] self.inner = None self.outer = None - self.points_thick = [] + self.points_thick: List[Point2D] = [] self.spaced_radius = None - self.spaced_points = [] + self.spaced_points: List[Point2D] = [] def __repr__(self): return f"Circle(center: {self.center}, radius: {self.radius}, spaced_radius: {self.spaced_radius}, inner: {self.inner}, outer: {self.outer})" @@ -43,6 +46,7 @@ class Circle: continue else: break + return self.points def circle_thick(self, inner: int, outer: int) -> List[Point2D]: """Compute discrete value of a 2d-circle with thickness. @@ -114,16 +118,17 @@ class Circle: center = self.center self.spaced_points = [ - Point2D(cos(2 * pi / number * i) * radius, - sin(2 * pi / number * i) * radius) + Point2D(round(cos(2 * pi / number * i) * radius), + round(sin(2 * pi / number * i) * radius)) for i in range(0, number + 1) ] for i in range(len(self.spaced_points)): - self.spaced_points[i] = Point2D( + current_point = Point2D( self.spaced_points[i].x + center.x, self.spaced_points[i].y + center.y ).round() + self.spaced_points[i] = current_point return self.spaced_points def _x_line(self, x1, x2, y): diff --git a/networks/geometry/Point2D.py b/networks/geometry/Point2D.py index b2bedff..ef8a804 100644 --- a/networks/geometry/Point2D.py +++ b/networks/geometry/Point2D.py @@ -1,6 +1,8 @@ -import numpy as np -from typing import List from math import atan2, sqrt +from typing import List, Union + +import numpy as np + from Enums import ROTATION @@ -21,10 +23,26 @@ class Point2D: return self.x == other.x and self.y == other.y return False + def __add__(self, other): + if isinstance(other, np.ndarray) and other.shape == (2,): + return Point2D(self.x + other[0], self.y + other[1]) + elif isinstance(other, Point2D): + return Point2D(self.x + other.x, self.y + other.y) + else: + raise TypeError(f"Unsupported type for addition: {type(other)}") + + def __sub__(self, other): + if isinstance(other, np.ndarray) and other.shape == (2,): + return Point2D(self.x - other[0], self.y - other[1]) + elif isinstance(other, Point2D): + return Point2D(self.x - other.x, self.y - other.y) + else: + raise TypeError(f"Unsupported type for subtraction: {type(other)}") + def is_in_triangle(self, xy0: "Point2D", xy1: "Point2D", xy2: "Point2D"): """Returns True is the point is in a triangle defined by 3 others points. - From: https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle#:~:text=A%20simple%20way%20is%20to,point%20is%20inside%20the%20triangle. + From: https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle Args: xy0 (Type[Point2D]): Point of the triangle. @@ -190,12 +208,21 @@ class Point2D: 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.coordinates)) - - if (len(vectors) == 1): - return vectors[0] - else: + def to_arrays(points: Union[List["Point2D"], "Point2D"]) -> Union[List[np.array], "Point2D"]: + if isinstance(points, list): + vectors = [] + for point in points: + vectors.append(np.array(point.coordinates)) return vectors + else: + return np.array(points.coordinates) + + @staticmethod + def from_arrays(vectors: Union[List[np.array], "Point2D"]) -> Union[List["Point2D"], "Point2D"]: + if isinstance(vectors, list): + points = [] + for vector in vectors: + points.append(Point2D(vector[0], vector[1])) + return points + else: + return Point2D(vectors[0], vectors[1]) diff --git a/networks/geometry/Point3D.py b/networks/geometry/Point3D.py index 7873612..75fb36c 100644 --- a/networks/geometry/Point3D.py +++ b/networks/geometry/Point3D.py @@ -1,5 +1,6 @@ +from math import sqrt from typing import List -from math import atan2, sqrt + import numpy as np @@ -80,3 +81,14 @@ class Point3D: return vectors[0] else: return vectors + + @staticmethod + def from_arrays(vectors: List[np.array]) -> List["Point3D"]: + points = [] + for vector in vectors: + points.append(Point3D(vector[0], vector[1], vector[2])) + + if (len(points) == 1): + return points[0] + else: + return points diff --git a/networks/geometry/Polyline.py b/networks/geometry/Polyline.py index da20544..5f320ec 100644 --- a/networks/geometry/Polyline.py +++ b/networks/geometry/Polyline.py @@ -1,11 +1,17 @@ -from networks.geometry.Point2D import Point2D +from math import inf, sqrt +from typing import List, Tuple, Union -from math import sqrt, inf import numpy as np +from networks.geometry.Circle import Circle +from networks.geometry.Point2D import Point2D +from networks.geometry.Segment2D import Segment2D + +# from Enums import LINE_THICKNESS_MODE, LINE_OVERLAP + class Polyline: - def __init__(self, points: list["Point2D"]): + def __init__(self, points: List[Point2D]): """A polyline with smooth corners, only composed of segments and circle arc. Mathematics and algorithms behind this can be found here: https://cdr.lib.unc.edu/concern/dissertations/pz50gw814?locale=en, E2 Construction of arc roads from polylines, page 210. @@ -18,7 +24,8 @@ class Polyline: >>> Polyline((Point2D(0, 0), Point2D(0, 10), Point2D(50, 10), Point2D(20, 20))) """ - self.points_array = Point2D.to_vectors( + self.output_points = points + self.points_array = Point2D.to_arrays( self._remove_collinear_points(points)) self.length_polyline = len(self.points_array) @@ -26,36 +33,46 @@ class Polyline: raise ValueError("The list must contain at least 4 elements.") self.vectors = [None] * self.length_polyline # v - self.lengths = [None] * (self.length_polyline - 1) # l + self.lengths = [0] * (self.length_polyline - 1) # l self.unit_vectors = [None] * self.length_polyline # n self.tangente = [0] * self.length_polyline # f # alpha, maximum radius factor - self.alpha_radii = [None] * self.length_polyline + self.alpha_radii = [0] * self.length_polyline - self.radii = [None] * self.length_polyline # r - self.centers = [None] * self.length_polyline # c + # Useful outputs. In order to not break indexation, each list has the same length, even if for n points, there is n-2 radius. + # Lists will start and end with None. + self.radii = [0] * self.length_polyline # r, list of points + self.centers = [None] * self.length_polyline # c, list of points + # list of tuple of points (first intersection, corresponding corner, last intersection) self.acrs_intersections = [None] * self.length_polyline + self.arcs = [[]] * self.length_polyline # list of points + # self.not_arcs = [[]] * self.length_polyline + # For n points, there is n-1 segments. Last element should stays None. + self.segments = [None] * \ + self.length_polyline # list of segments + + # Run procedure self._compute_requirements() self._compute_alpha_radii() self._alpha_assign(0, self.length_polyline-1) - - self.output_points = points + self.get_radii() + self.get_centers() + self.get_arcs_intersections() + self.get_arcs() + self.get_segments() def __repr__(self): return str(self.alpha_radii) - def get_radii(self): + def get_radii(self) -> List[Union[int]]: for i in range(1, self.length_polyline-1): self.radii[i] = round(self.alpha_radii[i] * self.tangente[i]) return self.radii - def get_centers(self): - if self.radii == [None] * self.length_polyline: - raise ValueError("No radii found. Run get_radii before.") - + def get_centers(self) -> List[Union[Point2D, None]]: 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])) @@ -65,16 +82,68 @@ class Polyline: self.centers[i] = Point2D(array[0], array[1]).round() return self.centers - def get_arcs_intersections(self): + def get_arcs_intersections(self) -> List[Tuple[Point2D]]: + """Get arcs intersections points. + + First and last elements elements of the list should be None. For n points, there are n-1 segments, and n-2 angle. + + Returns: + list[tuple(Point2D)]: List of tuples composed - in order - of the first arc points, the corner points, the last arc points. The corresponding arc circle is inside this triangle. + """ for i in range(1, self.length_polyline-1): - point_1 = self.points_array[i] - \ - self.alpha_radii[i] * self.unit_vectors[i-1] - point_2 = self.points_array[i] + \ - self.alpha_radii[i] * self.unit_vectors[i] - self.acrs_intersections[i] = Point2D( - point_1[0], point_1[1]).round(), Point2D(self.points_array[i][0], self.points_array[i][1]), Point2D(point_2[0], point_2[1]).round() + point_1 = Point2D.from_arrays(self.points_array[i] - + self.alpha_radii[i] * self.unit_vectors[i-1]) + point_2 = Point2D.from_arrays(self.points_array[i] + + self.alpha_radii[i] * self.unit_vectors[i]) + self.acrs_intersections[i] = point_1.round(), Point2D.from_arrays( + self.points_array[i]), point_2.round() return self.acrs_intersections + def get_arcs(self) -> List[Point2D]: + for i in range(1, self.length_polyline-1): + circle = Circle(self.centers[i]) + circle.circle(self.radii[i]) + for j in range(len(circle.points)): + if circle.points[j].is_in_triangle(self.acrs_intersections[i][0], self.acrs_intersections[i][1], self.acrs_intersections[i][2]): + self.arcs[i].append(circle.points[j]) + # for j in range(len(circle.points)): + # if (circle.points[j] in Segment2D(self.acrs_intersections[i][0], self.acrs_intersections[i][1]).segment(LINE_OVERLAP.MINOR)): + # self.not_arcs[i].append(circle.points[j]) + # print("hzeh") + # if (circle.points[j] in Segment2D(self.acrs_intersections[i][1], self.acrs_intersections[i][2]).segment(LINE_OVERLAP.MINOR)): + # self.not_arcs[i].append(circle.points[j]) + # print("hzeh") + # if (circle.points[j] in Segment2D(self.acrs_intersections[i][2], self.acrs_intersections[i][0]).segment(LINE_OVERLAP.MINOR)): + # self.not_arcs[i].append(circle.points[j]) + # print("hzeh") + return self.arcs + + def get_segments(self) -> List[Segment2D]: + """Get the segments between the circle arcs and at the start and end. + + Last list element should be None, and last usable index is -2 or self.length_polyline - 2. For n points, there are n-1 segments. + + Returns: + list[Segment2D]: List of segments in order. + """ + # Get first segment. + # segments index is 0, corresponding to the first points_array to the first point ([0]) of the first arc (acrs_intersections[1]). + # First arc index is 1 because index 0 is None due to fix list lenght. Is it a good choice? + self.segments[1] = Segment2D(Point2D.from_arrays( + self.points_array[0]), self.acrs_intersections[1][0]) + + # Get segments between arcs + for i in range(2, self.length_polyline - 2): + self.segments[i] = Segment2D(Point2D(self.acrs_intersections[i][0].x, self.acrs_intersections[i][0].y), Point2D( + self.acrs_intersections[i-1][-1].x, self.acrs_intersections[i-1][-1].y)) + + # Get last segment. Index is -2 because last index -1 should be None due to the same list lenght. + # For n points, there are n-1 segments. + self.segments[-2] = Segment2D(Point2D.from_arrays( + self.points_array[-1]), self.acrs_intersections[-2][2]) + + return self.segments + def _alpha_assign(self, start_index: int, end_index: int): """ The alpha-assign procedure assigning radii based on a polyline. @@ -101,8 +170,8 @@ class Polyline: minimum_radius, minimum_index = current_radius, i alpha_low, alpha_high = alpha_a, alpha_b - alpha_a = min(self.lengths[end_index-2], - self.lengths[end_index-1]-self.alpha_radii[end_index]) + alpha_a = min( + self.lengths[end_index-2], self.lengths[end_index-1]-self.alpha_radii[end_index]) current_radius = max(self.tangente[end_index-1]*alpha_a, self.tangente[end_index] * self.alpha_radii[end_index]) # Radius at final segment @@ -123,9 +192,8 @@ class Polyline: """ Returns the radius that balances the radii on either end segement i. """ - - alpha_a = min(self.lengths[i-1], (self.lengths[i]*self.tangente[i+1]) / - (self.tangente[i] + self.tangente[i+1])) + alpha_a = min(self.lengths[i-1], (self.lengths[i] * + self.tangente[i+1])/(self.tangente[i] + self.tangente[i+1])) alpha_b = min(self.lengths[i+1], self.lengths[i]-alpha_a) return alpha_a, alpha_b, min(self.tangente[i]*alpha_a, self.tangente[i+1]*alpha_b) diff --git a/networks/geometry/Segment2D.py b/networks/geometry/Segment2D.py index 46cc70c..2acf5e8 100644 --- a/networks/geometry/Segment2D.py +++ b/networks/geometry/Segment2D.py @@ -1,21 +1,23 @@ -from typing import List +from typing import List, Union + +import numpy as np + from Enums import LINE_OVERLAP, LINE_THICKNESS_MODE from networks.geometry.Point2D import Point2D -from math import sqrt class Segment2D: def __init__(self, start: Point2D, end: Point2D): self.start = start self.end = end - self.points = [] - self.points_thick = [] + self.points: List[Point2D] = [] + self.points_thick: List[Point2D] = [] self.thickness = None def __repr__(self): - return str(self.points) + return str(f"Segment2D(start: {self.start}, end: {self.end}, points: {self.points})") - def segment(self, start: Point2D = None, end: Point2D = None, 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) -> Union[List[Point2D], None]: """Modified Bresenham draw (line) with optional overlap. From: https://github.com/ArminJo/Arduino-BlueDisplay/blob/master/src/LocalGUI/ThickLine.hpp @@ -29,7 +31,7 @@ class Segment2D: >>> Segment2D(Point2D(0, 0), Point2D(10, 15)) """ - if start == None or end == None: + if start is None or end is None: start = self.start.copy() end = self.end.copy() else: @@ -90,6 +92,7 @@ class Segment2D: if not _is_computing_thickness: return self.points + return None def segment_thick(self, thickness: int, thickness_mode: LINE_THICKNESS_MODE) -> List[Point2D]: """Bresenham with thickness. @@ -208,7 +211,7 @@ class Segment2D: self.segment( start, end, overlap=overlap, _is_computing_thickness=True) - return self.points + return self.points_thick def perpendicular(self, distance: int) -> List[Point2D]: """Compute perpendicular points from both side of the segment placed at start level. diff --git a/output_image.png b/output_image.png index d90c854..b091c41 100644 Binary files a/output_image.png and b/output_image.png differ