Files
GDMC-2024/networks/legacy_roads/maths.py
2024-06-16 16:35:15 +02:00

1156 lines
35 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from math import sqrt
from math import pi
from math import cos, sin
import matplotlib.pyplot as plt
import numpy as np
from scipy import interpolate
def line(xyz1, xyz2, pixelPerfect=True):
"""
Calculate a line between two points in 3D space.
https://www.geeksforgeeks.org/bresenhams-algorithm-for-3-d-line-drawing/
Args:
xyz1 (tuple): First coordinates.
xyz2 (tuple): Second coordinates.
pixelPerfect (bool, optional): Blocks will be placed diagonally,
not side by side if pixelPerfect is True. Defaults to True.
Returns:
list: List of blocks.
"""
(x1, y1, z1) = xyz1
(x2, y2, z2) = xyz2
x1, y1, z1, x2, y2, z2 = (
round(x1),
round(y1),
round(z1),
round(x2),
round(y2),
round(z2),
)
ListOfPoints = []
ListOfPoints.append((x1, y1, z1))
dx = abs(x2 - x1)
dy = abs(y2 - y1)
dz = abs(z2 - z1)
if x2 > x1:
xs = 1
else:
xs = -1
if y2 > y1:
ys = 1
else:
ys = -1
if z2 > z1:
zs = 1
else:
zs = -1
# Driving axis is X-axis
if dx >= dy and dx >= dz:
p1 = 2 * dy - dx
p2 = 2 * dz - dx
while x1 != x2:
x1 += xs
ListOfPoints.append((x1, y1, z1))
if p1 >= 0:
y1 += ys
if not pixelPerfect:
if ListOfPoints[-1][1] != y1:
ListOfPoints.append((x1, y1, z1))
p1 -= 2 * dx
if p2 >= 0:
z1 += zs
if not pixelPerfect:
if ListOfPoints[-1][2] != z1:
ListOfPoints.append((x1, y1, z1))
p2 -= 2 * dx
p1 += 2 * dy
p2 += 2 * dz
# Driving axis is Y-axis
elif dy >= dx and dy >= dz:
p1 = 2 * dx - dy
p2 = 2 * dz - dy
while y1 != y2:
y1 += ys
ListOfPoints.append((x1, y1, z1))
if p1 >= 0:
x1 += xs
if not pixelPerfect:
if ListOfPoints[-1][0] != x1:
ListOfPoints.append((x1, y1, z1))
p1 -= 2 * dy
if p2 >= 0:
z1 += zs
if not pixelPerfect:
if ListOfPoints[-1][2] != z1:
ListOfPoints.append((x1, y1, z1))
p2 -= 2 * dy
p1 += 2 * dx
p2 += 2 * dz
# Driving axis is Z-axis
else:
p1 = 2 * dy - dz
p2 = 2 * dx - dz
while z1 != z2:
z1 += zs
ListOfPoints.append((x1, y1, z1))
if p1 >= 0:
y1 += ys
if not pixelPerfect:
if ListOfPoints[-1][1] != y1:
ListOfPoints.append((x1, y1, z1))
p1 -= 2 * dz
if p2 >= 0:
x1 += xs
if not pixelPerfect:
if ListOfPoints[-1][0] != x1:
ListOfPoints.append((x1, y1, z1))
p2 -= 2 * dz
p1 += 2 * dy
p2 += 2 * dx
return ListOfPoints
def offset(distance, xy1, xy2):
"""
Compute the coordinates of perpendicular points from two points. 2D
only.
Args:
distance (int): Distance from the line[xy1;xy2] of the
perpendicular points.
xy1 (tuple): First position.
xy2 (tuple): Second position.
Returns:
tuple: The coordinates of perpendicular points.
A: Perpendicular from [xy1;xy2] at distance from pos1.
B: perpendicular from [xy1;xy2] at -distance from pos1.
C: perpendicular from [xy2;xy1] at distance from pos2.
D: perpendicular from [xy2;xy1] at -distance from pos2.
"""
A, B = perpendicular(distance * 2, xy1, xy2)
C, D = perpendicular(distance * 2, xy2, xy1)
return ([A, D], [B, C])
def perpendicular(distance, xy1, xy2):
"""
Return a tuple of the perpendicular coordinates.
Args:
distance (int): Distance from the line[xy1;xy2].
xy1 (tuple): First coordinates.
xy2 (tuple): Second coordinates.
Returns:
tuple: Coordinates of the line length distance, perpendicular
to [xy1; xy2] at xy1.
"""
(x1, y1) = xy1
(x2, y2) = xy2
dx = x1 - x2
dy = y1 - y2
dist = sqrt(dx * dx + dy * dy)
dx /= dist
dy /= dist
x3 = x1 + (distance / 2) * dy
y3 = y1 - (distance / 2) * dx
x4 = x1 - (distance / 2) * dy
y4 = y1 + (distance / 2) * dx
return ((round(x3), round(y3)), (round(x4), round(y4)))
def curve(points, number_true_pts=40, debug=False):
"""
Returns a 3d curve.
https://stackoverflow.com/questions/18962175/spline-interpolation-coefficients-of-a-line-curve-in-3d-space
Args:
points (np.array): Points where the curves should pass.
number_true_pts (int, optional): Number of points to compute. Defaults to 40.
debug (bool, optional): Just a visual graphic. Defaults to False.
Returns:
tuple: Tuple of list of each coordinate.
"""
# Remove duplicates.
points = tuple(map(tuple, points))
points = sorted(set(points), key=points.index)
x_sample = []
y_sample = []
z_sample = []
for i in range(len(points)):
x_sample.append(points[i][0])
z_sample.append(points[i][1])
y_sample.append(points[i][2])
x_sample = np.array(x_sample)
y_sample = np.array(y_sample)
z_sample = np.array(z_sample)
tck, u = interpolate.splprep([x_sample, y_sample, z_sample], s=2, k=2)
x_knots, y_knots, z_knots = interpolate.splev(tck[0], tck)
u_fine = np.linspace(0, 1, number_true_pts)
x_fine, y_fine, z_fine = interpolate.splev(u_fine, tck)
if debug:
fig2 = plt.figure(2)
ax3d = fig2.add_subplot(111, projection="3d")
ax3d.plot(x_sample, y_sample, z_sample, "r*")
ax3d.plot(x_knots, y_knots, z_knots, "go")
ax3d.plot(x_fine, y_fine, z_fine, "r")
fig2.show()
plt.show()
x = x_fine.tolist()
z = y_fine.tolist()
y = z_fine.tolist()
for i in x:
i = round(i)
for i in y:
i = round(i)
for i in z:
i = round(i)
return x, y, z
def curveOffset(x, y, z, distance=5):
"""
Offset a curve.
Args:
x (list): List of x coordinates.
y (list): List of y coordinates.
z (list): List of z coordinates.
distance (int, optional): Distance of offsetting. Defaults to 5.
Returns:
tuple: Lists of points from the upper curve and the lower curve.
TODO:
The accuracy can be improved by finding the inner and outer arc:
connect the points of the arc and not calculate their
middle.
"""
lineA = []
lineB = []
line0 = []
line1 = []
# Offsetting
for i in range(len(x) - 1):
parallel = offset(distance, (x[i], z[i]), (x[i + 1], z[i + 1]))
lineA.append(
(
(parallel[0][0][0], y[i], parallel[0][0][1]),
(parallel[0][1][0], y[i + 1], parallel[0][1][1]),
)
)
lineB.append(
(
(parallel[1][0][0], y[i], parallel[1][0][1]),
(parallel[1][1][0], y[i + 1], parallel[1][1][1]),
)
)
# First points
# print(x, y, z, distance)
# print(x, len(x))
# print("lineA:", lineA)
# print("parallel:", parallel)
line0.append(
(
round(lineA[0][0][0]),
round(lineA[0][0][1]),
round(lineA[0][0][2]),
)
)
line1.append(
(
round(lineB[0][0][0]),
round(lineB[0][0][1]),
round(lineB[0][0][2]),
)
)
# Middle of between segments
for i in range(len(lineA) - 1):
line0.append(
(
round((lineA[i][1][0] + lineA[i + 1][0][0]) / 2),
round((lineA[i][1][1] + lineA[i + 1][0][1]) / 2),
round((lineA[i][1][2] + lineA[i + 1][0][2]) / 2),
)
)
line1.append(
(
round((lineB[i][1][0] + lineB[i + 1][0][0]) / 2),
round((lineB[i][1][1] + lineB[i + 1][0][1]) / 2),
round((lineB[i][1][2] + lineB[i + 1][0][2]) / 2),
)
)
# Last points
line0.append(
(
round(lineA[-1][1][0]),
round(lineA[-1][1][1]),
round(lineA[-1][1][2]),
)
)
line1.append(
(
round(lineB[-1][1][0]),
round(lineB[-1][1][1]),
round(lineB[-1][1][2]),
)
)
return line0, line1
def pixelPerfect(path):
"""
Remove blocks that are side by side in the path. Keep the blocks
that are in diagonal.
Args:
path (list): List of coordinates from a path.
Returns:
list: List cleaned.
TODO:
Add 3D.
"""
# NotPixelPerfect detection
if len(path) == 1 or len(path) == 0:
return path
else:
notPixelPerfect = []
c = 0
while c < len(path):
if c > 0 and c + 1 < len(path):
if (
(
path[c - 1][0] == path[c][0]
or path[c - 1][1] == path[c][1]
)
and (
path[c + 1][0] == path[c][0]
or path[c + 1][1] == path[c][1]
)
and path[c - 1][1] != path[c + 1][1]
and path[c - 1][0] != path[c + 1][0]
):
notPixelPerfect.append(path[c])
c += 1
# Double notPixelPerfect detection
if len(notPixelPerfect) == 1 or len(notPixelPerfect) == 0:
return notPixelPerfect
else:
d = 0
while d < len(notPixelPerfect):
if d + 1 < len(notPixelPerfect):
if (
notPixelPerfect[d][0] == notPixelPerfect[d + 1][0]
and (notPixelPerfect[d][1] - notPixelPerfect[d + 1][1])
in {1, -1}
) or (
notPixelPerfect[d][1] == notPixelPerfect[d + 1][1]
and (notPixelPerfect[d][0] - notPixelPerfect[d + 1][0])
in {1, -1}
):
notPixelPerfect.remove(notPixelPerfect[d + 1])
d += 1
# Remove notPixelPerfect from path
for i in range(len(notPixelPerfect)):
path.remove(notPixelPerfect[i])
return path
def cleanLine(path): # HERE
"""
Clean and smooth a list of blocks. Works in 2d but supports 3d.
Args:
path (list): List of blocks.
Returns:
list: List cleaned.
TODO:
Do not work perfectly since 16/04/2021.
Add new patterns.
Problem with i -= 10 : solved but not understand why.
16/04/2021.
"""
pathTemp = []
for i in path:
if i not in pathTemp:
pathTemp.append(i)
path = pathTemp
i = 0
while i < len(path):
# 2 blocks, 90 degrees, 2 blocks = 1 block, 1 block, 1 block
if i + 3 < len(path):
if (
path[i][0] == path[i + 1][0]
and path[i + 2][-1] == path[i + 3][-1]
):
if len(path[i + 1]) == 3:
path.insert(
(i + 1),
(path[i + 2][0], path[i + 2][1], path[i + 1][-1]),
)
else:
path.insert((i + 1), (path[i + 2][0], path[i + 1][-1]))
del path[i + 2] # 2nd block
del path[i + 2] # 3rd block
i -= 1
continue
elif (
path[i][-1] == path[i + 1][-1]
and path[i + 2][0] == path[i + 3][0]
):
if len(path[i + 1]) == 3:
path.insert(
(i + 1),
(path[i + 1][0], path[i + 1][1], path[i + 2][-1]),
)
else:
path.insert(
(i + 1),
(path[i + 1][0], path[i + 2][-1]),
)
del path[i + 2] # 2nd block
del path[i + 2] # 3rd block
i -= 1
continue
# 1 block, 3 blocks, 1 block = 1 block, 2 blocks, 2 blocks
if i - 1 >= 0 and i + 5 <= len(path):
if (
(
path[i + 1][-1] == path[i + 2][-1]
and path[i + 2][-1] == path[i + 3][-1]
)
and (
path[i + 1][-1] != path[i][-1]
and path[i + 3][-1] != path[i + 4][-1]
)
and (
path[i - 1][-1] != path[i][-1]
and path[i + 4][-1] != path[i + 5][-1]
)
):
if len(path[i]) == 3:
path.insert(
(i + 1), (path[i + 1][0], path[i + 1][1], path[i][-1])
)
else:
path.insert((i + 1), (path[i + 1][0], path[i][-1]))
del path[i + 2] # 2nd block
i -= 1
continue
elif (
(
path[i + 1][0] == path[i + 2][0]
and path[i + 2][0] == path[i + 3][0]
)
and (
path[i + 1][0] != path[i][0]
and path[i + 3][0] != path[i + 4][0]
)
and (
path[i - 1][0] != path[i][0]
and path[i + 4][0] != path[i + 5][0]
)
):
if len(path[i]) == 3:
path.insert(
(i + 1), (path[i][0], path[i][1], path[i + 1][-1])
)
else:
path.insert((i + 1), (path[i][0], path[i + 1][-1]))
del path[i + 2] # 2nd block
i -= 1
continue
i += 1
return path
def distance2D(A, B): # TODO : Can be better.
return sqrt((B[0] - A[0]) ** 2 + (B[1] - A[1]) ** 2)
def curveSurface(
points,
distance,
resolution=7,
pixelPerfect=False,
factor=2,
start=0,
returnLine=True,
): # HERE
"""
Create a curve with a thickness.
Args:
points (numpy.ndarray): Points where the curve should go.
distance (int): Thickness.
resolution (int, optional): Number of blocks that separate each
point to calculate parallel curves. 0 to use the points
calculated to create the curve. Defaults to 7.
pixelPerfect (bool, optional): True to avoid heaps. Defaults to
False.
factor (int, optional): Number of sub-line that will be
calculated to avoid hole with coordinates. Defaults to 2.
Returns:
dict: Key 0 is the list of coordinates of the center line.
Positive keys are lists of coordinates of lines on the right
side, negative keys are for the left side.
>>> curveSurface(
np.array(
[
[12, 248, -103],
[-5, 219, -85],
[-22, 205, -128],
[-51, 70, -240],
[40, 198, -166],
[19, 241, -102],
[-6, 62, -223],
]
),
5,
resolution=7,
)
"""
if len((points)) >= 3:
# Calculate resolution of the main curve depending of the total curve length.
lenCurve = 0
for i in range(len(points) - 1):
lenCurve += sqrt(
((points[i][0] - points[i + 1][0]) ** 2)
+ ((points[i][1] - points[i + 1][1]) ** 2)
+ ((points[i][2] - points[i + 1][2]) ** 2)
)
number_true_pts = round(lenCurve / 6)
# Calculate the main line.
X, Y, Z = curve(points, number_true_pts)
if len(X) < 2:
X, Y, Z = (
(points[0][0], points[1][0]),
(points[0][1], points[1][1]),
(points[0][2], points[1][2]),
)
else:
X, Y, Z = (
(points[0][0], points[1][0]),
(points[0][1], points[1][1]),
(points[0][2], points[1][2]),
)
centerLineTemp = []
for i in range(len(X) - 1):
xyz0 = X[i], Y[i], Z[i]
xyz1 = (X[i + 1], Y[i + 1], Z[i + 1])
centerLineTemp.extend(line(xyz0, xyz1))
if not returnLine:
returnPoints = []
for i in range(len(X)):
returnPoints.append((round(X[i]), round(Y[i]), round(Z[i])))
# Clean the main line.
# centerLine = cleanLine(centerLineTemp)
centerLine = centerLineTemp
# Offset.
centerPoints = []
if resolution != 0:
for i in range(0, len(centerLine), resolution):
centerPoints.append(centerLine[i])
else:
for i in range(len(X)):
centerPoints.append((X[i], Y[i], Z[i]))
X = [centerPoints[i][0] for i in range(len(centerPoints))]
Y = [centerPoints[i][1] for i in range(len(centerPoints))]
Z = [centerPoints[i][2] for i in range(len(centerPoints))]
rightPoints = []
leftPoints = []
# print(centerLine, "XYZ centerPoint to offset")
# print(cleanLine(centerLineTemp), "with cleanLine")
for i in range(start * factor, distance * factor):
rightPoint, leftPoint = curveOffset(X, Y, Z, i / factor)
rightPoints.append(rightPoint)
leftPoints.append(leftPoint)
rightLine = []
leftLine = []
rightSide = []
leftSide = []
if returnLine == True: # Creating lines on each side between each point.
for i in range(len(rightPoints)):
for j in range(len(rightPoints[i]) - 1):
rightLine.extend(
line(
rightPoints[i][j],
rightPoints[i][j + 1],
pixelPerfect,
)
)
rightSide.append(rightLine)
rightLine = []
for i in range(len(leftPoints)):
for j in range(len(leftPoints[i]) - 1):
leftLine.extend(
line(
leftPoints[i][j],
leftPoints[i][j + 1],
pixelPerfect,
)
)
leftSide.append(leftLine)
leftLine = []
else: # Do not create lines. Points instead.
for i in range(len(rightPoints)):
for j in range(len(rightPoints[i])):
rightLine.append(rightPoints[i][j])
rightSide.append(rightLine)
rightLine = []
for i in range(len(leftPoints)):
for j in range(len(leftPoints[i])):
leftLine.append(leftPoints[i][j])
leftSide.append(leftLine)
leftLine = []
# Returns. 0 is the center line, positive values are lines on the
# right, negative values are lines on the left.
smoothCurveSurfaceDict = {}
if returnLine:
smoothCurveSurfaceDict[0] = centerLine
else:
smoothCurveSurfaceDict[0] = returnPoints
countLine = 0
for l in rightSide:
# l = cleanLine(l)
countLine += 1
smoothCurveSurfaceDict[countLine] = l
countLine = 0
for l in leftSide:
# l = cleanLine(l)
countLine -= 1
smoothCurveSurfaceDict[countLine] = l
return smoothCurveSurfaceDict
def getAngle(xy0, xy1, xy2):
"""
Compute angle (in degrees) for xy0, xy1, xy2 corner.
https://stackoverflow.com/questions/13226038/calculating-angle-between-two-vectors-in-python
Args:
xy0 (numpy.ndarray): Points in the form of [x,y].
xy1 (numpy.ndarray): Points in the form of [x,y].
xy2 (numpy.ndarray): Points in the form of [x,y].
Returns:
float: Angle negative for counterclockwise angle, angle positive
for counterclockwise angle.
"""
if xy2 is None:
xy2 = xy1 + np.array([1, 0])
v0 = np.array(xy0) - np.array(xy1)
v1 = np.array(xy2) - np.array(xy1)
angle = np.math.atan2(np.linalg.det([v0, v1]), np.dot(v0, v1))
return np.degrees(angle)
def lineIntersection(line0, line1, fullLine=True):
"""
Find (or not) intersection between two lines. Works in 2d but
supports 3d.
https://stackoverflow.com/questions/20677795/how-do-i-compute-the-intersection-point-of-two-lines
Args:
line0 (tuple): Tuple of tuple of coordinates.
line1 (tuple): Tuple of tuple of coordinates.
fullLine (bool, optional): True to find intersections along
full line - not just in the segment.
Returns:
tuple: Coordinates (2d).
>>> lineIntersection(((0, 0), (0, 5)), ((2.5, 2.5), (-2.5, 2.5)))
"""
xdiff = (line0[0][0] - line0[1][0], line1[0][0] - line1[1][0])
ydiff = (line0[0][-1] - line0[1][-1], line1[0][-1] - line1[1][-1])
def det(a, b):
return a[0] * b[-1] - a[-1] * b[0]
div = det(xdiff, ydiff)
if div == 0:
return None
d = (det(*line0), det(*line1))
x = det(d, xdiff) / div
y = det(d, ydiff) / div
if not fullLine:
if (
min(line0[0][0], line0[1][0]) <= x <= max(line0[0][0], line0[1][0])
and min(line1[0][0], line1[1][0])
<= x
<= max(line1[0][0], line1[1][0])
and min(line0[0][-1], line0[1][-1])
<= y
<= max(line0[0][-1], line0[1][-1])
and min(line1[0][-1], line1[1][-1])
<= y
<= max(line1[0][-1], line1[1][-1])
):
return x, y
else:
return None
else:
return x, y
def circleLineSegmentIntersection(
circleCenter, circleRadius, xy0, xy1, fullLine=True, tangentTol=1e-9
):
"""
Find the points at which a circle intersects a line-segment. This
can happen at 0, 1, or 2 points. Works in 2d but supports 3d.
https://stackoverflow.com/questions/30844482/what-is-most-efficient-way-to-find-the-intersection-of-a-line-and-a-circle-in-py
Note: We follow: http://mathworld.wolfram.com/Circle-LineIntersection.html
Args:
circleCenter (tuple): The (x, y) location of the circle center.
circleRadius (int): The radius of the circle.
xy0 (tuple): The (x, y) location of the first point of the
segment.
xy1 ([tuple]): The (x, y) location of the second point of the
segment.
fullLine (bool, optional): True to find intersections along
full line - not just in the segment. False will just return
intersections within the segment. Defaults to True.
tangentTol (float, optional): Numerical tolerance at which we
decide the intersections are close enough to consider it a
tangent. Defaults to 1e-9.
Returns:
list: A list of length 0, 1, or 2, where each element is a point
at which the circle intercepts a line segment (2d).
"""
(p1x, p1y), (p2x, p2y), (cx, cy) = (
(xy0[0], xy0[-1]),
(xy1[0], xy1[-1]),
(circleCenter[0], circleCenter[1]),
)
(x1, y1), (x2, y2) = (p1x - cx, p1y - cy), (p2x - cx, p2y - cy)
dx, dy = (x2 - x1), (y2 - y1)
dr = (dx ** 2 + dy ** 2) ** 0.5
big_d = x1 * y2 - x2 * y1
discriminant = circleRadius ** 2 * dr ** 2 - big_d ** 2
if discriminant < 0: # No intersection between circle and line
return []
else: # There may be 0, 1, or 2 intersections with the segment
intersections = [
(
cx
+ (
big_d * dy
+ sign * (-1 if dy < 0 else 1) * dx * discriminant ** 0.5
)
/ dr ** 2,
cy
+ (-big_d * dx + sign * abs(dy) * discriminant ** 0.5)
/ dr ** 2,
)
for sign in ((1, -1) if dy < 0 else (-1, 1))
] # This makes sure the order along the segment is correct
if (
not fullLine
): # If only considering the segment, filter out intersections that do not fall within the segment
fraction_along_segment = [
(xi - p1x) / dx if abs(dx) > abs(dy) else (yi - p1y) / dy
for xi, yi in intersections
]
intersections = [
pt
for pt, frac in zip(intersections, fraction_along_segment)
if 0 <= frac <= 1
]
if (
len(intersections) == 2 and abs(discriminant) <= tangentTol
): # If line is tangent to circle, return just one point (as both intersections have same location)
return [intersections[0]]
else:
return intersections
def circle(xyC, r):
"""
Can be used for circle or disc.
Args:
xyC (tuple): Coordinates of the center.
r (int): Radius of the circle.
Returns:
dict: Keys are distance from the circle. Value is a list of all
coordinates at this distance. 0 for a circle. Negative values
for a disc, positive values for a hole.
"""
area = (
(round(xyC[0]) - round(r), round(xyC[1]) - round(r)),
(round(xyC[0]) + round(r) + 1, round(xyC[1]) + round(r) + 1),
)
circle = {}
for x in range(area[0][0], area[1][0]):
for y in range(area[0][1], area[1][1]):
d = round(distance2D((x, y), (xyC))) - r
if circle.get(d) == None:
circle[d] = []
circle[d].append((x, y))
return circle
def circleIntersections(xy0, r0, xy1, r1):
# https://stackoverflow.com/questions/55816902/finding-the-intersection-of-two-circles
x0, y0 = xy0
x1, y1 = xy1
d = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
# Non intersecting.
if d > r0 + r1:
return None
# One circle within other.
if d < abs(r0 - r1):
return None
# Coincident circles.
if d == 0 and r0 == r1:
return None
else:
a = (r0 ** 2 - r1 ** 2 + d ** 2) / (2 * d)
h = sqrt(r0 ** 2 - a ** 2)
x2 = x0 + a * (x1 - x0) / d
y2 = y0 + a * (y1 - y0) / d
x3 = x2 + h * (y1 - y0) / d
y3 = y2 - h * (x1 - x0) / d
x4 = x2 - h * (y1 - y0) / d
y4 = y2 + h * (x1 - x0) / d
return ((x3, y3), (x4, y4))
def InTriangle(point, xy0, xy1, xy2):
# 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.
dX = point[0] - xy0[0]
dY = point[1] - xy0[1]
dX20 = xy2[0] - xy0[0]
dY20 = xy2[1] - xy0[1]
dX10 = xy1[0] - xy0[0]
dY10 = xy1[1] - xy0[1]
s_p = (dY20 * dX) - (dX20 * dY)
t_p = (dX10 * dY) - (dY10 * dX)
D = (dX10 * dY20) - (dY10 * dX20)
if D > 0:
return (s_p >= 0) and (t_p >= 0) and (s_p + t_p) <= D
else:
return (s_p <= 0) and (t_p <= 0) and (s_p + t_p) >= D
def circlePoints(xyC, r, n=100):
# https://stackoverflow.com/questions/8487893/generate-all-the-points-on-the-circumference-of-a-circle
points = [
(cos(2 * pi / n * x) * r, sin(2 * pi / n * x) * r)
for x in range(0, n + 1)
]
for i in range(len(points)):
points[i] = (
points[i][0] + xyC[0],
points[i][1] + xyC[1],
)
return points
def optimizedPath(coords, start=None):
# https://stackoverflow.com/questions/45829155/sort-points-in-order-to-have-a-continuous-curve-using-python
if start is None:
start = coords[0]
pass_by = coords
path = [start]
pass_by.remove(start)
while pass_by:
nearest = min(pass_by, key=lambda x: distance2D(path[-1], x))
path.append(nearest)
pass_by.remove(nearest)
return path
def nearest(points, start):
return min(points, key=lambda x: distance2D(start, x))
def sortRotation(points):
"""
Sort point in a rotation order. Works in 2d but supports 3d.
https://stackoverflow.com/questions/58377015/counterclockwise-sorting-of-x-y-data
Args:
points: List of points to sort in the form of [(x, y, z), (x, y,
z)] or [(x, y), (x, y), (x, y), (x, y)]...
Returns:
list: List of tuples of coordinates sorted (2d or 3d).
>>> sortRotation([(0, 45, 100), (4, -5, 5),(-5, 36, -2)])
[(0, 45, 100), (-5, 36, -2), (4, -5, 5)]
"""
x, y = [], []
for i in range(len(points)):
x.append(points[i][0])
y.append(points[i][-1])
x, y = np.array(x), np.array(y)
x0 = np.mean(x)
y0 = np.mean(y)
r = np.sqrt((x - x0) ** 2 + (y - y0) ** 2)
angles = np.where(
(y - y0) > 0,
np.arccos((x - x0) / r),
2 * np.pi - np.arccos((x - x0) / r),
)
mask = np.argsort(angles)
x_sorted = list(x[mask])
y_sorted = list(y[mask])
# Rearrange tuples to get the right coordinates.
sortedPoints = []
for i in range(len(points)):
j = 0
while (x_sorted[i] != points[j][0]) and (y_sorted[i] != points[j][-1]):
j += 1
else:
if len(points[0]) == 3:
sortedPoints.append((x_sorted[i], points[j][1], y_sorted[i]))
else:
sortedPoints.append((x_sorted[i], y_sorted[i]))
return sortedPoints
def curveCornerIntersectionPoints(
line0, line1, startDistance, angleAdaptation=False
):
"""
Create points between the two lines to smooth the intersection.
Args:
line0 (tuple): Tuple of tuple. Line coordinates. Order matters.
line1 (tuple): Tuple of tuple. Line coordinates. Order matters.
startDistance (int): distance from the intersection where the
curve should starts.
angleAdaptation (bool, optional): True will adapt the
startDistance depending of the angle between the two lines.
False will force the distance to be startDistance. Defaults to
False.
Returns:
[list]: List of tuple of coordinates (2d) that forms the curve.
Starts on the line and end on the other line.
>>> curveCornerIntersectionPoints(((0, 0), (50, 20)), ((-5, 50), (25, -5)), 10)
"""
intersection = lineIntersection(line0, line1, fullLine=True)
if intersection == None:
return None
# Define automatically the distance from the intersection, where the curve
# starts.
if angleAdaptation:
angle = getAngle(
(line0[0][0], line0[0][-1]),
intersection,
(line1[0][0], line1[0][-1]),
)
# Set here the radius of the circle for a square angle.
startDistance = startDistance * abs(1 / (angle / 90))
startCurvePoint = circleLineSegmentIntersection(
intersection, startDistance, line0[0], intersection, fullLine=True
)[0]
endCurvePoint = circleLineSegmentIntersection(
intersection, startDistance, line1[0], intersection, fullLine=True
)[0]
# Higher value for better precision
perpendicular0 = perpendicular(10e3, startCurvePoint, intersection)[0]
perpendicular1 = perpendicular(10e3, endCurvePoint, intersection)[1]
center = lineIntersection(
(perpendicular0, startCurvePoint), (perpendicular1, endCurvePoint)
)
# Distance with startCurvePoint and endCurvePoint from the center are the
# same.
radius = distance2D(startCurvePoint, center)
circle = circlePoints(
center, round(radius), 32
) # n=round((2 * pi * radius) / 32)
# Find the correct point on the circle.
curveCornerPointsTemp = [startCurvePoint]
for point in circle:
if InTriangle(point, intersection, startCurvePoint, endCurvePoint):
curveCornerPointsTemp.append(point)
curveCornerPointsTemp.append(endCurvePoint)
# Be sure that all the points are in correct order.
curveCornerPoints = optimizedPath(curveCornerPointsTemp, startCurvePoint)
return curveCornerPoints
def curveCornerIntersectionLine(
line0, line1, startDistance, angleAdaptation=False, center=()
):
"""
Create a continuous circular line between the two lines to smooth
the intersection.
Args:
line0 (tuple): Tuple of tuple. Line coordinates. Order matters.
line1 (tuple): Tuple of tuple. Line coordinates. Order matters.
startDistance (int): distance from the intersection where the
curve should starts.
angleAdaptation (bool, optional): True will adapt the
startDistance depending of the angle between the two lines.
False will force the distance to be startDistance. Defaults to
False.
Returns:
[list]: List of tuple of coordinates (2d) that forms the curve.
Starts on the line and end on the other line.
TODO:
angleAdaptation : Set circle radius and not startDistance.
Polar coordinates / Unit circle instead of InTriangle.
>>> curveCornerIntersectionLine(((0, 0), (50, 20)), ((-5, 50), (25, -5)), 10)
"""
intersection = lineIntersection(line0, line1, fullLine=True)
if intersection == None:
return None
# Define automatically the distance from the intersection, where the curve
# starts.
if angleAdaptation:
angle = getAngle(
(line0[0][0], line0[0][-1]),
intersection,
(line1[0][0], line1[0][-1]),
)
# Set here the radius of the circle for a square angle.
startDistance = startDistance * abs(1 / (angle / 90))
startCurvePoint = circleLineSegmentIntersection(
intersection, startDistance, line0[0], intersection, fullLine=True
)[0]
endCurvePoint = circleLineSegmentIntersection(
intersection, startDistance, line1[0], intersection, fullLine=True
)[0]
# Higher value for better precision
perpendicular0 = perpendicular(10e3, startCurvePoint, intersection)[0]
perpendicular1 = perpendicular(10e3, endCurvePoint, intersection)[1]
if center == ():
center = lineIntersection(
(perpendicular0, startCurvePoint), (perpendicular1, endCurvePoint)
)
# Distance with startCurvePoint and endCurvePoint from the center
# are almost the same.
radius = distance2D(startCurvePoint, center)
circleArc = circle(center, round(radius))[0]
# Find the correct point on the circle.
curveCornerPointsTemp = [startCurvePoint]
for point in circleArc:
if InTriangle(point, intersection, startCurvePoint, endCurvePoint):
curveCornerPointsTemp.append(point)
# curveCornerPointsTemp.append(endCurvePoint)
# Be sure that all the points are in correct order.
curveCornerPoints = optimizedPath(curveCornerPointsTemp, startCurvePoint)
return curveCornerPoints, center
def middleLine(xyz0, xyz1):
x = (xyz0[0] + xyz1[0]) / 2
z = (xyz0[-1] + xyz1[-1]) / 2
if len(xyz0) <= 2:
return (x, z)
else:
y = (xyz0[1] + xyz1[1]) / 2
return (round(x), round(y), round(z))