# coding=utf-8
"""Module for boolean operations on 2D polygons (union, intersection, difference, xor).
The functions here are derived from the pypolybool python library available at
https://github.com/KaivnD/pypolybool
The pypolybool library is, itself, a pure Python port of the polybooljs JavaScript
library maintained by Sean Mconnelly available at https://github.com/velipso/polybooljs
Full documentation of the method is available at
https://sean.cm/a/polygon-clipping-pt2
Based somewhat on the F. Martinez (2013) algorithm.
Francisco MartÃnez, Carlos Ogayar, Juan R. Jiménez, Antonio J. Rueda (2013),
"A simple algorithm for Boolean operations on polygons",
Advances in Engineering Software, Volume 64, Pages 11-19, ISSN 0965-9978,
https://doi.org/10.1016/j.advengsoft.2013.04.004.
"""
from __future__ import division
"""____________OBJECTS FOR INPUT/OUTPUT FROM BOOLEAN OPERATIONS____________"""
[docs]
class BooleanPoint:
"""2D Point class used in polygon boolean operations.
Args:
x: Float for X coordinate.
y: Float for Y coordinate
"""
def __init__(self, x, y):
self.x = x
self.y = y
[docs]
def is_equivalent(self, other_pt, tol):
"""Check if this point is equivalent ot another within tolerance."""
if not isinstance(other_pt, BooleanPoint):
return False
return abs(self.x - other_pt.x) < tol and abs(self.y - other_pt.y) < tol
[docs]
@staticmethod
def collinear(pt1, pt2, pt3, tolerance):
"""Get a boolean for whether 3 points are colinear."""
dx1 = pt1.x - pt2.x
dy1 = pt1.y - pt2.y
dx2 = pt2.x - pt3.x
dy2 = pt2.y - pt3.y
return abs(dx1 * dy2 - dx2 * dy1) < tolerance
[docs]
@staticmethod
def compare(pt1, pt2, tolerance):
"""Get an integer for the relationship between two points.
Zero indicates equal. Positive 1 is to the right. Negative 1 is to the left.
"""
if abs(pt1.x - pt2.x) < tolerance:
return 0 if abs(pt1.y - pt2.y) < tolerance else -1 if pt1.y < pt2.y else 1
return -1 if pt1.x < pt2.x else 1
[docs]
@staticmethod
def point_above_or_on_line(point, left, right, tolerance):
"""Get a boolean for whether a point is above or on a line.
Args:
point: BooleanPoint to be evaluated.
left: BooleanPoint for the left of the line segment.
right: BooleanPoint for the right of the line segment
"""
return (right.x - left.x) * (point.y - left.y) - (right.y - left.y) * (
point.x - left.x
) >= -tolerance
[docs]
@staticmethod
def between(point, left, right, tolerance):
"""Get a boolean for whether a point is between two points.
Args:
point: BooleanPoint to be evaluated.
left: BooleanPoint for the left.
right: BooleanPoint for the right.
"""
dPyLy = point.y - left.y
dRxLx = right.x - left.x
dPxLx = point.x - left.x
dRyLy = right.y - left.y
dot = dPxLx * dRxLx + dPyLy * dRyLy
if dot < tolerance:
return False
sqlen = dRxLx * dRxLx + dRyLy * dRyLy
if dot - sqlen > -tolerance:
return False
return True
@staticmethod
def _lines_intersect(a0, a1, b0, b1, tolerance):
"""Get an _IntersectionPoint object for the intersection of two line segments.
Args:
a0: BooleanPoint for the first point of the first line segment.
a1: BooleanPoint for the second point of the first line segment.
b0: BooleanPoint for the first point of the second line segment.
b1: BooleanPoint for the second point of the second line segment.
"""
adx = a1.x - a0.x
ady = a1.y - a0.y
bdx = b1.x - b0.x
bdy = b1.y - b0.y
axb = adx * bdy - ady * bdx
if abs(axb) < tolerance:
return None
dx = a0.x - b0.x
dy = a0.y - b0.y
a = (bdx * dy - bdy * dx) / axb
b = (adx * dy - ady * dx) / axb
return _IntersectionPoint(
BooleanPoint.__calc_along_using_value(a, tolerance),
BooleanPoint.__calc_along_using_value(b, tolerance),
BooleanPoint(a0.x + a * adx, a0.y + a * ady),
)
@staticmethod
def __calc_along_using_value(value, tolerance):
if value <= -tolerance:
return -2
elif value < tolerance:
return -1
elif value - 1 <= -tolerance:
return 0
elif value - 1 < tolerance:
return 1
else:
return 2
def __repr__(self):
return "{},{}".format(self.x, self.y)
def __str__(self):
return "{},{}".format(self.x, self.y)
[docs]
class BooleanPolygon:
"""Polygon class used in polygon boolean operations.
Args:
regions: A list of lists of BooleanPoints representing the 2D points defining
the regions of the Polygon. The first sub-list is typically the
boundary of the polygon and each successive list represents a hole
within the boundary. It is also permissable for the holes
to lie outside the first polygon, in which case the shape is
interpreted as a MultiPolygon. As an alternative to BooleanPoints,
tuples of two float values are also permissable in which case the
values represent the X and Y coordinates of each vertex.
is_inverted: A boolean for whether the Polygon is inverted or not. For
polygons input to the boolean methods, this value should always be
False. (Default: False)
"""
def __init__(self, regions, is_inverted=False):
_regions = []
for region in regions:
tmp = []
for pt in region:
if isinstance(pt, BooleanPoint):
tmp.append(pt)
elif isinstance(pt, tuple):
x, y = pt
tmp.append(BooleanPoint(x, y))
_regions.append(tmp)
self.regions = _regions
self.is_inverted = is_inverted
"""____________PRIMARY COMPUTATION CLASSES AND METHODS____________"""
class _Fill:
def __init__(self, below=None, above=None):
self.below = below
self.above = above
def __repr__(self):
return "{},{}".format(self.above, self.below)
def __str__(self):
return "{},{}".format(self.above, self.below)
class _Segment:
def __init__(self, start, end, myfill=None, otherfill=None):
self.start = start
self.end = end
self.myfill = myfill
self.otherfill = otherfill
def __repr__(self):
return "S: {}, E: {}".format(self.start, self.end)
def __str__(self):
return "S: {}, E: {}".format(self.start, self.end)
class _PolySegments:
def __init__(self, segments=None, is_inverted=False):
self.segments = segments
self.is_inverted = is_inverted
class _CombinedPolySegments:
def __init__(self, combined=None, is_inverted1=False, is_inverted2=False):
self.combined = combined
self.is_inverted1 = is_inverted1
self.is_inverted2 = is_inverted2
class _Matcher:
def __init__(self, index, matchesHead, matchesPt1):
self.index = index
self.matchesHead = matchesHead
self.matchesPt1 = matchesPt1
class _IntersectionPoint:
def __init__(self, alongA, alongB, pt):
self.alongA = alongA
self.alongB = alongB
self.pt = pt
class _Node:
def __init__(
self, isRoot=False, isStart=False, pt=None, seg=None, primary=False,
next=None, previous=None, other=None, ev=None, status=None, remove=None,
):
self.status = status
self.other = other
self.ev = ev
self.previous = previous
self.next = next
self.isRoot = isRoot
self.remove = remove
self.isStart = isStart
self.pt = pt
self.seg = seg
self.primary = primary
class _Transition:
def __init__(self, after, before, insert):
self.after = after
self.before = before
self.insert = insert
class _LinkedList:
def __init__(self):
self.__root = _Node(isRoot=True)
def exists(self, node):
if node is None or node is self.__root:
return False
return True
def isEmpty(self):
return self.__root.next is None
def getHead(self):
return self.__root.next
def insertBefore(self, node, check):
last = self.__root
here = self.__root.next
while here is not None:
if check(here):
node.previous = here.previous
node.next = here
here.previous.next = node
here.previous = node
return
last = here
here = here.next
last.next = node
node.previous = last
node.next = None
def findTransition(self, check):
previous = self.__root
here = self.__root.next
while here is not None:
if check(here):
break
previous = here
here = here.next
def insert_func(node):
node.previous = previous
node.next = here
previous.next = node
if here is not None:
here.previous = node
return node
return _Transition(
before=(None if previous is self.__root else previous),
after=here,
insert=insert_func,
)
@staticmethod
def node(data):
data.previous = None
data.next = None
def remove_func():
data.previous.next = data.next
if data.next is not None:
data.next.previous = data.previous
data.previous = None
data.next = None
data.remove = remove_func
return data
class _Intersecter:
"""Primary intersection class."""
def __init__(self, selfIntersection, tol):
self.selfIntersection = selfIntersection
self.tol = tol
self.__eventRoot = _LinkedList()
def newsegment(self, start, end):
return _Segment(start=start, end=end, myfill=_Fill())
def segmentCopy(self, start, end, seg):
return _Segment(
start=start, end=end, myfill=_Fill(seg.myfill.below, seg.myfill.above)
)
def __eventCompare(self, p1IsStart, p11, p12, p2IsStart, p21, p22):
comp = BooleanPoint.compare(p11, p21, self.tol)
if comp != 0:
return comp
if p12.is_equivalent(p22, self.tol):
return 0
if p1IsStart != p2IsStart:
return 1 if p1IsStart else -1
return (
1
if BooleanPoint.point_above_or_on_line(
p12, p21 if p2IsStart else p22, p22 if p2IsStart else p21, self.tol
)
else -1
)
def __eventAdd(self, ev, otherPt):
def check_func(here):
comp = self.__eventCompare(
ev.isStart, ev.pt, otherPt, here.isStart, here.pt, here.other.pt
)
return comp < 0
self.__eventRoot.insertBefore(ev, check_func)
def __eventAddSegmentStart(self, segment, primary):
evStart = _LinkedList.node(
_Node(
isStart=True,
pt=segment.start,
seg=segment,
primary=primary,
)
)
self.__eventAdd(evStart, segment.end)
return evStart
def __eventAddSegmentEnd(self, evStart, segment, primary):
evEnd = _LinkedList.node(
_Node(
isStart=False,
pt=segment.end,
seg=segment,
primary=primary,
other=evStart,
)
)
evStart.other = evEnd
self.__eventAdd(evEnd, evStart.pt)
def eventAddSegment(self, segment, primary):
evStart = self.__eventAddSegmentStart(segment, primary)
self.__eventAddSegmentEnd(evStart, segment, primary)
return evStart
def __eventUpdateEnd(self, ev, end):
ev.other.remove()
ev.seg.end = end
ev.other.pt = end
self.__eventAdd(ev.other, ev.pt)
def __eventDivide(self, ev, pt):
ns = self.segmentCopy(pt, ev.seg.end, ev.seg)
self.__eventUpdateEnd(ev, pt)
return self.eventAddSegment(ns, ev.primary)
def __statusCompare(self, ev1, ev2):
a1 = ev1.seg.start
a2 = ev1.seg.end
b1 = ev2.seg.start
b2 = ev2.seg.end
if BooleanPoint.collinear(a1, b1, b2, self.tol):
if BooleanPoint.collinear(a2, b1, b2, self.tol):
return 1
return 1 if BooleanPoint.point_above_or_on_line(a2, b1, b2, self.tol) else -1
return 1 if BooleanPoint.point_above_or_on_line(a1, b1, b2, self.tol) else -1
def __statusFindSurrounding(self, statusRoot, ev):
def check_func(here):
return self.__statusCompare(ev, here.ev) > 0
return statusRoot.findTransition(check_func)
def __checkIntersection(self, ev1, ev2):
seg1 = ev1.seg
seg2 = ev2.seg
a1 = seg1.start
a2 = seg1.end
b1 = seg2.start
b2 = seg2.end
i = BooleanPoint._lines_intersect(a1, a2, b1, b2, self.tol)
if i is None:
if not BooleanPoint.collinear(a1, a2, b1, self.tol):
return None
if a1.is_equivalent(b2, self.tol) or a2.is_equivalent(b1, self.tol):
return None
a1EquB1 = a1.is_equivalent(b1, self.tol)
a2EquB2 = a2.is_equivalent(b2, self.tol)
if a1EquB1 and a2EquB2:
return ev2
a1Between = not a1EquB1 and BooleanPoint.between(a1, b1, b2, self.tol)
a2Between = not a2EquB2 and BooleanPoint.between(a2, b1, b2, self.tol)
if a1EquB1:
if a2Between:
self.__eventDivide(ev2, a2)
else:
self.__eventDivide(ev1, b2)
return ev2
elif a1Between:
if not a2EquB2:
if a2Between:
self.__eventDivide(ev2, a2)
else:
self.__eventDivide(ev1, b2)
self.__eventDivide(ev2, a1)
else:
if i.alongA == 0:
if i.alongB == -1:
self.__eventDivide(ev1, b1)
elif i.alongB == 0:
self.__eventDivide(ev1, i.pt)
elif i.alongB == 1:
self.__eventDivide(ev1, b2)
if i.alongB == 0:
if i.alongA == -1:
self.__eventDivide(ev2, a1)
elif i.alongA == 0:
self.__eventDivide(ev2, i.pt)
elif i.alongA == 1:
self.__eventDivide(ev2, a2)
return None
def __checkBothIntersections(self, above, ev, below):
if above is not None:
eve = self.__checkIntersection(ev, above)
if eve is not None:
return eve
if below is not None:
return self.__checkIntersection(ev, below)
return None
def calculate(self, primaryPolyInverted, secondaryPolyInverted):
statusRoot = _LinkedList()
segments = []
cnt = 0
while not self.__eventRoot.isEmpty():
cnt += 1
ev = self.__eventRoot.getHead()
if ev.isStart:
surrounding = self.__statusFindSurrounding(statusRoot, ev)
above = (
surrounding.before.ev if surrounding.before is not None else None
)
below = surrounding.after.ev if surrounding.after is not None else None
eve = self.__checkBothIntersections(above, ev, below)
if eve is not None:
if self.selfIntersection:
toggle = False
if ev.seg.myfill.below is None:
toggle = True
else:
toggle = ev.seg.myfill.above != ev.seg.myfill.below
if toggle:
eve.seg.myfill.above = not eve.seg.myfill.above
else:
eve.seg.otherfill = ev.seg.myfill
ev.other.remove()
ev.remove()
if self.__eventRoot.getHead() is not ev:
continue
if self.selfIntersection:
toggle = False
if ev.seg.myfill.below is None:
toggle = True
else:
toggle = ev.seg.myfill.above != ev.seg.myfill.below
if below is None:
ev.seg.myfill.below = primaryPolyInverted
else:
ev.seg.myfill.below = below.seg.myfill.above
if toggle:
ev.seg.myfill.above = not ev.seg.myfill.below
else:
ev.seg.myfill.above = ev.seg.myfill.below
else:
if ev.seg.otherfill is None:
inside = False
if below is None:
inside = (
secondaryPolyInverted
if ev.primary
else primaryPolyInverted
)
else:
if ev.primary == below.primary:
inside = below.seg.otherfill.above
else:
inside = below.seg.myfill.above
ev.seg.otherfill = _Fill(inside, inside)
ev.other.status = surrounding.insert(_LinkedList.node(_Node(ev=ev)))
else:
st = ev.status
if st is None:
raise Exception(
'PolyBool: Zero-length segment detected; '
'your tolerance is probably too small or too large'
)
if statusRoot.exists(st.previous) and statusRoot.exists(st.next):
self.__checkIntersection(st.previous.ev, st.next.ev)
st.remove()
if not ev.primary:
s = ev.seg.myfill
ev.seg.myfill = ev.seg.otherfill
ev.seg.otherfill = s
segments.append(ev.seg)
self.__eventRoot.getHead().remove()
return segments
class _RegionIntersecter(_Intersecter):
def __init__(self, tol):
_Intersecter.__init__(self, True, tol)
def addRegion(self, region):
pt2 = region[-1]
for i in range(len(region)):
pt1 = pt2
pt2 = region[i]
forward = BooleanPoint.compare(pt1, pt2, self.tol)
if forward == 0:
continue
seg = self.newsegment(
pt1 if forward < 0 else pt2, pt2 if forward < 0 else pt1
)
self.eventAddSegment(seg, True)
def calculate(self, inverted):
return _Intersecter.calculate(self, inverted, False)
class _SegmentIntersecter(_Intersecter):
def __init__(self, tol):
_Intersecter.__init__(self, False, tol)
def calculate(
self, segments1, is_inverted1, segments2, is_inverted2
):
for seg in segments1:
self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), True)
for seg in segments2:
self.eventAddSegment(self.segmentCopy(seg.start, seg.end, seg), False)
return _Intersecter.calculate(self, is_inverted1, is_inverted2)
class _SegmentChainerMatcher:
def __init__(self):
self.firstMatch = _Matcher(0, False, False)
self.secondMatch = _Matcher(0, False, False)
self.nextMatch = self.firstMatch
def setMatch(self, index, matchesHead, matchesPt1):
self.nextMatch.index = index
self.nextMatch.matchesHead = matchesHead
self.nextMatch.matchesPt1 = matchesPt1
if self.nextMatch is self.firstMatch:
self.nextMatch = self.secondMatch
return False
self.nextMatch = None
return True
def _list_shift(list):
list.pop(0)
def _list_pop(list):
list.pop()
def _list_splice(list, index, count):
del list[index: index + count]
def _list_unshift(list, element):
list.insert(0, element)
def _segmentChainer(segments, tol):
regions = []
chains = []
for seg in segments:
pt1 = seg.start
pt2 = seg.end
if pt1.is_equivalent(pt2, tol):
continue
scm = _SegmentChainerMatcher()
for i in range(len(chains)):
chain = chains[i]
head = chain[0]
tail = chain[-1]
if head.is_equivalent(pt1, tol):
if scm.setMatch(i, True, True):
break
elif head.is_equivalent(pt2, tol):
if scm.setMatch(i, True, False):
break
elif tail.is_equivalent(pt1, tol):
if scm.setMatch(i, False, True):
break
elif tail.is_equivalent(pt2, tol):
if scm.setMatch(i, False, False):
break
if scm.nextMatch is scm.firstMatch:
chains.append([pt1, pt2])
continue
if scm.nextMatch is scm.secondMatch:
index = scm.firstMatch.index
pt = pt2 if scm.firstMatch.matchesPt1 else pt1
addToHead = scm.firstMatch.matchesHead
chain = chains[index]
grow = chain[0] if addToHead else chain[-1]
grow2 = chain[1] if addToHead else chain[-2]
oppo = chain[-1] if addToHead else chain[0]
oppo2 = chain[-2] if addToHead else chain[1]
if BooleanPoint.collinear(grow2, grow, pt, tol):
if addToHead:
_list_shift(chain)
else:
_list_pop(chain)
grow = grow2
if oppo.is_equivalent(pt, tol):
_list_splice(chains, index, 1)
if BooleanPoint.collinear(oppo2, oppo, grow, tol):
if addToHead:
_list_pop(chain)
else:
_list_shift(chain)
regions.append(chain)
continue
if addToHead:
_list_unshift(chain, pt)
else:
chain.append(pt)
continue
def reverseChain(index):
chains[index].reverse()
def appendChain(index1, index2):
chain1 = chains[index1]
chain2 = chains[index2]
tail = chain1[-1]
tail2 = chain1[-2]
head = chain2[0]
head2 = chain2[1]
if BooleanPoint.collinear(tail2, tail, head, tol):
_list_pop(chain1)
tail = tail2
if BooleanPoint.collinear(tail, head, head2, tol):
_list_shift(chain2)
chains[index1] = chain1 + chain2
_list_splice(chains, index2, 1)
f = scm.firstMatch.index
s = scm.secondMatch.index
reverseF = len(chains[f]) < len(chains[s])
if scm.firstMatch.matchesHead:
if scm.secondMatch.matchesHead:
if reverseF:
reverseChain(f)
appendChain(f, s)
else:
reverseChain(s)
appendChain(s, f)
else:
appendChain(s, f)
else:
if scm.secondMatch.matchesHead:
appendChain(f, s)
else:
if reverseF:
reverseChain(f)
appendChain(s, f)
else:
reverseChain(s)
appendChain(f, s)
return regions
def __select(segments, selection):
result = []
for seg in segments:
index = (
(8 if seg.myfill.above else 0)
+ (4 if seg.myfill.below else 0)
+ (2 if seg.otherfill is not None and seg.otherfill.above else 0)
+ (1 if seg.otherfill is not None and seg.otherfill.below else 0)
)
if selection[index] != 0:
result.append(
_Segment(
start=seg.start,
end=seg.end,
myfill=_Fill(selection[index] == 2, above=selection[index] == 1),
)
)
return result
"""____________CORE INTERFACE FOR MANAGING INTERSECTIONS____________"""
def _segments(poly, tol):
"""Get the intersected PolySegments of a BooleanPolygon.
Args:
poly: A BooleanPolygon for which PolySegments will be computed.
tol: The intersection tolerance.
"""
i = _RegionIntersecter(tol)
for region in poly.regions:
i.addRegion(region)
return _PolySegments(i.calculate(poly.is_inverted), poly.is_inverted)
def _combine(segments1, segments2, tol):
"""Combine intersected PolySegments into a CombinedPolySegments object.
Args:
segments1: The first PolySegments object to be combined.
segments2: The second PolySegments to be combined.
tol: The intersection tolerance.
"""
i = _SegmentIntersecter(tol)
return _CombinedPolySegments(
i.calculate(
segments1.segments,
segments1.is_inverted,
segments2.segments,
segments2.is_inverted,
),
segments1.is_inverted,
segments2.is_inverted,
)
def _select_union(polyseg):
"""Select the union from the PolySegments.
above1 below1 above2 below2 Keep? Value
0 0 0 0 => no 0
0 0 0 1 => yes filled below 2
0 0 1 0 => yes filled above 1
0 0 1 1 => no 0
0 1 0 0 => yes filled below 2
0 1 0 1 => yes filled below 2
0 1 1 0 => no 0
0 1 1 1 => no 0
1 0 0 0 => yes filled above 1
1 0 0 1 => no 0
1 0 1 0 => yes filled above 1
1 0 1 1 => no 0
1 1 0 0 => no 0
1 1 0 1 => no 0
1 1 1 0 => no 0
1 1 1 1 => no 0
"""
return _PolySegments(
segments=__select(
# fmt:off
polyseg.combined, [
0, 2, 1, 0,
2, 2, 0, 0,
1, 0, 1, 0,
0, 0, 0, 0,
]
# fmt:on
),
is_inverted=(polyseg.is_inverted1 or polyseg.is_inverted2),
)
def _select_intersect(polyseg):
"""Select the intersection from the PolySegments.
above1 below1 above2 below2 Keep? Value
0 0 0 0 => no 0
0 0 0 1 => no 0
0 0 1 0 => no 0
0 0 1 1 => no 0
0 1 0 0 => no 0
0 1 0 1 => yes filled below 2
0 1 1 0 => no 0
0 1 1 1 => yes filled below 2
1 0 0 0 => no 0
1 0 0 1 => no 0
1 0 1 0 => yes filled above 1
1 0 1 1 => yes filled above 1
1 1 0 0 => no 0
1 1 0 1 => yes filled below 2
1 1 1 0 => yes filled above 1
1 1 1 1 => no 0
"""
return _PolySegments(
segments=__select(
# fmt:off
polyseg.combined, [
0, 0, 0, 0,
0, 2, 0, 2,
0, 0, 1, 1,
0, 2, 1, 0
]
# fmt:on
),
is_inverted=(polyseg.is_inverted1 and polyseg.is_inverted2),
)
def _select_difference(polyseg):
"""Select the difference from the PolySegments.
above1 below1 above2 below2 Keep? Value
0 0 0 0 => no 0
0 0 0 1 => no 0
0 0 1 0 => no 0
0 0 1 1 => no 0
0 1 0 0 => yes filled below 2
0 1 0 1 => no 0
0 1 1 0 => yes filled below 2
0 1 1 1 => no 0
1 0 0 0 => yes filled above 1
1 0 0 1 => yes filled above 1
1 0 1 0 => no 0
1 0 1 1 => no 0
1 1 0 0 => no 0
1 1 0 1 => yes filled above 1
1 1 1 0 => yes filled below 2
1 1 1 1 => no 0
"""
return _PolySegments(
segments=__select(
# fmt:off
polyseg.combined, [
0, 0, 0, 0,
2, 0, 2, 0,
1, 1, 0, 0,
0, 1, 2, 0
]
# fmt:on
),
is_inverted=(polyseg.is_inverted1 and not polyseg.is_inverted2),
)
def _select_difference_rev(polyseg):
"""Select the reversed difference from the PolySegments.
above1 below1 above2 below2 Keep? Value
0 0 0 0 => no 0
0 0 0 1 => yes filled below 2
0 0 1 0 => yes filled above 1
0 0 1 1 => no 0
0 1 0 0 => no 0
0 1 0 1 => no 0
0 1 1 0 => yes filled above 1
0 1 1 1 => yes filled above 1
1 0 0 0 => no 0
1 0 0 1 => yes filled below 2
1 0 1 0 => no 0
1 0 1 1 => yes filled below 2
1 1 0 0 => no 0
1 1 0 1 => no 0
1 1 1 0 => no 0
1 1 1 1 => no 0
"""
return _PolySegments(
segments=__select(
# fmt:off
polyseg.combined, [
0, 2, 1, 0,
0, 0, 1, 1,
0, 2, 0, 2,
0, 0, 0, 0
]
# fmt:on
),
is_inverted=(not polyseg.is_inverted1 and polyseg.is_inverted2),
)
def _select_xor(polyseg):
"""Select the exclusive disjunction from the PolySegments.
above1 below1 above2 below2 Keep? Value
0 0 0 0 => no 0
0 0 0 1 => yes filled below 2
0 0 1 0 => yes filled above 1
0 0 1 1 => no 0
0 1 0 0 => yes filled below 2
0 1 0 1 => no 0
0 1 1 0 => no 0
0 1 1 1 => yes filled above 1
1 0 0 0 => yes filled above 1
1 0 0 1 => no 0
1 0 1 0 => no 0
1 0 1 1 => yes filled below 2
1 1 0 0 => no 0
1 1 0 1 => yes filled above 1
1 1 1 0 => yes filled below 2
1 1 1 1 => no 0
"""
return _PolySegments(
segments=__select(
# fmt:off
polyseg.combined, [
0, 2, 1, 0,
2, 0, 0, 1,
1, 0, 0, 2,
0, 1, 2, 0
]
# fmt:on
),
is_inverted=(polyseg.is_inverted1 != polyseg.is_inverted2),
)
def _polygon(segments, tol):
return BooleanPolygon(_segmentChainer(segments.segments, tol), segments.is_inverted)
def __operate(poly1, poly2, selector, tol):
firstPolygonRegions = _segments(poly1, tol)
secondPolygonRegions = _segments(poly2, tol)
combinedSegments = _combine(firstPolygonRegions, secondPolygonRegions, tol)
seg = selector(combinedSegments)
return _polygon(seg, tol)
"""____________PUBLIC FUNCTIONS FOR BOOLEAN OPERATIONS____________"""
[docs]
def union_all(polygons, tolerance):
"""Get a BooleanPolygon for the union of multiple polygons.
Using this method is more computationally efficient than calling the union()
method multiple times as this method will only compute the intersection of
the segments once.
Args:
polygons: An array of BooleanPolygons for which the union will be computed.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A BooleanPolygon for the union across all of the input polygons.
"""
seg1 = _segments(polygons[0], tolerance)
for i in range(1, len(polygons)):
seg2 = _segments(polygons[i], tolerance)
comb = _combine(seg1, seg2, tolerance)
seg1 = _select_union(comb)
return _polygon(seg1, tolerance)
[docs]
def intersect_all(polygons, tolerance):
"""Get a BooleanPolygon for the intersection of multiple polygons.
Using this method is more computationally efficient than calling the intersect()
method multiple times as this method will only compute the intersection of
the segments once.
Args:
polygons: An array of BooleanPolygons for which the intersection will
be computed.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A BooleanPolygon for the intersection across all of the input polygons.
"""
seg1 = _segments(polygons[0], tolerance)
for i in range(1, len(polygons)):
seg2 = _segments(polygons[i], tolerance)
comb = _combine(seg1, seg2, tolerance)
seg1 = _select_intersect(comb)
return _polygon(seg1, tolerance)
[docs]
def split(poly1, poly2, tolerance):
"""Split two BooleanPolygons with one another to get the intersection and difference.
Using this method is more computationally efficient than calling the intersect()
and difference() methods individually as this method will only compute the
intersection of the segments once.
Args:
poly1: A BooleanPolygon for the first polygon that will be split with
the second polygon.
poly2: A BooleanPolygon for the second polygon that will be split with
the first polygon.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A tuple with three elements
- intersection: A BooleanPolygon for the intersection of the two
input polygons.
- poly1_difference: A BooleanPolygon for the portion of poly1 that does
not overlap with poly2. When combined with the intersection, this
makes a split version of poly1.
- poly2_difference: A BooleanPolygon for the portion of poly2 that does
not overlap with poly1. When combined with the intersection, this
makes a split version of poly2.
"""
first_regions = _segments(poly1, tolerance)
second_regions = _segments(poly2, tolerance)
comb = _combine(first_regions, second_regions, tolerance)
intersection = _polygon(_select_intersect(comb), tolerance)
poly1_difference = _polygon(_select_difference(comb), tolerance)
poly2_difference = _polygon(_select_difference_rev(comb), tolerance)
return intersection, poly1_difference, poly2_difference
[docs]
def union(poly1, poly2, tolerance):
"""Get a BooleanPolygon for the union of two polygons.
Note that the result will not differentiate hole polygons from boundary polygons.
Args:
poly1: A BooleanPolygon for the first polygon for which the union will
be computed.
poly2: A BooleanPolygon for the second polygon for which the union will
be computed.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A BooleanPolygon for the union of the two polygons.
"""
return __operate(poly1, poly2, _select_union, tolerance)
[docs]
def intersect(poly1, poly2, tolerance):
"""Get a BooleanPolygon for the intersection of two polygons.
Args:
poly1: A BooleanPolygon for the first polygon for which the intersection
will be computed.
poly2: A BooleanPolygon for the second polygon for which the intersection
will be computed.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A BooleanPolygon for the intersection of the two polygons.
"""
return __operate(poly1, poly2, _select_intersect, tolerance)
[docs]
def difference(poly1, poly2, tolerance):
"""Get a BooleanPolygon for the subtraction of poly2 from poly1.
Args:
poly1: A BooleanPolygon for the the polygon that will be subtracted from.
poly2: A BooleanPolygon for the polygon to subtract with.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A BooleanPolygon for the difference of poly1 - poly2.
"""
return __operate(poly1, poly2, _select_difference, tolerance)
[docs]
def difference_reversed(poly1, poly2, tolerance):
"""Get a BooleanPolygon for the subtraction of poly1 from poly2.
Args:
poly1: A BooleanPolygon for the polygon to subtract with.
poly2: A BooleanPolygon for the the polygon that will be subtracted from.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A BooleanPolygon for the difference of poly2 - poly1.
"""
return __operate(poly1, poly2, _select_difference_rev, tolerance)
[docs]
def xor(poly1, poly2, tolerance):
"""Get a BooleanPolygon for the exclusive disjunction of two Polygons.
Note that this method is prone to merging holes that may exist in the
result into the boundary to create a single list of joined vertices,
which may not always be desirable. In this case, it may be desirable
to do two separate difference calculations instead or use the split method.
Also note that, when the result includes separate polygons for holes,
it will not differentiate hole polygons from boundary polygons.
Args:
poly1: A BooleanPolygon for the first polygon for which the exclusive
disjunction will be computed.
poly2: A BooleanPolygon for the second polygon for which the exclusive
disjunction will be computed.
tolerance: The minimum distance between points before they are
considered distinct from one another.
Returns:
A BooleanPolygon for the exclusive disjunction of the two polygons.
"""
return __operate(poly1, poly2, _select_xor, tolerance)