Skip to content

Commit e5b356e

Browse files
committed
Moved 2D geometry helper functions into a separate file.
1 parent ff6c10a commit e5b356e

File tree

2 files changed

+464
-468
lines changed

2 files changed

+464
-468
lines changed

geometryhelpers.py

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
import math
2+
3+
def intersection(cline1, cline2):
4+
"""Return intersection (x,y) of 2 clines expressed as (a,b,c) coeff."""
5+
a, b, c = cline1
6+
d, e, f = cline2
7+
i = b*f-c*e
8+
j = c*d-a*f
9+
k = a*e-b*d
10+
if k:
11+
return (i/k, j/k)
12+
return None
13+
14+
15+
def cnvrt_2pts_to_coef(pt1, pt2):
16+
"""Return (a,b,c) coefficients of cline defined by 2 (x,y) pts."""
17+
x1, y1 = pt1
18+
x2, y2 = pt2
19+
a = y2 - y1
20+
b = x1 - x2
21+
c = x2*y1-x1*y2
22+
return (a, b, c)
23+
24+
25+
def proj_pt_on_line(cline, pt):
26+
"""Return point which is the projection of pt on cline."""
27+
a, b, c = cline
28+
x, y = pt
29+
denom = a**2 + b**2
30+
if not denom:
31+
return pt
32+
xp = (b**2 * x - a*b*y - a*c) / denom
33+
yp = (a**2 * y - a*b*x - b*c) / denom
34+
return (xp, yp)
35+
36+
37+
def pnt_in_box_p(pnt, box):
38+
'''Point in box predicate: Return True if pnt is in box.'''
39+
x, y = pnt
40+
x1, y1, x2, y2 = box
41+
return bool(x1 < x < x2 and y1 < y < y2)
42+
43+
44+
def midpoint(p1, p2, f=.5):
45+
"""Return point part way (f=.5 by def) between points p1 and p2."""
46+
return (((p2[0]-p1[0])*f)+p1[0], ((p2[1]-p1[1])*f)+p1[1])
47+
48+
49+
def p2p_dist(p1, p2):
50+
"""Return the distance between two points"""
51+
x, y = p1
52+
u, v = p2
53+
return math.sqrt((x-u)**2 + (y-v)**2)
54+
55+
56+
def p2p_angle(p0, p1):
57+
"""Return angle (degrees) from p0 to p1."""
58+
return math.atan2(p1[1]-p0[1], p1[0]-p0[0])*180/math.pi
59+
60+
61+
def add_pt(p0, p1):
62+
return (p0[0]+p1[0], p0[1]+p1[1])
63+
64+
65+
def sub_pt(p0, p1):
66+
return (p0[0]-p1[0], p0[1]-p1[1])
67+
68+
69+
def line_circ_inters(x1, y1, x2, y2, xc, yc, r):
70+
'''Return list of intersection pts of line defined by pts x1,y1 and x2,y2
71+
and circle (cntr xc,yc and radius r).
72+
Uses algorithm from Paul Bourke's web page.'''
73+
intpnts = []
74+
num = (xc - x1)*(x2 - x1) + (yc - y1)*(y2 - y1)
75+
denom = (x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1)
76+
if denom == 0:
77+
return
78+
u = num / denom
79+
xp = x1 + u*(x2-x1)
80+
yp = y1 + u*(y2-y1)
81+
82+
a = (x2 - x1)**2 + (y2 - y1)**2
83+
b = 2*((x2-x1)*(x1-xc) + (y2-y1)*(y1-yc))
84+
c = xc**2+yc**2+x1**2+y1**2-2*(xc*x1+yc*y1)-r**2
85+
q = b**2 - 4*a*c
86+
if q == 0:
87+
intpnts.append((xp, yp))
88+
elif q:
89+
u1 = (-b+math.sqrt(abs(q)))/(2*a)
90+
u2 = (-b-math.sqrt(abs(q)))/(2*a)
91+
intpnts.append(((x1 + u1*(x2-x1)), (y1 + u1*(y2-y1))))
92+
intpnts.append(((x1 + u2*(x2-x1)), (y1 + u2*(y2-y1))))
93+
return intpnts
94+
95+
96+
def circ_circ_inters(x1, y1, r1, x2, y2, r2):
97+
'''Return list of intersection pts of 2 circles.
98+
Uses algorithm from Robert S. Wilson's web page.'''
99+
pts = []
100+
D = (x2-x1)**2 + (y2-y1)**2
101+
if not D:
102+
return pts # circles have same cntr; no intersection
103+
q = math.sqrt(abs(((r1+r2)**2-D)*(D-(r2-r1)**2)))
104+
pts = [((x2+x1)/2+(x2-x1)*(r1**2-r2**2)/(2*D)+(y2-y1)*q/(2*D),
105+
(y2+y1)/2+(y2-y1)*(r1**2-r2**2)/(2*D)-(x2-x1)*q/(2*D)),
106+
((x2+x1)/2+(x2-x1)*(r1**2-r2**2)/(2*D)-(y2-y1)*q/(2*D),
107+
(y2+y1)/2+(y2-y1)*(r1**2-r2**2)/(2*D)+(x2-x1)*q/(2*D))]
108+
if same_pt_p(pts[0], pts[1]):
109+
pts.pop() # circles are tangent
110+
return pts
111+
112+
113+
def same_pt_p(p1, p2):
114+
'''Return True if p1 and p2 are within 1e-10 of each other.'''
115+
return bool(p2p_dist(p1, p2) < 1e-6)
116+
117+
118+
def cline_box_intrsctn(cline, box):
119+
"""Return tuple of pts where line intersects edges of box."""
120+
x0, y0, x1, y1 = box
121+
pts = []
122+
segments = [((x0, y0), (x1, y0)),
123+
((x1, y0), (x1, y1)),
124+
((x1, y1), (x0, y1)),
125+
((x0, y1), (x0, y0))]
126+
for seg in segments:
127+
pt = intersection(cline, cnvrt_2pts_to_coef(seg[0], seg[1]))
128+
if pt:
129+
if p2p_dist(pt, seg[0]) <= p2p_dist(seg[0], seg[1]) and \
130+
p2p_dist(pt, seg[1]) <= p2p_dist(seg[0], seg[1]):
131+
if pt not in pts:
132+
pts.append(pt)
133+
return tuple(pts)
134+
135+
136+
def para_line(cline, pt):
137+
"""Return coeff of newline thru pt and parallel to cline."""
138+
a, b, c = cline
139+
x, y = pt
140+
cnew = -(a*x + b*y)
141+
return (a, b, cnew)
142+
143+
144+
def para_lines(cline, d):
145+
"""Return 2 parallel lines straddling line, offset d."""
146+
a, b, c = cline
147+
c1 = math.sqrt(a**2 + b**2)*d
148+
cline1 = (a, b, c + c1)
149+
cline2 = (a, b, c - c1)
150+
return (cline1, cline2)
151+
152+
153+
def perp_line(cline, pt):
154+
"""Return coeff of newline thru pt and perpend to cline."""
155+
a, b, c = cline
156+
x, y = pt
157+
cnew = a*y - b*x
158+
return (b, -a, cnew)
159+
160+
161+
def closer(p0, p1, p2):
162+
"""Return closer of p1 or p2 to point p0."""
163+
d1 = (p1[0] - p0[0])**2 + (p1[1] - p0[1])**2
164+
d2 = (p2[0] - p0[0])**2 + (p2[1] - p0[1])**2
165+
if d1 < d2:
166+
return p1
167+
return p2
168+
169+
170+
def farther(p0, p1, p2):
171+
"""Return farther of p1 or p2 from point p0."""
172+
d1 = (p1[0] - p0[0])**2 + (p1[1] - p0[1])**2
173+
d2 = (p2[0] - p0[0])**2 + (p2[1] - p0[1])**2
174+
if d1 > d2:
175+
return p1
176+
return p2
177+
178+
179+
def find_fillet_pts(r, commonpt, end1, end2):
180+
"""Return ctr of fillet (radius r) and tangent pts for corner
181+
defined by a common pt, and two adjacent corner pts."""
182+
line1 = cnvrt_2pts_to_coef(commonpt, end1)
183+
line2 = cnvrt_2pts_to_coef(commonpt, end2)
184+
# find 'interior' clines
185+
cl1a, cl1b = para_lines(line1, r)
186+
p2a = proj_pt_on_line(cl1a, end2)
187+
p2b = proj_pt_on_line(cl1b, end2)
188+
da = p2p_dist(p2a, end2)
189+
db = p2p_dist(p2b, end2)
190+
if da <= db:
191+
cl1 = cl1a
192+
else:
193+
cl1 = cl1b
194+
cl2a, cl2b = para_lines(line2, r)
195+
p1a = proj_pt_on_line(cl2a, end1)
196+
p1b = proj_pt_on_line(cl2b, end1)
197+
da = p2p_dist(p1a, end1)
198+
db = p2p_dist(p1b, end1)
199+
if da <= db:
200+
cl2 = cl2a
201+
else:
202+
cl2 = cl2b
203+
pc = intersection(cl1, cl2)
204+
p1 = proj_pt_on_line(line1, pc)
205+
p2 = proj_pt_on_line(line2, pc)
206+
return (pc, p1, p2)
207+
208+
209+
def find_common_pt(apair, bpair):
210+
"""Return (common pt, other pt from a, other pt from b), where a and b
211+
are coordinate pt pairs in (p1, p2) format."""
212+
p0, p1 = apair
213+
p2, p3 = bpair
214+
if same_pt_p(p0, p2):
215+
cp = p0 # common pt
216+
opa = p1 # other pt a
217+
opb = p3 # other pt b
218+
elif same_pt_p(p0, p3):
219+
cp = p0
220+
opa = p1
221+
opb = p2
222+
elif same_pt_p(p1, p2):
223+
cp = p1
224+
opa = p0
225+
opb = p3
226+
elif same_pt_p(p1, p3):
227+
cp = p1
228+
opa = p0
229+
opb = p2
230+
else:
231+
return
232+
return (cp, opa, opb)
233+
234+
235+
def cr_from_3p(p1, p2, p3):
236+
"""Return ctr pt and radius of circle on which 3 pts reside.
237+
From Paul Bourke's web page."""
238+
chord1 = cnvrt_2pts_to_coef(p1, p2)
239+
chord2 = cnvrt_2pts_to_coef(p2, p3)
240+
radial_line1 = perp_line(chord1, midpoint(p1, p2))
241+
radial_line2 = perp_line(chord2, midpoint(p2, p3))
242+
ctr = intersection(radial_line1, radial_line2)
243+
if ctr:
244+
radius = p2p_dist(p1, ctr)
245+
return (ctr, radius)
246+
247+
248+
def extendline(p0, p1, d):
249+
"""Return point which lies on extension of line segment p0-p1,
250+
beyond p1 by distance d."""
251+
pts = line_circ_inters(p0[0], p0[1], p1[0], p1[1], p1[0], p1[1], d)
252+
if pts:
253+
return farther(p0, pts[0], pts[1])
254+
return
255+
256+
257+
def shortenline(p0, p1, d):
258+
"""Return point which lies on line segment p0-p1,
259+
short of p1 by distance d."""
260+
pts = line_circ_inters(p0[0], p0[1], p1[0], p1[1], p1[0], p1[1], d)
261+
if pts:
262+
return closer(p0, pts[0], pts[1])
263+
return
264+
265+
266+
def line_tan_to_circ(circ, p):
267+
"""Return tan pts on circ of line through p."""
268+
c, r = circ
269+
d = p2p_dist(c, p)
270+
ang0 = p2p_angle(c, p)*math.pi/180
271+
theta = math.asin(r/d)
272+
ang1 = ang0+math.pi/2-theta
273+
ang2 = ang0-math.pi/2+theta
274+
p1 = (c[0]+(r*math.cos(ang1)), c[1]+(r*math.sin(ang1)))
275+
p2 = (c[0]+(r*math.cos(ang2)), c[1]+(r*math.sin(ang2)))
276+
return (p1, p2)
277+
278+
279+
def line_tan_to_2circs(circ1, circ2):
280+
"""Return tangent pts on line tangent to 2 circles.
281+
Order of circle picks determines which tangent line."""
282+
c1, r1 = circ1
283+
c2, r2 = circ2
284+
d = p2p_dist(c1, c2) # distance between centers
285+
ang_loc = p2p_angle(c2, c1)*math.pi/180 # angle of line of centers
286+
f = (r2/r1-1)/d # reciprocal dist from c1 to intersection of loc & tan line
287+
theta = math.asin(r1*f) # angle between loc and tangent line
288+
ang1 = (ang_loc + math.pi/2 - theta)
289+
# ang2 = (ang_loc - math.pi/2 + theta) # unused
290+
p1 = (c1[0]+(r1*math.cos(ang1)), c1[1]+(r1*math.sin(ang1)))
291+
p2 = (c2[0]+(r2*math.cos(ang1)), c2[1]+(r2*math.sin(ang1)))
292+
return (p1, p2)
293+
294+
295+
def angled_cline(pt, angle):
296+
"""Return cline through pt at angle (degrees)"""
297+
ang = angle * math.pi / 180
298+
dx = math.cos(ang)
299+
dy = math.sin(ang)
300+
p2 = (pt[0]+dx, pt[1]+dy)
301+
cline = cnvrt_2pts_to_coef(pt, p2)
302+
return cline
303+
304+
305+
def ang_bisector(p0, p1, p2, f=0.5):
306+
"""Return cline coefficients of line through vertex p0, factor=f
307+
between p1 and p2."""
308+
ang1 = math.atan2(p1[1]-p0[1], p1[0]-p0[0])
309+
ang2 = math.atan2(p2[1]-p0[1], p2[0]-p0[0])
310+
deltang = ang2 - ang1
311+
ang3 = (f * deltang + ang1)*180/math.pi
312+
return angled_cline(p0, ang3)
313+
314+
315+
def pt_on_RHS_p(pt, p0, p1):
316+
"""Return True if pt is on right hand side going from p0 to p1."""
317+
angline = p2p_angle(p0, p1)
318+
angpt = p2p_angle(p0, pt)
319+
if angline >= 0:
320+
if angline > angpt > angline-180:
321+
return True
322+
else:
323+
angline += 360
324+
if angpt < 0:
325+
angpt += 360
326+
if angline > angpt > angline-180:
327+
return True
328+
329+
330+
def rotate_pt(pt, ang, ctr):
331+
"""Return coordinates of pt rotated ang (deg) CCW about ctr.
332+
333+
This is a 3-step process:
334+
1. translate to place ctr at origin.
335+
2. rotate about origin (CCW version of Paul Bourke's algorithm.
336+
3. apply inverse translation. """
337+
x, y = sub_pt(pt, ctr)
338+
A = ang * math.pi / 180
339+
u = x * math.cos(A) - y * math.sin(A)
340+
v = y * math.cos(A) + x * math.sin(A)
341+
return add_pt((u, v), ctr)
342+

0 commit comments

Comments
 (0)