gerbmerge/gerbmerge/geometry.py

347 lines
14 KiB
Python

#!/usr/bin/env python
"""
General geometry support routines.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import math
# Ensure all list elements are unique
def uniqueify(L):
return list({}.fromkeys(L).keys())
# This function rounds an (X,Y) point to integer co-ordinates
def roundPoint(pt):
return (int(round(pt[0])),int(round(pt[1])))
# Returns True if the segment defined by endpoints p1 and p2 is vertical
def isSegmentVertical(p1, p2):
return p1[0]==p2[0]
# Returns True if the segment defined by endpoints p1 and p2 is horizontal
def isSegmentHorizontal(p1, p2):
return p1[1]==p2[1]
# Returns slope of a non-vertical line segment
def segmentSlope(p1, p2):
return float(p2[1]-p1[1])/(p2[0]-p1[0])
# Determine if the (X,Y) 'point' is on the line segment defined by endpoints p1
# and p2, both (X,Y) tuples. It's assumed that the point is on the line defined
# by the segment, but just may be beyond the endpoints. NOTE: No testing is
# performed to see if the point is actually on the line defined by the segment!
# This is assumed!
def isPointOnSegment(point, p1, p2):
if isSegmentVertical(p1,p2):
# Treat vertical lines by comparing Y-ordinates
return (point[1]-p2[1])*(point[1]-p1[1]) <= 0
else:
# Treat other lines, including horizontal lines, by comparing X-ordinates
return (point[0]-p2[0])*(point[0]-p1[0]) <= 0
# Returns (X,Y) point where the line segment defined by (X,Y) endpoints p1 and
# p2 intersects the line segment defined by endpoints q1 and q2. Only a single
# intersection point is allowed, so no coincident lines. If there is no point
# of intersection, None is returned.
def segmentXsegment1pt(p1, p2, q1, q2):
A,B = p1
C,D = p2
P,Q = q1
R,S = q2
# We have to consider special cases of one or other line segments being vertical
if isSegmentVertical(p1,p2):
if isSegmentVertical(q1,q2): return None
x = A
y = segmentSlope(q1,q2)*(A-P) + Q
elif isSegmentVertical(q1,q2):
x = P
y = segmentSlope(p1,p2)*(P-A) + B
else:
m1 = segmentSlope(p1,p2)
m2 = segmentSlope(q1,q2)
if m1==m2: return None
x = (A*m1 - B - P*m2 + Q) / (m1-m2)
y = m1*(x-A) + B
# Candidate point identified. Check to make sure it's on both line segments.
if isPointOnSegment((x,y), p1, p2) and isPointOnSegment((x,y), q1, q2):
return roundPoint((x,y))
else:
return None
# Returns True if the given (X,Y) 'point' is strictly within the rectangle
# defined by (LLX,LLY,URX,URY) co-ordinates (LL=lower left, UR=upper right).
def isPointStrictlyInRectangle(point, rect):
x,y = point
llx,lly,urx,ury = rect
return (llx < x < urx) and (lly < y < ury)
# This function takes two points which define the extents of a rectangle. The
# return value is a 5-tuple (ll, ul, ur, lr, rect) which comprises 4 points
# (lower-left, upper-left, upper-right, lower-right) and a rect object (minx,
# miny, maxx, maxy). If called with a single argument, it is expected to be
# a 4-tuple (x1,y1,x2,y2).
def canonicalizeExtents(pt1, pt2=None):
# First canonicalize lower-left and upper-right points
if pt2 is None:
maxx = max(pt1[0], pt1[2])
minx = min(pt1[0], pt1[2])
maxy = max(pt1[1], pt1[3])
miny = min(pt1[1], pt1[3])
else:
maxx = max(pt1[0], pt2[0])
minx = min(pt1[0], pt2[0])
maxy = max(pt1[1], pt2[1])
miny = min(pt1[1], pt2[1])
# Construct the four corners
llpt = (minx,miny)
urpt = (maxx,maxy)
ulpt = (minx,maxy)
lrpt = (maxx,miny)
# Construct a rect object for use by various functions
rect = (minx, miny, maxx, maxy)
return (llpt, ulpt, urpt, lrpt, rect)
# This function returns a list of intersection points of the line segment
# pt1-->pt2 and the box defined by corners llpt and urpt. These corners are
# canonicalized internally so they need not necessarily be lower-left and
# upper-right points.
#
# The return value may be a list of 0, 1, or 2 points. If the list has 2
# points, then the segment intersects the box in two points since both points
# are outside the box. If the list has 1 point, then the segment has one point
# inside the box and another point outside. If the list is empty, the segment
# has both points outside the box and there is no intersection, or has both
# points inside the box.
#
# Note that segments collinear with box edges produce no points of
# intersection.
def segmentXbox(pt1, pt2, llpt, urpt):
# First canonicalize lower-left and upper-right points
llpt, ulpt, urpt, lrpt, rect = canonicalizeExtents(llpt, urpt)
# Determine whether one point is inside the rectangle and the other is not.
# Note the XOR operator '^'
oneInOneOut = isPointStrictlyInRectangle(pt1,rect) ^ isPointStrictlyInRectangle(pt2,rect)
# Find all intersections of the segment with the 4 sides of the box,
# one side at a time. L will be the list of definitely-true intersections,
# while corners is a list of potential intersections. An intersection
# is potential if a) it is a corner, and b) there is another intersection
# of the line with the box somewhere else. This is how we handle
# corner intersections, which are sometimes legal (when one segment endpoint
# is inside the box and the other isn't, or when the segment intersects the
# box in two places) and sometimes not (when the segment is "tangent" to
# the box at the corner and the corner is the signle point of intersection).
L = []
corners = []
# Do not allow intersection if segment is collinear with box sides. For
# example, a horizontal line collinear with the box top side should not
# return an intersection with the upper-left or upper-right corner.
# Similarly, a point of intersection that is a corner should only be
# allowed if one segment point is inside the box and the other is not,
# otherwise it means the segment is "tangent" to the box at that corner.
# There is a case, however, in which a corner is a point of intersection
# with both segment points outside the box, and that is if there are two
# points of intersection, i.e., the segment goes completely through the box.
def checkIntersection(corner1, corner2):
# Check intersection with side of box
pt = segmentXsegment1pt(pt1, pt2, corner1, corner2)
if pt in (corner1,corner2):
# Only allow this corner intersection point if line is not
# horizontal/vertical and one point is inside rectangle while other is
# not, or the segment intersects the box in two places. Since oneInOneOut
# calls isPointStrictlyInRectangle(), which automatically excludes points
# on the box itself, horizontal/vertical lines collinear with box sides
# will always lead to oneInOneOut==False (since both will be "out of
# box").
if oneInOneOut:
L.append(pt)
else:
corners.append(pt) # Potentially a point of intersection...we'll have to wait and
# see if there is one more point of intersection somewhere else.
else:
# Not a corner intersection, so it's valid
if pt is not None: L.append(pt)
# Check intersection with left side of box
checkIntersection(llpt, ulpt)
# Check intersection with top side of box
checkIntersection(ulpt, urpt)
# Check intersection with right side of box
checkIntersection(urpt, lrpt)
# Check intersection with bottom side of box
checkIntersection(llpt, lrpt)
# Ensure all points are unique. We may get a double hit at the corners
# of the box.
L = uniqueify(L)
corners = uniqueify(corners)
# If the total number of intersections len(L)+len(corners) is 2, the corner
# is valid. If there is only a single corner, it's a tangent and invalid.
# However, if both corners are on the same side of the box, it's not valid.
numPts = len(L)+len(corners)
assert numPts <= 2
if numPts == 2:
if len(corners)==2 and (isSegmentHorizontal(corners[0], corners[1]) or isSegmentVertical(corners[0],corners[1])):
return []
else:
L += corners
L.sort() # Just for stability in assertion checking
return L
else:
L.sort()
return L # Correct if numPts==1, since it will be empty or contain a single valid intersection
# Correct if numPts==0, since it will be empty
# This function determines if two rectangles defined by 4-tuples
# (minx, miny, maxx, maxy) have any rectangle in common. If so, it is
# returned as a 4-tuple, else None is returned. This function assumes
# the rectangles are canonical so that minx<maxx, miny<maxy. If the
# optional allowLines parameter is True, rectangles that overlap on
# a line are considered overlapping, otherwise they must overlap with
# a rectangle of at least width 1.
def areExtentsOverlapping(E1, E2, allowLines=False):
minX,minY,maxX,maxY = E1
minU,minV,maxU,maxV = E2
if allowLines:
if (minU > maxX) or (maxU < minX) or (minV > maxY) or (maxV < minY):
return False
else:
return True
else:
if (minU >= maxX) or (maxU <= minX) or (minV >= maxY) or (maxV <= minY):
return False
else:
return True
# Compute the intersection of two rectangles defined by 4-tuples E1 and E2,
# which are not necessarily canonicalized.
def intersectExtents(E1, E2):
ll1, ul1, ur1, lr1, rect1 = canonicalizeExtents(E1)
ll2, ul2, ur2, lr2, rect2 = canonicalizeExtents(E2)
if not areExtentsOverlapping(rect1, rect2):
return None
xll = max(rect1[0], rect2[0]) # Maximum of minx values
yll = max(rect1[1], rect2[1]) # Maximum of miny values
xur = min(rect1[2], rect2[2]) # Minimum of maxx values
yur = min(rect1[3], rect2[3]) # Minimum of maxy values
return (xll, yll, xur, yur)
# This function returns True if rectangle E1 is wholly contained within
# rectangle E2. Both E1 and E2 are 4-tuples (minx,miny,maxx,maxy), not
# necessarily canonicalized. This function is like a slightly faster
# version of "intersectExtents(E1, E2)==E1".
def isRect1InRect2(E1, E2):
ll1, ul1, ur1, lr1, rect1 = canonicalizeExtents(E1)
ll2, ul2, ur2, lr2, rect2 = canonicalizeExtents(E2)
return (ll1[0] >= ll2[0]) and (ll1[1] >= ll2[1]) \
and (ur1[0] <= ur2[0]) and (ur1[1] <= ur2[1])
# Return width of rectangle, which may be 0 if bottom-left and upper-right X
# positions are the same. The rectangle is a 4-tuple (minx,miny,maxx,maxy).
def rectWidth(rect):
return abs(rect[2]-rect[0])
# Return height of rectangle, which may be 0 if bottom-left and upper-right Y
# positions are the same. The rectangle is a 4-tuple (minx,miny,maxx,maxy).
def rectHeight(rect):
return abs(rect[3]-rect[1])
# Return center (X,Y) co-ordinates of rectangle.
def rectCenter(rect):
dx = rectWidth(rect)
dy = rectHeight(rect)
if dx & 1: # Odd width: center is (left+right)/2 + 1/2
X = (rect[0] + rect[2] + 1)/2
else: # Even width: center is (left+right)/2
X = (rect[0] + rect[2])/2
if dy & 1:
Y = (rect[1] + rect[3] + 1)/2
else:
Y = (rect[1] + rect[3])/2
return (X,Y)
if __name__=="__main__":
llpt = (1000,1000)
urpt = (5000,5000)
# A segment that cuts across the box and intersects in corners
assert segmentXbox((0,0), (6000,6000), llpt, urpt) == [(1000,1000), (5000,5000)] # Two valid corners
assert segmentXbox((0,6000), (6000,0), llpt, urpt) == [(1000,5000), (5000,1000)] # Two valid corners
assert segmentXbox((500,500), (2500, 2500), llpt, urpt) == [(1000,1000)] # One valid corner
assert segmentXbox((2500,2500), (5500, 5500), llpt, urpt) == [(5000,5000)] # One valid corner
# Segments collinear with box sides
assert segmentXbox((1000,0), (1000,6000), llpt, urpt) == [] # Box side contained in segment
assert segmentXbox((1000,0), (1000,3000), llpt, urpt) == [] # Box side partially overlaps segment
assert segmentXbox((1000,2000), (1000,4000), llpt, urpt) == [] # Segment contained in box side
# Segments fully contained within box
assert segmentXbox((1500,2000), (2000,2500), llpt, urpt) == []
# Segments with points on box sides
assert segmentXbox((2500,1000), (2700,1200), llpt, urpt) == [(2500,1000)] # One point on box side
assert segmentXbox((2500,1000), (2700,5000), llpt, urpt) == [(2500,1000), (2700,5000)] # Two points on box sides
# Segment intersects box at one point
assert segmentXbox((3500,5500), (3000, 2500), llpt, urpt) == [(3417, 5000)] # First point outside
assert segmentXbox((3500,1500), (3000, 6500), llpt, urpt) == [(3150, 5000)] # Second point outside
# Segment intersects box at two points, not corners
assert segmentXbox((500,3000), (1500,500), llpt, urpt) == [(1000,1750), (1300,1000)]
assert segmentXbox((2500,300), (5500,3500), llpt, urpt) == [(3156,1000), (5000,2967)]
assert segmentXbox((5200,1200), (2000,6000), llpt, urpt) == [(2667,5000), (5000, 1500)]
assert segmentXbox((3200,5200), (-10, 1200), llpt, urpt) == [(1000, 2459), (3040, 5000)]
assert segmentXbox((500,2000), (5500, 2000), llpt, urpt) == [(1000,2000), (5000, 2000)]
assert segmentXbox((5200,1250), (-200, 4800), llpt, urpt) == [(1000, 4011), (5000, 1381)]
assert segmentXbox((1300,200), (1300, 5200), llpt, urpt) == [(1300, 1000), (1300, 5000)]
assert segmentXbox((1200,200), (1300, 5200), llpt, urpt) == [(1216, 1000), (1296, 5000)]
assert intersectExtents( (100,100,500,500), (500,500,900,900) ) == None
assert intersectExtents( (100,100,500,500), (400,400,900,900) ) == (400,400,500,500)
assert intersectExtents( (100,100,500,500), (200,0,600,300) ) == (200,100,500,300)
assert intersectExtents( (100,100,500,500), (200,0,300,600) ) == (200,100,300,500)
assert intersectExtents( (100,100,500,500), (0,600,50,550) ) == None
assert intersectExtents( (100,100,500,500), (0,600,600,-10) ) == (100,100,500,500)
assert intersectExtents( (100,100,500,500), (0,600,600,200) ) == (100,200,500,500)
assert intersectExtents( (100,100,500,500), (0,600,300,300) ) == (100,300,300,500)
assert isRect1InRect2( (100,100,500,500), (0,600,50,550) ) == False
assert isRect1InRect2( (100,100,500,500), (0,600,600,-10) ) == True
assert isRect1InRect2( (100,100,500,500), (0,600,600,200) ) == False
assert isRect1InRect2( (100,100,500,500), (0,600,300,300) ) == False
assert isRect1InRect2( (100,100,500,500), (0,0,500,500) ) == True
print('All tests pass')