             AugmentedGraph.sortAndMergeDuplicates(of: &path2Intersections)
         }
         // create graph representations of the two paths
-        self.graph1 = AugmentedGraph.createGraph(for: self.path1, using: path1Intersections)
-        self.graph2 = (operation != .removeCrossings) ? AugmentedGraph.createGraph(for: self.path2, using: path2Intersections) : graph1
+        self.graph1 = PathGraph(for: path1, using: path1Intersections)
+        self.graph2 = (operation != .removeCrossings) ? PathGraph(for: path2, using: path2Intersections) : graph1
         // mark each edge as either included or excluded from the final result
-        self.classifyEdges(in: graph1, isForFirstPath: true)
+        self.classifyEdges(for: self.graph1, isForFirstPath: true)
         if operation != .removeCrossings {
-            self.classifyEdges(in: graph2, isForFirstPath: false)
+            self.classifyEdges(for: self.graph2, isForFirstPath: false)
         }
     }
     public func draw(_ context: CGContext) {
-        func drawList(_ list: [Node]) {
-            for i in 0..<list.count {
-                self.forEachNode(in: list[i]) { node, _ in
-
-                    switch node.edge!.inSolution {
+        func drawGraph(_ graph: PathGraph) {
+            for i in 0..<graph.components.count {
+                graph.components[i].forEachNode { node, _ in
+                    switch node.edge.inSolution {
                     case true:
                         Draw.setColor(context, color: Draw.red)
                     case false:
                         Draw.setColor(context, color: Draw.green)
                     }
-
-                    for curve in node.edge!.component.curves {
+                    for curve in node.component.curves {
                         Draw.drawCurve(context, curve: curve)
                     }
                 }
             }
         }
-        drawList(self.graph1)
-        drawList(self.graph2)
+        drawGraph(self.graph1)
+        drawGraph(self.graph2)
         Draw.reset(context)
     }
     internal func performOperation() -> Path {
-        func performOperation(for components: [Node], appendingToComponents list: inout [PathComponent]) {
-            components.forEach {
-                self.forEachNode(in: $0) { node, _ in
-                    guard node.edge?.inSolution == true, node.edge?.visited == false else { return }
+        func performOperation(for graph: PathGraph, appendingToComponents list: inout [PathComponent]) {
+            graph.components.forEach {
+                $0.forEachNode { node, _ in
+                    guard node.edge.inSolution == true, node.edge.visited == false else { return }
                     list.append(self.createComponent(from: node))
                 }
             }

        nodes = duplicatesRemoved
    }
    func createComponent(from startingNode: Node) -> PathComponent {
        let firstPoint = startingNode.edge!.component.startingPoint
        var points: [CGPoint] = [firstPoint]
        var orders: [Int] = []
        func visit(from node: Node) -> Node? {
            guard let forwardEdge = node.edge else {
                assertionFailure("consistency failure")
                return nil
            }
            guard let backwardsEdge = node.previous?.edge else {
                assertionFailure("consistency failure")
                return nil
            }
            if forwardEdge.inSolution, forwardEdge.visited == false {
                visit(forwardEdge.component)
                forwardEdge.visited = true
                return node.next
            } else if backwardsEdge.inSolution, backwardsEdge.visited == false {
                visit(backwardsEdge.component.reversed())
                backwardsEdge.visited = true
                return node.previous
            } else {
                return nil
            }
        }
        func visit(_ component: PathComponent) {
            points += component.points[1..<component.points.count]

}

private extension AugmentedGraph {
    /// traverses the list of edges and marks each edge as either .internal, .external, or .coincident with respect to `self.path`
    func classifyEdges(for graph: PathGraph, isForFirstPath: Bool) {
        func classifyEdge(for node: Node) {
            // TODO: we use a crummy point location
            let nextEdge = node.component.element(at: 0)
            let point = nextEdge.compute(0.5)
            let normal = nextEdge.normal(0.5)
            let smallDistance = CGFloat(Utils.epsilon)
            let point1 = point + smallDistance * normal
            let point2 = point - smallDistance * normal
            let included1 = self.pointIsContainedInBooleanResult(point: point1, operation: operation)
            let included2 = self.pointIsContainedInBooleanResult(point: point2, operation: operation)
            node.edge.inSolution = (included1 != included2)
            if !isForFirstPath, node.edge.inSolution, operation != .removeCrossings {
                // remove duplicate coincident edges
                let rule: PathFillRule = .evenOdd
                let edge1 = self.graph1.path.contains(point1, using: rule) != self.graph1.path.contains(point2, using: rule)
                let edge2 = self.graph2.path.contains(point1, using: rule) != self.graph2.path.contains(point2, using: rule)
                if edge1, edge2 {
                    node.edge.inSolution = false
                }
            }
        }
        func classifyEdges(for component: PathComponentGraph) {
            component.forEachNode { node, _ in
                classifyEdge(for: node)
            }
        }
        graph.components.forEach { classifyEdges(for: $0) }
    }
    func pointIsContainedInBooleanResult(point: CGPoint, operation: BooleanPathOperation) -> Bool {
        let rule: PathFillRule = (operation == .removeCrossings) ? .winding : .evenOdd
        let contained1 = self.graph1.path.contains(point, using: rule)
        let contained2 = operation != .removeCrossings ? self.graph2.path.contains(point, using: rule) : contained1
        switch operation {
        case .union:
            return contained1 || contained2
