#include "precomp.h" #include "prickliness.h" #include "DrawPrickliness.h" namespace Prickliness { //Get a vector to a point inside a convex DCEL plane Vector_3 PricklinessArrangement::GetVectorInPlane(const Arrangement_2::Face_const_handle face) const { std::vector edgePoints; if (face->has_outer_ccb()) { Arrangement_2::Ccb_halfedge_const_circulator currentHalfEdge = face->outer_ccb(); //Select a point on 2 of the (non-fictitious) edges to calculate a point inside the face do { //Fictitious: https://doc.cgal.org/latest/Arrangement_on_surface_2/index.html#title32 if (!currentHalfEdge->is_fictitious()) { if (currentHalfEdge->curve().is_ray()) { //Edge is an infinite ray originating from a vertex on an unbounded face, push an arbitrary point on the ray auto rayStart = currentHalfEdge->curve().ray().start(); auto rayVector = currentHalfEdge->curve().ray().direction().to_vector(); edgePoints.push_back(rayStart + (rayVector * Kernel::FT(0.5))); } else if (currentHalfEdge->curve().is_segment()) { //Edge is a line segment bounded by two vertices, push the point in the middle of the line segment auto heSegment = currentHalfEdge->curve().segment(); auto segmentVector = heSegment.end() - heSegment.start(); edgePoints.push_back(heSegment.start() + (segmentVector * Kernel::FT(0.5))); } } } while (++currentHalfEdge != face->outer_ccb() && edgePoints.size() < 2); } //Get the point in between the two edge points and convert it to 3D //the arrangement plane is at z = 10 Point_2 startFacePoint = edgePoints.at(0) + ((edgePoints.at(1) - edgePoints.at(0)) * Kernel::FT(0.5)); Vector_3 faceVector(startFacePoint.x(), startFacePoint.y(), 10.0); return faceVector; } bool PricklinessArrangement::IsBoundaryVert(const Delaunay& DT, Delaunay::Face_circulator adjacentFaces) const { Delaunay::Face_circulator first = adjacentFaces; do { if (DT.is_infinite(adjacentFaces)) { return true; } } while (++adjacentFaces != first); return false; } PricklinessArrangement::PricklinessArrangement(const Delaunay& DT) { Plane_3 arrangementPlane(Point_3(0.0, 0.0, 10.0), Vector_3(0.0, 0.0, 1.0)); std::vector arrangementLines; std::vector> perpPlanes; std::vector vertexDegrees; int currentVertIndex = 0; std::cout << "\t Constructing perpendicular planes" << std::endl; size_t DTVertCount = DT.number_of_vertices(); //size_t percent = DTVertCount / 100; //Loop through all the finite vertices of the DT, the underlying structures adds a point at infinity to connect all the boundary verts together for (Del_finite_vertices_iterator vertIterator = DT.finite_vertices_begin(); vertIterator != DT.finite_vertices_end(); vertIterator++) { if (IsBoundaryVert(DT, vertIterator->incident_faces())) { continue; } //Add a perp plane vector for this vertex perpPlanes.push_back(std::vector()); //Store current vertex index for the result plane traversal PerpPlaneIndices vnpTriple; vnpTriple.vertexIndex = currentVertIndex; //Get next vertex and its neighbours iterator Point_3 currentVertex = vertIterator->point(); Del_vertex_circulator neighbourCirculator = vertIterator->incident_vertices(), done(neighbourCirculator); int neighbourIndex = 0; if (neighbourCirculator != 0) { do { //Skip the vertex at infinity that the triangulation DS uses to connect the boundary verts if (!DT.is_infinite(neighbourCirculator)) { //Get neighbour vertex and subtract current current vertex to move it towards the origin Point_3 neighbour = neighbourCirculator->point(); Vector_3 neighbourToOrigin = neighbour - currentVertex; //Construct a line going through the neighbour and current vector at the origin //Flip the direction so it goes towards the current vector //(it's important to make sure it's oriented this way because we need to check if we are on the local maxima side of the plane with a dot on the normal) Line_3 lineThroughOrigin = Line_3(CGAL::ORIGIN, neighbourToOrigin).opposite(); //Store the index of the neighbour so we can use it to get it's perp plane vnpTriple.planeIndex = perpPlanes.at(currentVertIndex).size(); //Construct a plane perpendiculat to the neighbour line at the origin Plane_3 perpPlane = lineThroughOrigin.perpendicular_plane(CGAL::ORIGIN); perpPlanes.at(currentVertIndex).push_back(perpPlane); //Intersect the perp plane with the arrangement plane, store the resulting 2D line so we can mass add them in the arrangement auto intersection = CGAL::intersection(arrangementPlane, perpPlane); if (intersection) { if (Line_3* arrangementLine = boost::get(&*intersection)) { Point_3 p3 = arrangementLine->point(); Vector_3 v3 = arrangementLine->to_vector(); Arr_data_line_2 arrangementLine2(Point_2(p3.x(), p3.y()), Vector_2(v3.x(), v3.y())); arrangementLines.push_back(Arr_X_monotone_curve_2(arrangementLine2, vnpTriple)); //CGAL::insert(resultArrangement, Arr_X_monotone_curve_2(arrangementLine2, vnpTriple)); } else { //This will never happen because the perp planes go through the origin and the arrangement plane lies above it. std::cout << "Result of intersection between perpendicular plane and arrangement plane is a plane!" << std::endl; } } else { //Parallel planes degenerate case: Ignore? Should never happen unless two vertices are right on top of each other std::cout << "Empty intersection between perpendicular plane and arrangement plane!" << std::endl; } neighbourIndex++; } } while (++neighbourCirculator != done); vertexDegrees.push_back(neighbourIndex); //Save degree this way because vert->degree() counts fictitious neighbours.. } currentVertIndex++; //if (currentVertIndex % percent == 0) //{ // std::cout << currentVertIndex / percent << std::endl; //} } std::cout << "\t Constructing 2d arrangement (DCEL)" << std::endl; //Construct an arrangement (DCEL) that keeps track of the original lines the half-edges are created from //Arrangement_2 resultArrangement; CGAL::insert(resultArrangement, arrangementLines.begin(), arrangementLines.end()); arrangementLines.clear(); //FIFO queues for breadth first traversal of the arrangement std::queue faceQueue; std::queue> abovePlaneCountsQueue; std::cout << "\t Determining local maxima for DCEL faces" << std::endl; if (resultArrangement.number_of_faces() > 1) { //Calculate the local maxima for the (random) starting face auto startingFace = resultArrangement.faces_begin(); //Get a vector to a point in the face so we can check if we are above the perp planes Vector_3 startFaceVector = GetVectorInPlane(startingFace); //Construct the data structure keeping track of planes and the local maxima std::vector abovePlanesCount; abovePlanesCount.reserve(perpPlanes.size()); for (std::vector& planeVector : perpPlanes) { int aboveCount = 0; for (Plane_3& plane : planeVector) { //Count as above if perpendicular plane normal dot face > 0 if (CGAL::scalar_product(plane.orthogonal_vector(), startFaceVector) > Kernel::FT(0)) { aboveCount++; } } abovePlanesCount.push_back(aboveCount); } //Count for how many terrain vertices we are in the local maxima cone, this is the # local maxima for this arrangement face //We are in the cone if we are above all of its planes int localMaximaCount = 0; for (size_t i = 0; i < abovePlanesCount.size(); i++) { if (abovePlanesCount.at(i) == vertexDegrees.at(i)) { localMaximaCount++; } } startingFace->data().localMaximaCount = localMaximaCount; //Put the starting face and the counters for the planes on the queues for the breadth first iteration faceQueue.push(startingFace); abovePlaneCountsQueue.push(abovePlanesCount); //Breadth first search over the terrain: //Everytime we cross an edge check its corresponding perp plane(s) with a vector to the point in the next face: //if we are above a plane increment its corresponding above counter, decrement otherwise. //if the above counter is equal to the degree of the vertex after an increment we are within its local maxima cone and increment # local maxima //if the above counter is equal to the degree of the vertex before an decrement we have left the local maxima cone and decrement # local maxima while (!faceQueue.empty()) { auto currentFace = faceQueue.front(); faceQueue.pop(); std::vector currentAbovePlaneCounts = abovePlaneCountsQueue.front(); abovePlaneCountsQueue.pop(); //Iterate through the incident edges Arrangement_2::Ccb_halfedge_circulator currentHalfEdge = currentFace->outer_ccb(); do { //Fictitious: https://doc.cgal.org/latest/Arrangement_on_surface_2/index.html#title32 if (!currentHalfEdge->is_fictitious()) { //Skip neighbour face if already visited if (currentHalfEdge->twin()->face()->data().localMaximaCount != -1) { continue; } std::vector neighbourAbovePlanesCounts = currentAbovePlaneCounts; int localMaximaCount = currentFace->data().localMaximaCount; Vector_3 faceVector = GetVectorInPlane(currentHalfEdge->twin()->face()); //This is a for loop because the DCEL stores the data in a list in case of the degenerate case where multiple lines overlap for (PerpPlaneIndices ppIndices : currentHalfEdge->curve().data()) { Plane_3 perpPlane = perpPlanes.at(ppIndices.vertexIndex).at(ppIndices.planeIndex); //Which side of the perp plane did we go to? if (CGAL::scalar_product(perpPlane.orthogonal_vector(), faceVector) > Kernel::FT(0)) { //Above, increment above counter and check if inside local maxima cone neighbourAbovePlanesCounts.at(ppIndices.vertexIndex)++; if (neighbourAbovePlanesCounts.at(ppIndices.vertexIndex) == vertexDegrees.at(ppIndices.vertexIndex)) { localMaximaCount++; } } else { //Below, check if we were inside the local maxima cone and decrement above counter if (neighbourAbovePlanesCounts.at(ppIndices.vertexIndex) == vertexDegrees.at(ppIndices.vertexIndex)) { localMaximaCount--; } neighbourAbovePlanesCounts.at(ppIndices.vertexIndex)--; } } currentHalfEdge->twin()->face()->data().localMaximaCount = localMaximaCount; //Local maxima count for this face current most? if (prickliness < localMaximaCount) { prickliness = localMaximaCount; pricklyDirection = faceVector; } faceQueue.push(currentHalfEdge->twin()->face()); abovePlaneCountsQueue.push(neighbourAbovePlanesCounts); } } while (++currentHalfEdge != currentFace->outer_ccb()); } } //CGAL::draw(resultArrangement); } void PricklinessArrangement::Draw() { Arrangement_2::Vertex_const_iterator vit = resultArrangement.vertices_begin(); Kernel::FT minX = vit->point().x(); Kernel::FT minY = vit->point().y(); Kernel::FT maxX = vit->point().x(); Kernel::FT maxY = vit->point().y(); for (; vit != resultArrangement.vertices_end(); ++vit) { if (vit->point().x() < minX) { minX = vit->point().x(); } if (vit->point().y() < minY) { minY = vit->point().y(); } if (vit->point().x() > maxX) { maxX = vit->point().x(); } if (vit->point().y() > maxY) { maxY = vit->point().y(); } } Point_2 minPoint(minX, minY); Point_2 maxPoint(maxX, maxY); minPoint -= Vector_2(10, 10); maxPoint += Vector_2(10, 10); std::cout << "Min:" << minPoint << " Max:" << maxPoint << std::endl; Point_2 topLeft(minPoint.x(), maxPoint.y()); Point_2 bottomRight(maxPoint.x(), minPoint.y()); std::vector boundarybox; boundarybox.push_back(Arr_traits_2::Curve_2(topLeft, minPoint)); boundarybox.push_back(Arr_traits_2::Curve_2(minPoint, bottomRight)); boundarybox.push_back(Arr_traits_2::Curve_2(bottomRight, maxPoint)); boundarybox.push_back(Arr_traits_2::Curve_2(maxPoint, topLeft)); CGAL::insert(resultArrangement, boundarybox.begin(), boundarybox.end()); std::cout << "Drawing Prickliness (" << prickliness << "): " << resultArrangement.number_of_faces() << " Faces, " << resultArrangement.number_of_edges() << " Edges, " << resultArrangement.number_of_vertices() << " Vertices." << std::endl; DrawPrickliness(resultArrangement, prickliness, pricklyDirection); } void PricklinessArrangement::WriteToIpe(const std::filesystem::path& filePath, bool centerOnOrigin) { //Merge faces Arrangement_2::Edge_iterator edgeIt; std::vector redundantEdges; std::vector pricklyValues; for (edgeIt = resultArrangement.edges_begin(); edgeIt != resultArrangement.edges_end(); ++edgeIt) { Arrangement_2::Halfedge_handle hEdge = edgeIt; if (hEdge->face()->data().localMaximaCount == hEdge->twin()->face()->data().localMaximaCount) { int prickly = hEdge->face()->data().localMaximaCount; redundantEdges.push_back(hEdge); pricklyValues.push_back(hEdge->face()->data().localMaximaCount); } } int currIndex = 0; for (Arrangement_2::Halfedge_handle hEdge : redundantEdges) { Arrangement_2::Face_handle fh = resultArrangement.remove_edge(hEdge, true, true); fh->data().localMaximaCount = pricklyValues.at(currIndex); ++currIndex; } //Remove redundant vertices std::vector vertexHandles; for (Arrangement_2::Vertex_handle vertexHandle : resultArrangement.vertex_handles()) { vertexHandles.push_back(vertexHandle); } for (Arrangement_2::Vertex_handle vertexHandle : vertexHandles) { CGAL::remove_vertex(resultArrangement, vertexHandle); } vertexHandles.clear(); //Add bounding box #if 0 //Boundary box variable size Arrangement_2::Vertex_const_iterator vit = resultArrangement.vertices_begin(); Kernel::FT minX = vit->point().x(); Kernel::FT minY = vit->point().y(); Kernel::FT maxX = vit->point().x(); Kernel::FT maxY = vit->point().y(); for (; vit != resultArrangement.vertices_end(); ++vit) { if (vit->point().x() < minX) { minX = vit->point().x(); } if (vit->point().y() < minY) { minY = vit->point().y(); } if (vit->point().x() > maxX) { maxX = vit->point().x(); } if (vit->point().y() > maxY) { maxY = vit->point().y(); } } Point_2 minPoint(minX, minY); Point_2 maxPoint(maxX, maxY); minPoint -= Vector_2(1, 1); maxPoint += Vector_2(1, 1); #else //Static size Point_2 minPoint; Point_2 maxPoint; if (centerOnOrigin) { //square centered on prickly direction minPoint = CGAL::ORIGIN; maxPoint = CGAL::ORIGIN; } else { //square centered on prickly direction minPoint = Point_2(pricklyDirection.x(), pricklyDirection.y()); maxPoint = Point_2(pricklyDirection.x(), pricklyDirection.y()); } minPoint -= Vector_2(4, 4); maxPoint += Vector_2(4, 4); #endif // boundary box std::cout << "Min:" << minPoint << " Max:" << maxPoint << std::endl; Point_2 topLeft(minPoint.x(), maxPoint.y()); Point_2 bottomRight(maxPoint.x(), minPoint.y()); std::vector boundarybox; boundarybox.push_back(Arr_traits_2::Curve_2(topLeft, minPoint)); boundarybox.push_back(Arr_traits_2::Curve_2(minPoint, bottomRight)); boundarybox.push_back(Arr_traits_2::Curve_2(bottomRight, maxPoint)); boundarybox.push_back(Arr_traits_2::Curve_2(maxPoint, topLeft)); CGAL::insert(resultArrangement, boundarybox.begin(), boundarybox.end()); const std::string ipeFileStart = "\n" "\n" "\n" "\n" "\n" "\n"; const std::string ipeFileEnd = "\n"; Arrangement_2::Face_const_iterator fit; //Find lowest and highest values in current view (This double loop stuff is kind of ugly but it's only for some basic i/o so whatever) int currentViewMaximaMax = 0; int currentViewMaximaMin = prickliness; for (fit = resultArrangement.faces_begin(); fit != resultArrangement.faces_end(); ++fit) { //Only export finite faces if (!fit->is_unbounded() && !fit->is_fictitious() && fit->has_outer_ccb()) { if (fit->data().localMaximaCount != -1) { auto edgeCirculator = fit->outer_ccb(); auto startEdgeCirc = fit->outer_ccb(); bool inside = true; do { if (edgeCirculator->source()->point().x() < minPoint.x() || edgeCirculator->source()->point().x() > maxPoint.x() || edgeCirculator->source()->point().y() < minPoint.y() || edgeCirculator->source()->point().y() > maxPoint.y()) { inside = false; break; } } while (++edgeCirculator != startEdgeCirc); //Skip faces outside of bounding box if (!inside) { continue; } if (currentViewMaximaMax < fit->data().localMaximaCount) { currentViewMaximaMax = fit->data().localMaximaCount; } if (currentViewMaximaMin > fit->data().localMaximaCount) { currentViewMaximaMin = fit->data().localMaximaCount; } } } } std::cout << "Min prickly in view: " << currentViewMaximaMin << std::endl; std::cout << "Max prickly in view: " << currentViewMaximaMax << std::endl; std::ostringstream fileName; fileName << "prickliness_"; if (centerOnOrigin) { fileName << "O"; } else { fileName << "P"; } fileName << "_" << currentViewMaximaMin << "_to_" << currentViewMaximaMax << ".ipe"; std::ofstream ipeFileStream(filePath / fileName.str()); if (!ipeFileStream.is_open()) { std::cout << "\t Ipe file path invalid or not openable.." << std::endl; return; } ipeFileStream << ipeFileStart; //Export the arrangement faces. for (fit = resultArrangement.faces_begin(); fit != resultArrangement.faces_end(); ++fit) { //Only export finite faces if (!fit->is_unbounded() && !fit->is_fictitious() && fit->has_outer_ccb()) { auto edgeCirculator = fit->outer_ccb(); auto startEdgeCirc = fit->outer_ccb(); bool inside = true; do { if (edgeCirculator->source()->point().x() < minPoint.x() || edgeCirculator->source()->point().x() > maxPoint.x() || edgeCirculator->source()->point().y() < minPoint.y() || edgeCirculator->source()->point().y() > maxPoint.y()) { inside = false; break; } } while (++edgeCirculator != startEdgeCirc); //Skip faces outside of bounding box if (!inside) { continue; } //Determine face color if (fit->data().localMaximaCount != -1) { //double shade = (((double)fit->data().localMaximaCount / (double)prickliness)); //double shade = (((double)(fit->data().localMaximaCount - currentViewMaximaMin) / (double)(currentViewMaximaMax - currentViewMaximaMin))); ////Green -> Red (https://stackoverflow.com/a/7947812) //double r = std::fmin(2.0 * shade, 1.0); //double g = std::fmin(2.0 * (1.0 - shade), 1.0); //double b = 0.0; ////Yellow -> Red //double r = 1.0; //double g = 1.0 - shade; //double b = 0; //PricklyColor color = GetColorForPercentage((double)(fit->data().localMaximaCount - currentViewMaximaMin) / (double)(currentViewMaximaMax - currentViewMaximaMin)); PricklyColor color = GetColorForPercentage((double)(fit->data().localMaximaCount) / (double)(currentViewMaximaMax)); //Polygon opening tag, set both stroke and fill to pickly shade so we don't get the wierd lines in between the faces ipeFileStream << "\n"; } else { //White face for prickliless (I just made that word)faces ipeFileStream << "\n"; } edgeCirculator = fit->outer_ccb(); startEdgeCirc = fit->outer_ccb(); //Circle along the edges and add the vertices to the ipe polygon bool first = true; do { ipeFileStream << (edgeCirculator->source()->point().x() * Kernel::FT(10)) << " " << (edgeCirculator->source()->point().y() * Kernel::FT(10)); if (first) { ipeFileStream << " m\n"; first = false; } else { ipeFileStream << " l\n"; } } while (++edgeCirculator != startEdgeCirc); ipeFileStream << "h\n"; //Polygon closing tag ipeFileStream << "\n"; } } ipeFileStream << ipeFileEnd; } //Thank you https://stackoverflow.com/a/7128796 PricklinessArrangement::PricklyColor PricklinessArrangement::GetColorForPercentage(double percent) const { static std::vector> percentColors { { 0.0, { 1.0, 1.0, 1.0 }}, { 0.125, { 0.929411765, 0.97254902, 0.694117647 }}, { 0.25, { 0.780392157, 0.91372549, 0.705882353}}, { 0.375, { 0.498039216, 0.803921569, 0.733333333}}, { 0.5, { 0.254901961, 0.71372549, 0.768627451}}, { 0.625, { 0.11372549, 0.568627451, 0.752941176}}, { 0.75, { 0.133333333, 0.368627451, 0.658823529}}, { 0.875, { 0.145098039, 0.203921569, 0.580392157}}, { 1.0, { 0.031372549, 0.11372549, 0.345098039}} }; int i = 1; for (; i < percentColors.size() - 1; i++) { if (percent < percentColors.at(i).first) { break; } } std::pair lower = percentColors.at(i - 1); std::pair upper = percentColors.at(i); double range = upper.first - lower.first; double rangePercent = (percent - lower.first) / range; double percentLower = 1 - rangePercent; double percentUpper = rangePercent; PricklyColor color; color.r = lower.second.r * percentLower + upper.second.r * percentUpper; color.g = lower.second.g * percentLower + upper.second.g * percentUpper; color.b = lower.second.b * percentLower + upper.second.b * percentUpper; return color; } }