         Draw.reset(context)
     }
 
-    private func pointIsContainedInBooleanResult(point: CGPoint, operation: BooleanPathOperation) -> Bool {
+    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 }
+                    list.append(self.createComponent(from: node))
+                }
+            }
+        }
+        var components = [PathComponent]()
+        performOperation(for: self.graph1, appendingToComponents: &components)
+        performOperation(for: self.graph2, appendingToComponents: &components)
+        return Path(components: components)
+    }
+}
+
+private extension AugmentedGraph {
+
+    func pointIsContainedInBooleanResult(point: CGPoint, operation: BooleanPathOperation) -> Bool {
         let rule: PathFillRule = (operation == .removeCrossings) ? .winding : .evenOdd
         let contained1 = self.path1.contains(point, using: rule)
-        guard operation != .removeCrossings else { return contained1 }
-        let contained2 = self.path2.contains(point, using: rule)
+        let contained2 = operation != .removeCrossings ? self.path2.contains(point, using: rule) : contained1
         switch operation {
         case .union:
             return contained1 || contained2

        nodes = duplicatesRemoved
    }

    public init(path1: Path, path2: Path, intersections: [PathIntersection], operation: BooleanPathOperation) {
        self.operation = operation
        self.path1 = path1
        self.path2 = path2

        // create intersection nodes for each intersection
        var path1Intersections: [IntersectionNode] = []
        var path2Intersections: [IntersectionNode] = []
        intersections.forEach {
            let node1 = IntersectionNode(location: $0.indexedPathLocation1)
            let node2 = IntersectionNode(location: $0.indexedPathLocation2)
            node1.addNeighbor(node2 /*, pathNumber: 1 */)
            node2.addNeighbor(node1 /*, pathNumber: 0 */)
            path1Intersections.append(node1)
            if operation != .removeCrossings {
                path2Intersections.append(node2)
            } else {
                path1Intersections.append(node1)
                path1Intersections.append(node2)
            }
        }
        AugmentedGraph.sortAndMergeDuplicates(of: &path1Intersections)
        if operation != .removeCrossings {
            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
        // mark each intersection as either entry or exit
        self.classifyEdges(in: graph1)
        if operation != .removeCrossings {
            self.classifyEdges(in: graph2)
        }
    }

    private func createComponent(from node: IntersectionNode) -> PathComponent {
        var points: [CGPoint] = [node.edge!.component.startingPoint]
        var orders = [Int]()
        func visit(from node: IntersectionNode) -> IntersectionNode? {
            guard let forwardEdge = node.edge else {
                assertionFailure("consistency failure")
                return nil

            points += component.points[1..<component.points.count]
            orders += component.orders
        }
        var nextNode: Node? = node
        while let currentNode = nextNode {
            nextNode = nil
            if let temp = visit(from: currentNode) {
