You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
13 KiB
Python
362 lines
13 KiB
Python
7 months ago
|
from math import acos, asin, atan2, cos, pi, sin, sqrt
|
||
|
import numpy as np
|
||
|
import sys
|
||
|
|
||
|
try:
|
||
|
GLOBAL_DIR = sys._MEIPASS # temporary folder with libs & data files
|
||
|
except:
|
||
|
GLOBAL_DIR = "."
|
||
|
|
||
|
|
||
|
class Math_Ops():
|
||
|
'''
|
||
|
This class provides general mathematical operations that are not directly available through numpy
|
||
|
'''
|
||
|
|
||
|
@staticmethod
|
||
|
def deg_sph2cart(spherical_vec):
|
||
|
''' Converts SimSpark's spherical coordinates in degrees to cartesian coordinates '''
|
||
|
r = spherical_vec[0]
|
||
|
h = spherical_vec[1] * pi / 180
|
||
|
v = spherical_vec[2] * pi / 180
|
||
|
return np.array([r * cos(v) * cos(h), r * cos(v) * sin(h), r * sin(v)])
|
||
|
|
||
|
@staticmethod
|
||
|
def deg_sin(deg_angle):
|
||
|
''' Returns sin of degrees '''
|
||
|
return sin(deg_angle * pi / 180)
|
||
|
|
||
|
@staticmethod
|
||
|
def deg_cos(deg_angle):
|
||
|
''' Returns cos of degrees '''
|
||
|
return cos(deg_angle * pi / 180)
|
||
|
|
||
|
@staticmethod
|
||
|
def to_3d(vec_2d, value=0) -> np.ndarray:
|
||
|
''' Returns new 3d vector from 2d vector '''
|
||
|
return np.append(vec_2d,value)
|
||
|
|
||
|
@staticmethod
|
||
|
def to_2d_as_3d(vec_3d) -> np.ndarray:
|
||
|
''' Returns new 3d vector where the 3rd dimension is zero '''
|
||
|
vec_2d_as_3d = np.copy(vec_3d)
|
||
|
vec_2d_as_3d[2] = 0
|
||
|
return vec_2d_as_3d
|
||
|
|
||
|
@staticmethod
|
||
|
def normalize_vec(vec) -> np.ndarray:
|
||
|
''' Divides vector by its length '''
|
||
|
size = np.linalg.norm(vec)
|
||
|
if size == 0: return vec
|
||
|
return vec / size
|
||
|
|
||
|
@staticmethod
|
||
|
def get_active_directory(dir:str) -> str:
|
||
|
global GLOBAL_DIR
|
||
|
return GLOBAL_DIR + dir
|
||
|
|
||
|
@staticmethod
|
||
|
def acos(val):
|
||
|
''' arccosine function that limits input '''
|
||
|
return acos( np.clip(val,-1,1) )
|
||
|
|
||
|
@staticmethod
|
||
|
def asin(val):
|
||
|
''' arcsine function that limits input '''
|
||
|
return asin( np.clip(val,-1,1) )
|
||
|
|
||
|
@staticmethod
|
||
|
def normalize_deg(val):
|
||
|
''' normalize val in range [-180,180[ '''
|
||
|
return (val + 180.0) % 360 - 180
|
||
|
|
||
|
@staticmethod
|
||
|
def normalize_rad(val):
|
||
|
''' normalize val in range [-pi,pi[ '''
|
||
|
return (val + pi) % (2*pi) - pi
|
||
|
|
||
|
@staticmethod
|
||
|
def deg_to_rad(val):
|
||
|
''' convert degrees to radians '''
|
||
|
return val * 0.01745329251994330
|
||
|
|
||
|
@staticmethod
|
||
|
def rad_to_deg(val):
|
||
|
''' convert radians to degrees '''
|
||
|
return val * 57.29577951308232
|
||
|
|
||
|
@staticmethod
|
||
|
def vector_angle(vector, is_rad=False):
|
||
|
''' angle (degrees or radians) of 2D vector '''
|
||
|
if is_rad:
|
||
|
return atan2(vector[1], vector[0])
|
||
|
else:
|
||
|
return atan2(vector[1], vector[0]) * 180 / pi
|
||
|
|
||
|
@staticmethod
|
||
|
def vectors_angle(vec1, vec2, is_rad=False):
|
||
|
''' get angle between vectors (degrees or radians) '''
|
||
|
ang_rad = acos(np.dot(Math_Ops.normalize_vec(vec1),Math_Ops.normalize_vec(vec2)))
|
||
|
return ang_rad if is_rad else ang_rad * 180 / pi
|
||
|
|
||
|
@staticmethod
|
||
|
def vector_from_angle(angle, is_rad=False):
|
||
|
''' unit vector with direction given by `angle` '''
|
||
|
if is_rad:
|
||
|
return np.array([cos(angle), sin(angle)], float)
|
||
|
else:
|
||
|
return np.array([Math_Ops.deg_cos(angle), Math_Ops.deg_sin(angle)], float)
|
||
|
|
||
|
@staticmethod
|
||
|
def target_abs_angle(pos2d, target, is_rad=False):
|
||
|
''' angle (degrees or radians) of vector (target-pos2d) '''
|
||
|
if is_rad:
|
||
|
return atan2(target[1]-pos2d[1], target[0]-pos2d[0])
|
||
|
else:
|
||
|
return atan2(target[1]-pos2d[1], target[0]-pos2d[0]) * 180 / pi
|
||
|
|
||
|
@staticmethod
|
||
|
def target_rel_angle(pos2d, ori, target, is_rad=False):
|
||
|
''' relative angle (degrees or radians) of target if we're located at 'pos2d' with orientation 'ori' (degrees or radians) '''
|
||
|
if is_rad:
|
||
|
return Math_Ops.normalize_rad( atan2(target[1]-pos2d[1], target[0]-pos2d[0]) - ori )
|
||
|
else:
|
||
|
return Math_Ops.normalize_deg( atan2(target[1]-pos2d[1], target[0]-pos2d[0]) * 180 / pi - ori )
|
||
|
|
||
|
@staticmethod
|
||
|
def rotate_2d_vec(vec, angle, is_rad=False):
|
||
|
''' rotate 2D vector anticlockwise around the origin by `angle` '''
|
||
|
cos_ang = cos(angle) if is_rad else cos(angle * pi / 180)
|
||
|
sin_ang = sin(angle) if is_rad else sin(angle * pi / 180)
|
||
|
return np.array([cos_ang*vec[0]-sin_ang*vec[1], sin_ang*vec[0]+cos_ang*vec[1]])
|
||
|
|
||
|
@staticmethod
|
||
|
def distance_point_to_line(p:np.ndarray, a:np.ndarray, b:np.ndarray):
|
||
|
'''
|
||
|
Distance between point p and 2d line 'ab' (and side where p is)
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
a : ndarray
|
||
|
2D point that defines line
|
||
|
b : ndarray
|
||
|
2D point that defines line
|
||
|
p : ndarray
|
||
|
2D point
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
distance : float
|
||
|
distance between line and point
|
||
|
side : str
|
||
|
if we are at a, looking at b, p may be at our "left" or "right"
|
||
|
'''
|
||
|
line_len = np.linalg.norm(b-a)
|
||
|
|
||
|
if line_len == 0: # assumes vertical line
|
||
|
dist = sdist = np.linalg.norm(p-a)
|
||
|
else:
|
||
|
sdist = np.cross(b-a,p-a)/line_len
|
||
|
dist = abs(sdist)
|
||
|
|
||
|
return dist, "left" if sdist>0 else "right"
|
||
|
|
||
|
@staticmethod
|
||
|
def distance_point_to_segment(p:np.ndarray, a:np.ndarray, b:np.ndarray):
|
||
|
''' Distance from point p to 2d line segment 'ab' '''
|
||
|
|
||
|
ap = p-a
|
||
|
ab = b-a
|
||
|
|
||
|
ad = Math_Ops.vector_projection(ap,ab)
|
||
|
|
||
|
# Is d in ab? We can find k in (ad = k * ab) without computing any norm
|
||
|
# we use the largest dimension of ab to avoid division by 0
|
||
|
k = ad[0]/ab[0] if abs(ab[0])>abs(ab[1]) else ad[1]/ab[1]
|
||
|
|
||
|
if k <= 0: return np.linalg.norm(ap)
|
||
|
elif k >= 1: return np.linalg.norm(p-b)
|
||
|
else: return np.linalg.norm(p-(ad + a)) # p-d
|
||
|
|
||
|
@staticmethod
|
||
|
def distance_point_to_ray(p:np.ndarray, ray_start:np.ndarray, ray_direction:np.ndarray):
|
||
|
''' Distance from point p to 2d ray '''
|
||
|
|
||
|
rp = p-ray_start
|
||
|
rd = Math_Ops.vector_projection(rp,ray_direction)
|
||
|
|
||
|
# Is d in ray? We can find k in (rd = k * ray_direction) without computing any norm
|
||
|
# we use the largest dimension of ray_direction to avoid division by 0
|
||
|
k = rd[0]/ray_direction[0] if abs(ray_direction[0])>abs(ray_direction[1]) else rd[1]/ray_direction[1]
|
||
|
|
||
|
if k <= 0: return np.linalg.norm(rp)
|
||
|
else: return np.linalg.norm(p-(rd + ray_start)) # p-d
|
||
|
|
||
|
@staticmethod
|
||
|
def closest_point_on_ray_to_point(p:np.ndarray, ray_start:np.ndarray, ray_direction:np.ndarray):
|
||
|
''' Point on ray closest to point p '''
|
||
|
|
||
|
rp = p-ray_start
|
||
|
rd = Math_Ops.vector_projection(rp,ray_direction)
|
||
|
|
||
|
# Is d in ray? We can find k in (rd = k * ray_direction) without computing any norm
|
||
|
# we use the largest dimension of ray_direction to avoid division by 0
|
||
|
k = rd[0]/ray_direction[0] if abs(ray_direction[0])>abs(ray_direction[1]) else rd[1]/ray_direction[1]
|
||
|
|
||
|
if k <= 0: return ray_start
|
||
|
else: return rd + ray_start
|
||
|
|
||
|
@staticmethod
|
||
|
def does_circle_intersect_segment(p:np.ndarray, r, a:np.ndarray, b:np.ndarray):
|
||
|
''' Returns true if circle (center p, radius r) intersect 2d line segment '''
|
||
|
|
||
|
ap = p-a
|
||
|
ab = b-a
|
||
|
|
||
|
ad = Math_Ops.vector_projection(ap,ab)
|
||
|
|
||
|
# Is d in ab? We can find k in (ad = k * ab) without computing any norm
|
||
|
# we use the largest dimension of ab to avoid division by 0
|
||
|
k = ad[0]/ab[0] if abs(ab[0])>abs(ab[1]) else ad[1]/ab[1]
|
||
|
|
||
|
if k <= 0: return np.dot(ap,ap) <= r*r
|
||
|
elif k >= 1: return np.dot(p-b,p-b) <= r*r
|
||
|
|
||
|
dp = p-(ad + a)
|
||
|
return np.dot(dp,dp) <= r*r
|
||
|
|
||
|
@staticmethod
|
||
|
def vector_projection(a:np.ndarray, b:np.ndarray):
|
||
|
''' Vector projection of a onto b '''
|
||
|
b_dot = np.dot(b,b)
|
||
|
return b * np.dot(a,b) / b_dot if b_dot != 0 else b
|
||
|
|
||
|
@staticmethod
|
||
|
def do_noncollinear_segments_intersect(a,b,c,d):
|
||
|
'''
|
||
|
Check if 2d line segment 'ab' intersects with noncollinear 2d line segment 'cd'
|
||
|
Explanation: https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
|
||
|
'''
|
||
|
|
||
|
ccw = lambda a,b,c: (c[1]-a[1]) * (b[0]-a[0]) > (b[1]-a[1]) * (c[0]-a[0])
|
||
|
return ccw(a,c,d) != ccw(b,c,d) and ccw(a,b,c) != ccw(a,b,d)
|
||
|
|
||
|
@staticmethod
|
||
|
def intersection_segment_opp_goal(a:np.ndarray, b:np.ndarray):
|
||
|
''' Computes the intersection point of 2d segment 'ab' and the opponents' goal (front line) '''
|
||
|
vec_x = b[0]-a[0]
|
||
|
|
||
|
# Collinear intersections are not accepted
|
||
|
if vec_x == 0: return None
|
||
|
|
||
|
k = (15.01-a[0])/vec_x
|
||
|
|
||
|
# No collision
|
||
|
if k < 0 or k > 1: return None
|
||
|
|
||
|
intersection_pt = a + (b-a) * k
|
||
|
|
||
|
if -1.01 <= intersection_pt[1] <= 1.01:
|
||
|
return intersection_pt
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
@staticmethod
|
||
|
def intersection_circle_opp_goal(p:np.ndarray, r):
|
||
|
'''
|
||
|
Computes the intersection segment of circle (center p, radius r) and the opponents' goal (front line)
|
||
|
Only the y coordinates are returned since the x coordinates are always equal to 15
|
||
|
'''
|
||
|
|
||
|
x_dev = abs(15-p[0])
|
||
|
|
||
|
if x_dev > r:
|
||
|
return None # no intersection with x=15
|
||
|
|
||
|
y_dev = sqrt(r*r - x_dev*x_dev)
|
||
|
|
||
|
p1 = max(p[1] - y_dev, -1.01)
|
||
|
p2 = min(p[1] + y_dev, 1.01)
|
||
|
|
||
|
if p1 == p2:
|
||
|
return p1 # return the y coordinate of a single intersection point
|
||
|
elif p2 < p1:
|
||
|
return None # no intersection
|
||
|
else:
|
||
|
return p1, p2 # return the y coordinates of the intersection segment
|
||
|
|
||
|
|
||
|
@staticmethod
|
||
|
def distance_point_to_opp_goal(p:np.ndarray):
|
||
|
''' Distance between point 'p' and the opponents' goal (front line) '''
|
||
|
|
||
|
if p[1] < -1.01:
|
||
|
return np.linalg.norm( p-(15,-1.01) )
|
||
|
elif p[1] > 1.01:
|
||
|
return np.linalg.norm( p-(15, 1.01) )
|
||
|
else:
|
||
|
return abs(15-p[0])
|
||
|
|
||
|
|
||
|
@staticmethod
|
||
|
def circle_line_segment_intersection(circle_center, circle_radius, pt1, pt2, full_line=True, tangent_tol=1e-9):
|
||
|
""" Find the points at which a circle intersects a line-segment. This can happen at 0, 1, or 2 points.
|
||
|
|
||
|
:param circle_center: The (x, y) location of the circle center
|
||
|
:param circle_radius: The radius of the circle
|
||
|
:param pt1: The (x, y) location of the first point of the segment
|
||
|
:param pt2: The (x, y) location of the second point of the segment
|
||
|
:param full_line: True to find intersections along full line - not just in the segment. False will just return intersections within the segment.
|
||
|
:param tangent_tol: Numerical tolerance at which we decide the intersections are close enough to consider it a tangent
|
||
|
:return Sequence[Tuple[float, float]]: A list of length 0, 1, or 2, where each element is a point at which the circle intercepts a line segment.
|
||
|
|
||
|
Note: We follow: http://mathworld.wolfram.com/Circle-LineIntersection.html
|
||
|
"""
|
||
|
|
||
|
(p1x, p1y), (p2x, p2y), (cx, cy) = pt1, pt2, circle_center
|
||
|
(x1, y1), (x2, y2) = (p1x - cx, p1y - cy), (p2x - cx, p2y - cy)
|
||
|
dx, dy = (x2 - x1), (y2 - y1)
|
||
|
dr = (dx ** 2 + dy ** 2)**.5
|
||
|
big_d = x1 * y2 - x2 * y1
|
||
|
discriminant = circle_radius ** 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**.5) / dr ** 2,
|
||
|
cy + (-big_d * dx + sign * abs(dy) * discriminant**.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 full_line: # 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 line is tangent to circle, return just one point (as both intersections have same location)
|
||
|
if len(intersections) == 2 and abs(discriminant) <= tangent_tol:
|
||
|
return [intersections[0]]
|
||
|
else:
|
||
|
return intersections
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
# adapted from https://stackoverflow.com/questions/3252194/numpy-and-line-intersections
|
||
|
@staticmethod
|
||
|
def get_line_intersection(a1, a2, b1, b2):
|
||
|
"""
|
||
|
Returns the point of intersection of the lines passing through a2,a1 and b2,b1.
|
||
|
a1: [x, y] a point on the first line
|
||
|
a2: [x, y] another point on the first line
|
||
|
b1: [x, y] a point on the second line
|
||
|
b2: [x, y] another point on the second line
|
||
|
"""
|
||
|
s = np.vstack([a1,a2,b1,b2]) # s for stacked
|
||
|
h = np.hstack((s, np.ones((4, 1)))) # h for homogeneous
|
||
|
l1 = np.cross(h[0], h[1]) # get first line
|
||
|
l2 = np.cross(h[2], h[3]) # get second line
|
||
|
x, y, z = np.cross(l1, l2) # point of intersection
|
||
|
if z == 0: # lines are parallel
|
||
|
return np.array([float('inf'), float('inf')])
|
||
|
return np.array([x/z, y/z],float)
|