// David Eberly, Geometric Tools, Redmond WA 98052 // Copyright (c) 1998-2020 // Distributed under the Boost Software License, Version 1.0. // https://www.boost.org/LICENSE_1_0.txt // https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt // Version: 4.0.2019.08.13 #pragma once #include #include #include #include #include #include // Extract the minimal cycle basis for a planar graph. The input vertices and // edges must form a graph for which edges intersect only at vertices; that is, // no two edges must intersect at an interior point of one of the edges. The // algorithm is described in // https://www.geometrictools.com/Documentation/MinimalCycleBasis.pdf // The graph might have filaments, which are polylines in the graph that are // not shared by a cycle. These are also extracted by the implementation. // Because the inputs to the constructor are vertices and edges of the graph, // isolated vertices are ignored. // // The computations that determine which adjacent vertex to visit next during // a filament or cycle traversal do not require division, so the exact // arithmetic type BSNumber suffices for ComputeType when you // want to ensure a correct output. (Floating-point rounding errors // potentially can lead to an incorrect output.) namespace WwiseGTE { template class MinimalCycleBasis { public: struct Tree { std::vector cycle; std::vector> children; }; // The input positions and edges must form a planar graph for which // edges intersect only at vertices; that is, no two edges must // intersect at an interior point of one of the edges. MinimalCycleBasis( std::vector> const& positions, std::vector> const& edges, std::vector>& forest) { forest.clear(); if (positions.size() == 0 || edges.size() == 0) { // The graph is empty, so there are no filaments or cycles. return; } // Determine the unique positions referenced by the edges. std::map> unique; for (auto const& edge : edges) { for (int i = 0; i < 2; ++i) { int name = edge[i]; if (unique.find(name) == unique.end()) { auto vertex = std::make_shared(name, &positions[name]); unique.insert(std::make_pair(name, vertex)); } } } // Assign responsibility for ownership of the Vertex objects. std::vector vertices; mVertexStorage.reserve(unique.size()); vertices.reserve(unique.size()); for (auto const& element : unique) { mVertexStorage.push_back(element.second); vertices.push_back(element.second.get()); } // Determine the adjacencies from the edge information. for (auto const& edge : edges) { auto iter0 = unique.find(edge[0]); auto iter1 = unique.find(edge[1]); iter0->second->adjacent.insert(iter1->second.get()); iter1->second->adjacent.insert(iter0->second.get()); } // Get the connected components of the graph. The 'visited' flags // are 0 (unvisited), 1 (discovered), 2 (finished). The Vertex // constructor sets all 'visited' flags to 0. std::vector> components; for (auto vInitial : mVertexStorage) { if (vInitial->visited == 0) { components.push_back(std::vector()); DepthFirstSearch(vInitial.get(), components.back()); } } // The depth-first search is used later for collecting vertices // for subgraphs that are detached from the main graph, so the // 'visited' flags must be reset to zero after component finding. for (auto vertex : mVertexStorage) { vertex->visited = 0; } // Get the primitives for the components. for (auto& component : components) { forest.push_back(ExtractBasis(component)); } } // No copy or assignment allowed. MinimalCycleBasis(MinimalCycleBasis const&) = delete; MinimalCycleBasis& operator=(MinimalCycleBasis const&) = delete; private: struct Vertex { Vertex(int inName, std::array const* inPosition) : name(inName), position(inPosition), visited(0) { } bool operator< (Vertex const& vertex) const { return name < vertex.name; } // The index into the 'positions' input provided to the call to // operator(). The index is used when reporting cycles to the // caller of the constructor for MinimalCycleBasis. int name; // Multiple vertices can share a position during processing of // graph components. std::array const* position; // The mVertexStorage member owns the Vertex objects and maintains // the reference counts on those objects. The adjacent pointers // are considered to be weak pointers; neither object ownership // nor reference counting is required by 'adjacent'. std::set adjacent; // Support for depth-first traversal of a graph. int visited; }; // The constructor uses GetComponents(...) and DepthFirstSearch(...) // to get the connected components of the graph implied by the input // 'edges'. Recursive processing uses only DepthFirstSearch(...) to // collect vertices of the subgraphs of the original graph. static void DepthFirstSearch(Vertex* vInitial, std::vector& component) { std::stack vStack; vStack.push(vInitial); while (vStack.size() > 0) { Vertex* vertex = vStack.top(); vertex->visited = 1; size_t i = 0; for (auto adjacent : vertex->adjacent) { if (adjacent && adjacent->visited == 0) { vStack.push(adjacent); break; } ++i; } if (i == vertex->adjacent.size()) { vertex->visited = 2; component.push_back(vertex); vStack.pop(); } } } // Support for traversing a simply connected component of the graph. std::shared_ptr ExtractBasis(std::vector& component) { // The root will not have its 'cycle' member set. The children // are the cycle trees extracted from the component. auto tree = std::make_shared(); while (component.size() > 0) { RemoveFilaments(component); if (component.size() > 0) { tree->children.push_back(ExtractCycleFromComponent(component)); } } if (tree->cycle.size() == 0 && tree->children.size() == 1) { // Replace the parent by the child to avoid having two empty // cycles in parent/child. auto child = tree->children.back(); tree->cycle = std::move(child->cycle); tree->children = std::move(child->children); } return tree; } void RemoveFilaments(std::vector& component) { // Locate all filament endpoints, which are vertices, each having // exactly one adjacent vertex. std::vector endpoints; for (auto vertex : component) { if (vertex->adjacent.size() == 1) { endpoints.push_back(vertex); } } if (endpoints.size() > 0) { // Remove the filaments from the component. If a filament has // two endpoints, each having one adjacent vertex, the // adjacency set of the final visited vertex become empty. // We must test for that condition before starting a new // filament removal. for (auto vertex : endpoints) { if (vertex->adjacent.size() == 1) { // Traverse the filament and remove the vertices. while (vertex->adjacent.size() == 1) { // Break the connection between the two vertices. Vertex* adjacent = *vertex->adjacent.begin(); adjacent->adjacent.erase(vertex); vertex->adjacent.erase(adjacent); // Traverse to the adjacent vertex. vertex = adjacent; } } } // At this time the component is either empty (it was a union // of polylines) or it has no filaments and at least one // cycle. Remove the isolated vertices generated by filament // extraction. std::vector remaining; remaining.reserve(component.size()); for (auto vertex : component) { if (vertex->adjacent.size() > 0) { remaining.push_back(vertex); } } component = std::move(remaining); } } std::shared_ptr ExtractCycleFromComponent(std::vector& component) { // Search for the left-most vertex of the component. If two or // more vertices attain minimum x-value, select the one that has // minimum y-value. Vertex* minVertex = component[0]; for (auto vertex : component) { if (*vertex->position < *minVertex->position) { minVertex = vertex; } } // Traverse the closed walk, duplicating the starting vertex as // the last vertex. std::vector closedWalk; Vertex* vCurr = minVertex; Vertex* vStart = vCurr; closedWalk.push_back(vStart); Vertex* vAdj = GetClockwiseMost(nullptr, vStart); while (vAdj != vStart) { closedWalk.push_back(vAdj); Vertex* vNext = GetCounterclockwiseMost(vCurr, vAdj); vCurr = vAdj; vAdj = vNext; } closedWalk.push_back(vStart); // Recursively process the closed walk to extract cycles. auto tree = ExtractCycleFromClosedWalk(closedWalk); // The isolated vertices generated by cycle removal are also // removed from the component. std::vector remaining; remaining.reserve(component.size()); for (auto vertex : component) { if (vertex->adjacent.size() > 0) { remaining.push_back(vertex); } } component = std::move(remaining); return tree; } std::shared_ptr ExtractCycleFromClosedWalk(std::vector& closedWalk) { auto tree = std::make_shared(); std::map duplicates; std::set detachments; int numClosedWalk = static_cast(closedWalk.size()); for (int i = 1; i < numClosedWalk - 1; ++i) { auto diter = duplicates.find(closedWalk[i]); if (diter == duplicates.end()) { // We have not yet visited this vertex. duplicates.insert(std::make_pair(closedWalk[i], i)); continue; } // The vertex has been visited previously. Collapse the // closed walk by removing the subwalk sharing this vertex. // Note that the vertex is pointed to by // closedWalk[diter->second] and closedWalk[i]. int iMin = diter->second, iMax = i; detachments.insert(iMin); for (int j = iMin + 1; j < iMax; ++j) { Vertex* vertex = closedWalk[j]; duplicates.erase(vertex); detachments.erase(j); } closedWalk.erase(closedWalk.begin() + iMin + 1, closedWalk.begin() + iMax + 1); numClosedWalk = static_cast(closedWalk.size()); i = iMin; } if (numClosedWalk > 3) { // We do not know whether closedWalk[0] is a detachment point. // To determine this, we must test for any edges strictly // contained by the wedge formed by the edges // and // . However, we must execute // this test even for the known detachment points. The // ensuing logic is designed to handle this and reduce the // amount of code, so we insert closedWalk[0] into the // detachment set and will ignore it later if it actually // is not. detachments.insert(0); // Detach subgraphs from the vertices of the cycle. for (auto i : detachments) { Vertex* original = closedWalk[i]; Vertex* maxVertex = closedWalk[i + 1]; Vertex* minVertex = (i > 0 ? closedWalk[i - 1] : closedWalk[numClosedWalk - 2]); std::array dMin, dMax; for (int j = 0; j < 2; ++j) { dMin[j] = (*minVertex->position)[j] - (*original->position)[j]; dMax[j] = (*maxVertex->position)[j] - (*original->position)[j]; } // For debugging. bool isConvex = (dMax[0] * dMin[1] >= dMax[1] * dMin[0]); (void)isConvex; std::set inWedge; std::set adjacent = original->adjacent; for (auto vertex : adjacent) { if (vertex->name == minVertex->name || vertex->name == maxVertex->name) { continue; } std::array dVer; for (int j = 0; j < 2; ++j) { dVer[j] = (*vertex->position)[j] - (*original->position)[j]; } bool containsVertex; if (isConvex) { containsVertex = dVer[0] * dMin[1] > dVer[1] * dMin[0] && dVer[0] * dMax[1] < dVer[1] * dMax[0]; } else { containsVertex = (dVer[0] * dMin[1] > dVer[1] * dMin[0]) || (dVer[0] * dMax[1] < dVer[1] * dMax[0]); } if (containsVertex) { inWedge.insert(vertex); } } if (inWedge.size() > 0) { // The clone will manage the adjacents for 'original' // that lie inside the wedge defined by the first and // last edges of the subgraph rooted at 'original'. // The sorting is in the clockwise direction. auto clone = std::make_shared(original->name, original->position); mVertexStorage.push_back(clone); // Detach the edges inside the wedge. for (auto vertex : inWedge) { original->adjacent.erase(vertex); vertex->adjacent.erase(original); clone->adjacent.insert(vertex); vertex->adjacent.insert(clone.get()); } // Get the subgraph (it is a single connected // component). std::vector component; DepthFirstSearch(clone.get(), component); // Extract the cycles of the subgraph. tree->children.push_back(ExtractBasis(component)); } // else the candidate was closedWalk[0] and it has no // subgraph to detach. } tree->cycle = std::move(ExtractCycle(closedWalk)); } else { // Detach the subgraph from vertex closedWalk[0]; the subgraph // is attached via a filament. Vertex* original = closedWalk[0]; Vertex* adjacent = closedWalk[1]; auto clone = std::make_shared(original->name, original->position); mVertexStorage.push_back(clone); original->adjacent.erase(adjacent); adjacent->adjacent.erase(original); clone->adjacent.insert(adjacent); adjacent->adjacent.insert(clone.get()); // Get the subgraph (it is a single connected component). std::vector component; DepthFirstSearch(clone.get(), component); // Extract the cycles of the subgraph. tree->children.push_back(ExtractBasis(component)); if (tree->cycle.size() == 0 && tree->children.size() == 1) { // Replace the parent by the child to avoid having two // empty cycles in parent/child. auto child = tree->children.back(); tree->cycle = std::move(child->cycle); tree->children = std::move(child->children); } } return tree; } std::vector ExtractCycle(std::vector& closedWalk) { // TODO: This logic was designed not to remove filaments after // the cycle deletion is complete. Modify this to allow filament // removal. // The closed walk is a cycle. int const numVertices = static_cast(closedWalk.size()); std::vector cycle(numVertices); for (int i = 0; i < numVertices; ++i) { cycle[i] = closedWalk[i]->name; } // The clockwise-most edge is always removable. Vertex* v0 = closedWalk[0]; Vertex* v1 = closedWalk[1]; Vertex* vBranch = (v0->adjacent.size() > 2 ? v0 : nullptr); v0->adjacent.erase(v1); v1->adjacent.erase(v0); // Remove edges while traversing counterclockwise. while (v1 != vBranch && v1->adjacent.size() == 1) { Vertex* adj = *v1->adjacent.begin(); v1->adjacent.erase(adj); adj->adjacent.erase(v1); v1 = adj; } if (v1 != v0) { // If v1 had exactly 3 adjacent vertices, removal of the CCW // edge that shared v1 leads to v1 having 2 adjacent vertices. // When the CW removal occurs and we reach v1, the edge // deletion will lead to v1 having 1 adjacent vertex, making // it a filament endpoint. We must ensure we do not delete v1 // in this case, allowing the recursive algorithm to handle // the filament later. vBranch = v1; // Remove edges while traversing clockwise. while (v0 != vBranch && v0->adjacent.size() == 1) { v1 = *v0->adjacent.begin(); v0->adjacent.erase(v1); v1->adjacent.erase(v0); v0 = v1; } } // else the cycle is its own connected component. return cycle; } Vertex* GetClockwiseMost(Vertex* vPrev, Vertex* vCurr) const { Vertex* vNext = nullptr; bool vCurrConvex = false; std::array dCurr, dNext; if (vPrev) { dCurr[0] = (*vCurr->position)[0] - (*vPrev->position)[0]; dCurr[1] = (*vCurr->position)[1] - (*vPrev->position)[1]; } else { dCurr[0] = static_cast(0); dCurr[1] = static_cast(-1); } for (auto vAdj : vCurr->adjacent) { // vAdj is a vertex adjacent to vCurr. No backtracking is // allowed. if (vAdj == vPrev) { continue; } // Compute the potential direction to move in. std::array dAdj; dAdj[0] = (*vAdj->position)[0] - (*vCurr->position)[0]; dAdj[1] = (*vAdj->position)[1] - (*vCurr->position)[1]; // Select the first candidate. if (!vNext) { vNext = vAdj; dNext = dAdj; vCurrConvex = (dNext[0] * dCurr[1] <= dNext[1] * dCurr[0]); continue; } // Update if the next candidate is clockwise of the current // clockwise-most vertex. if (vCurrConvex) { if (dCurr[0] * dAdj[1] < dCurr[1] * dAdj[0] || dNext[0] * dAdj[1] < dNext[1] * dAdj[0]) { vNext = vAdj; dNext = dAdj; vCurrConvex = (dNext[0] * dCurr[1] <= dNext[1] * dCurr[0]); } } else { if (dCurr[0] * dAdj[1] < dCurr[1] * dAdj[0] && dNext[0] * dAdj[1] < dNext[1] * dAdj[0]) { vNext = vAdj; dNext = dAdj; vCurrConvex = (dNext[0] * dCurr[1] < dNext[1] * dCurr[0]); } } } return vNext; } Vertex* GetCounterclockwiseMost(Vertex* vPrev, Vertex* vCurr) const { Vertex* vNext = nullptr; bool vCurrConvex = false; std::array dCurr, dNext; if (vPrev) { dCurr[0] = (*vCurr->position)[0] - (*vPrev->position)[0]; dCurr[1] = (*vCurr->position)[1] - (*vPrev->position)[1]; } else { dCurr[0] = static_cast(0); dCurr[1] = static_cast(-1); } for (auto vAdj : vCurr->adjacent) { // vAdj is a vertex adjacent to vCurr. No backtracking is // allowed. if (vAdj == vPrev) { continue; } // Compute the potential direction to move in. std::array dAdj; dAdj[0] = (*vAdj->position)[0] - (*vCurr->position)[0]; dAdj[1] = (*vAdj->position)[1] - (*vCurr->position)[1]; // Select the first candidate. if (!vNext) { vNext = vAdj; dNext = dAdj; vCurrConvex = (dNext[0] * dCurr[1] <= dNext[1] * dCurr[0]); continue; } // Select the next candidate if it is counterclockwise of the // current counterclockwise-most vertex. if (vCurrConvex) { if (dCurr[0] * dAdj[1] > dCurr[1] * dAdj[0] && dNext[0] * dAdj[1] > dNext[1] * dAdj[0]) { vNext = vAdj; dNext = dAdj; vCurrConvex = (dNext[0] * dCurr[1] <= dNext[1] * dCurr[0]); } } else { if (dCurr[0] * dAdj[1] > dCurr[1] * dAdj[0] || dNext[0] * dAdj[1] > dNext[1] * dAdj[0]) { vNext = vAdj; dNext = dAdj; vCurrConvex = (dNext[0] * dCurr[1] <= dNext[1] * dCurr[0]); } } } return vNext; } // Storage for referenced vertices of the original graph and for new // vertices added during graph traversal. std::vector> mVertexStorage; }; }