from profile import Profiler from util import gen_point, Side, display @Profiler("calc sidedness") def sidedness(p1, p2, p3, eps=0.0000001): # Find line from p1 to p2, ask where p3 is in relation to this y = p3.y * (p2.x - p1.x) x = p3.x a = (p2.y - p1.y) b = p2.y * (p2.x - p1.x) - a * p2.x if y - eps < a * x + b < y + eps: return Side.ON elif y > a * x + b: return Side.ABOVE return Side.BELOW @Profiler("graham_scan") def graham_scan(points): # A funky issue where both a and b become negative in the sidedness test causes us to have to use # Side.ABOVE for both tests, regardless of UH or LH. with Profiler("sorting points"): sorted_points = sorted(points) UH = sorted_points[:2] with Profiler("iterating points", excluded=("calc sidedness",)): for s in sorted_points[2:]: while len(UH) > 1 and (sidedness(UH[-2], UH[-1], s) != Side.ABOVE): del UH[-1] UH.append(s) reversed_list = list(reversed(sorted_points)) reversed_list.append(UH[0]) LH = reversed_list[:2] with Profiler("iterating points", excluded=("calc sidedness",)): for s in reversed_list[2:]: while len(LH) > 1 and (sidedness(LH[-2], LH[-1], s) != Side.ABOVE): del LH[-1] LH.append(s) return UH + LH if __name__ == '__main__': p = [gen_point() for _ in range(30)] hull = graham_scan(p) display(p, hull)