Skip to content

Commit d0789e5

Browse files
committed
New library
1 parent 7d43d10 commit d0789e5

1 file changed

Lines changed: 245 additions & 0 deletions

File tree

vicutils/geometry/geometry.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,247 @@
1+
# Geometry Library - Reusable classes for geometric operations
12

3+
from functools import cache
4+
import bisect
25

6+
7+
class Line:
8+
"""Represents an infinite line in 2D space using equation ax + by = c"""
9+
10+
def __init__(self, p1, p2, epsilon=1e-3):
11+
"""
12+
Initialize a line through two points.
13+
14+
Args:
15+
p1: (x, y) tuple - first point
16+
p2: (x, y) tuple - second point
17+
epsilon: tolerance for floating point comparisons
18+
"""
19+
self.epsilon = epsilon
20+
x1, y1 = p1
21+
x2, y2 = p2
22+
23+
if x1 == x2:
24+
# Vertical line: x = x1
25+
self.a, self.b, self.c = 1, 0, x1
26+
else:
27+
# General line: y = ax + b -> ax - y = -b
28+
a = (y1 - y2) / (x1 - x2)
29+
b = (x2 * y1 - x1 * y2) / (x2 - x1)
30+
self.a, self.b, self.c = a, -1, -b
31+
32+
self.line = self
33+
34+
def has(self, p):
35+
"""Check if point p lies on this line"""
36+
if not p:
37+
return False
38+
x, y = p
39+
return abs(x * self.a + y * self.b - self.c) <= self.epsilon
40+
41+
def inter(self, other):
42+
"""
43+
Find intersection point with another line.
44+
45+
Returns:
46+
(x, y) tuple if lines intersect, None if parallel
47+
"""
48+
a, b, p = self.a, self.b, self.c
49+
c, d, q = other.a, other.b, other.c
50+
51+
det = a * d - b * c
52+
if abs(det) <= self.epsilon:
53+
return None # Lines are parallel
54+
55+
# Solve system using Cramer's rule
56+
a, b, c, d = d, -b, -c, a
57+
x = a * p + b * q
58+
y = c * p + d * q
59+
return (x / det, y / det)
60+
61+
62+
class Ray:
63+
"""Represents a line segment (bounded portion of a line)"""
64+
65+
def __init__(self, line, condition):
66+
"""
67+
Initialize a ray with a line and boundary condition.
68+
69+
Args:
70+
line: Line object
71+
condition: function(p) -> bool that checks if point is within bounds
72+
"""
73+
self.line = line
74+
self.condition = condition
75+
76+
def has(self, p):
77+
"""Check if point p lies on this ray (on line AND within bounds)"""
78+
if not p:
79+
return False
80+
if not self.line.has(p):
81+
return False
82+
return self.condition(p)
83+
84+
def inter(self, ray):
85+
"""
86+
Check if this ray intersects with another ray.
87+
88+
Returns:
89+
True if rays intersect, False otherwise
90+
"""
91+
inter = self.line.inter(ray.line)
92+
if not inter:
93+
return False
94+
return self.has(inter) and ray.has(inter)
95+
96+
97+
class Polygon:
98+
"""Represents a polygon defined by its vertices"""
99+
100+
def __init__(self, vertices, precision=2, epsilon=1e-3):
101+
"""
102+
Initialize a polygon with vertices.
103+
104+
Args:
105+
vertices: List of (x, y) tuples (can be floats)
106+
precision: Number of decimal digits (e.g., 2 for 0.01 precision)
107+
Coordinates are scaled by 10^precision to work with integers
108+
epsilon: tolerance for floating point comparisons in Line operations
109+
"""
110+
self.precision = precision
111+
self.multiplier = 10 ** precision
112+
self.epsilon = epsilon
113+
114+
# Extract unique x and y coordinates, sort them, and create mappings
115+
xs = sorted(set(x for x, y in vertices))
116+
ys = sorted(set(y for x, y in vertices))
117+
118+
# Store mappings for converting back to original coordinates
119+
self.xs = xs
120+
self.ys = ys
121+
122+
# Convert vertices to indices (scaled integer coordinates)
123+
indexedVertices = [(xs.index(x), ys.index(y)) for x, y in vertices]
124+
125+
# Build edges as rays using indexed coordinates
126+
self.edges = []
127+
n = len(indexedVertices)
128+
129+
for i in range(n):
130+
x1, y1 = indexedVertices[i]
131+
x2, y2 = indexedVertices[(i + 1) % n]
132+
133+
line = Line((x1, y1), (x2, y2), epsilon=self.epsilon)
134+
135+
# Create condition for ray based on edge direction
136+
if x1 == x2:
137+
# Vertical edge: y must be in range
138+
def condition(p, Y1=y1, Y2=y2):
139+
_, y = p
140+
return min(Y1, Y2) <= y <= max(Y1, Y2)
141+
elif y1 == y2:
142+
# Horizontal edge: x must be in range
143+
def condition(p, X1=x1, X2=x2):
144+
x, _ = p
145+
return min(X1, X2) <= x <= max(X1, X2)
146+
else:
147+
# Diagonal edge: both x and y must be in range
148+
def condition(p, X1=x1, X2=x2, Y1=y1, Y2=y2):
149+
x, y = p
150+
return (min(X1, X2) <= x <= max(X1, X2) and
151+
min(Y1, Y2) <= y <= max(Y1, Y2))
152+
153+
self.edges.append(Ray(line, condition))
154+
155+
def addCoords(self, coords):
156+
"""
157+
Pre-add a list of coordinates to the mappings.
158+
Useful to add many coordinates at once efficiently.
159+
160+
Args:
161+
coords: List of (x, y) tuples to add to the coordinate mappings
162+
"""
163+
# Extract all unique x and y values from coords
164+
newXs = set(x for x, y in coords)
165+
newYs = set(y for x, y in coords)
166+
167+
# Merge and sort if there are new coordinates
168+
xsChanged = False
169+
ysChanged = False
170+
171+
for x in newXs:
172+
if x not in self.xs:
173+
xsChanged = True
174+
break
175+
176+
for y in newYs:
177+
if y not in self.ys:
178+
ysChanged = True
179+
break
180+
181+
if xsChanged:
182+
self.xs = sorted(set(self.xs) | newXs)
183+
if ysChanged:
184+
self.ys = sorted(set(self.ys) | newYs)
185+
186+
def _toIndexed(self, p):
187+
"""
188+
Convert a point in original coordinates to indexed coordinates.
189+
If the coordinate doesn't exist, it's inserted in sorted order.
190+
191+
Args:
192+
p: (x, y) tuple in original coordinate system
193+
194+
Returns:
195+
(xIdx, yIdx) tuple in indexed coordinate system
196+
"""
197+
xOrig, yOrig = p
198+
199+
# Find or insert x coordinate using binary search
200+
xIdx = bisect.bisect_left(self.xs, xOrig)
201+
if xIdx >= len(self.xs) or self.xs[xIdx] != xOrig:
202+
self.xs.insert(xIdx, xOrig)
203+
204+
# Find or insert y coordinate using binary search
205+
yIdx = bisect.bisect_left(self.ys, yOrig)
206+
if yIdx >= len(self.ys) or self.ys[yIdx] != yOrig:
207+
self.ys.insert(yIdx, yOrig)
208+
209+
return (xIdx, yIdx)
210+
211+
@cache
212+
def inside(self, p):
213+
"""
214+
Check if point p is inside the polygon using ray casting algorithm.
215+
216+
Args:
217+
p: (x, y) tuple in original coordinate system
218+
219+
Returns:
220+
True if point is inside polygon, False otherwise
221+
"""
222+
# Convert original coordinates to indexed coordinates
223+
x, y = self._toIndexed(p)
224+
225+
# Cast rays in four cardinal directions from the point
226+
# A point is inside if all four rays intersect at least one edge
227+
rays = [
228+
Ray(Line((x, y), (x - 10, y), epsilon=self.epsilon),
229+
lambda p1, X=x: p1[0] <= X), # Left
230+
Ray(Line((x, y), (x, y + 10), epsilon=self.epsilon),
231+
lambda p1, Y=y: p1[1] >= Y), # Up
232+
Ray(Line((x, y), (x + 10, y), epsilon=self.epsilon),
233+
lambda p1, X=x: p1[0] >= X), # Right
234+
Ray(Line((x, y), (x, y - 10), epsilon=self.epsilon),
235+
lambda p1, Y=y: p1[1] <= Y) # Down
236+
]
237+
238+
hits = [0, 0, 0, 0]
239+
for edge in self.edges:
240+
for i, ray in enumerate(rays):
241+
if ray.inter(edge):
242+
hits[i] = 1
243+
# Early exit if all rays hit
244+
if sum(hits) == 4:
245+
return True
246+
247+
return sum(hits) == 4

0 commit comments

Comments
 (0)