#include "path_extraction.h" #include "parameterization.h" #include "stretch_angles.h" #include "stripe_patterns.h" #include "timer.h" #include #include #include #include #include #include #ifdef USE_ORTOOLS #include #include #include #include #endif #ifdef USE_PARDISO #include using LDLTSolver = Eigen::PardisoLDLT>; using LLTSolver = Eigen::PardisoLLT>; using LUSolver = Eigen::PardisoLU>; #else using LDLTSolver = Eigen::SimplicialLDLT>; using LLTSolver = Eigen::SimplicialLLT>; using LUSolver = Eigen::SparseLU>; #endif // USE_PARDISO using namespace geometrycentral; using namespace geometrycentral::surface; namespace { #ifdef USE_ORTOOLS std::vector computeReordering(const std::vector& endPoints, double timeLimit) { using namespace operations_research; // Create the VRP routing model. The 1 means we are only looking for a single path. int nPaths = endPoints.size() / 4; RoutingIndexManager manager(2 * nPaths + 1, 1, RoutingIndexManager::NodeIndex{2 * nPaths}); RoutingModel routing(manager); // For every path node, add a disjunction so that we do not also trace its reverse for(int64_t i = 0; i < nPaths; ++i) routing.AddDisjunction({2 * i, 2 * i + 1}); // Wrap the distance function so that it converts to an integer, as or-tools requires. // Distances are rounded to the nearest nanometer value, should be precise enough const double COST_MULTIPLIER = 1e6; int64_t transitCallbackIndex = routing.RegisterTransitCallback([&](int64_t i, int64_t j) -> int64_t { int64_t fromNode = manager.IndexToNode(i).value(); int64_t toNode = manager.IndexToNode(j).value(); if(fromNode == 2 * nPaths || toNode == 2 * nPaths) return 0; double dist = (endPoints[2 * fromNode + 1] - endPoints[2 * toNode]).norm(); return static_cast(COST_MULTIPLIER * dist); }); routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); RoutingSearchParameters searchParameters = DefaultRoutingSearchParameters(); searchParameters.mutable_time_limit()->set_seconds(timeLimit); searchParameters.set_solution_limit(kint64max); searchParameters.set_first_solution_strategy(FirstSolutionStrategy::SAVINGS); searchParameters.set_local_search_metaheuristic(LocalSearchMetaheuristic::GUIDED_LOCAL_SEARCH); // solve routing problem auto finalAssignment = std::make_unique(routing.SolveWithParameters(searchParameters)); // Iterate over the result std::vector result; result.reserve(nPaths); int64_t index = routing.Start(0); for(int i = 0; i < nPaths; ++i) { index = manager.IndexToNode(finalAssignment->Value(routing.NextVar(index))).value(); assert(index <= 2 * nPaths); result.push_back(index); } // build polyline list from the list of endpoint indices return result; } #else std::vector greedyIndexing(const std::vector& endPoints, EmbeddedGeometryInterface& geometry) { // find closest end-point to home Vector3 homePos = {-80, -80}; double minDist = std::numeric_limits::max(); int minIdx; for(int j = 0; j < endPoints.size(); ++j) { if((endPoints[j] - homePos).norm() < minDist) { minDist = (endPoints[j] - homePos).norm(); minIdx = j; } } // indices into endPoints paired with the direction of isoline (true = standard, false = reversed) std::vector endPointIndices(endPoints.size() / 2); // tells whether isolines[i] has been visited or not std::vector visited(endPoints.size() / 2, false); visited[minIdx / 2] = true; // order end-point indices by proximity endPointIndices[0] = minIdx; Vector3 current; if(minIdx % 2 == 0) current = endPoints[minIdx + 1]; else current = endPoints[minIdx - 1]; for(int i = 1; i < endPoints.size() / 2; ++i) { // find closest point to current double minDist = std::numeric_limits::max(); int minIdx; for(int j = 0; j < endPoints.size(); ++j) { if(!visited[j / 2] && (current - endPoints[j]).norm() < minDist) { minDist = (current - endPoints[j]).norm(); minIdx = j; } } // list index of isoline + whether it should be reversed or not endPointIndices[i] = minIdx; // set current to the other end-point if(minIdx % 2 == 0) current = endPoints[minIdx + 1]; else current = endPoints[minIdx - 1]; visited[minIdx / 2] = true; } return endPointIndices; } #endif } // namespace std::vector> orderPolylines(const std::vector>& isolines, EmbeddedGeometryInterface& geometry, double timeLimit) { #ifdef USE_ORTOOLS // build list of isoline end-points, enumerated twice (one for each direction) std::vector endPoints(4 * isolines.size()); for(size_t i = 0; i < isolines.size(); ++i) { endPoints[4 * i] = isolines[i][0]; endPoints[4 * i + 1] = isolines[i][isolines[i].size() - 1]; endPoints[4 * i + 2] = endPoints[4 * i + 1]; endPoints[4 * i + 3] = endPoints[4 * i]; } // TSP solving std::vector indices = computeReordering(endPoints, timeLimit); #else std::vector endPoints(2 * isolines.size()); for(int i = 0; i < isolines.size(); ++i) { endPoints[2 * i] = isolines[i][0]; endPoints[2 * i + 1] = isolines[i][isolines[i].size() - 1]; } std::vector indices = greedyIndexing(endPoints, geometry); #endif // assemble final polyline list std::vector> polylines(isolines.size()); for(size_t i = 0; i < isolines.size(); ++i) { auto isoline = isolines.at(indices[i] / 2); if(indices[i] % 2 == 0) { polylines[i] = isoline; } else // enumerate vertices from last to first { polylines[i].reserve(isoline.size()); for(int j = isoline.size() - 1; j >= 0; --j) { polylines[i].push_back(isoline[j]); } } } return polylines; } std::vector> simplifyPolylines(const std::vector>& polylines, double z) { std::vector> simplified; for(auto& polyline: polylines) { // convert to Eigen Eigen::MatrixXd P(polyline.size(), 2); for(size_t i = 0; i < polyline.size(); ++i) P.row(i) << polyline[i].x, polyline[i].y; // simplify polyline (RDP) Eigen::MatrixXd S; Eigen::VectorXi J; igl::ramer_douglas_peucker(P, 1e-2, S, J); // convert back to geometrycentral std::vector simplifiedPolyline(S.rows()); for(int i = 0; i < S.rows(); ++i) simplifiedPolyline[i] = Vector3{S(i, 0), S(i, 1), z}; simplified.push_back(simplifiedPolyline); } return simplified; } void writePaths(const std::string& filename, const std::vector>& paths, double height) { std::ofstream s(filename, std::ios_base::app); for(const auto& path: paths) { s << path.size() << "\n"; for(Vector3 p: path) s << p.x << " " << p.y << " " << height << "\n"; } } void drawPathsAndTravels(const std::vector>& polylines, double spacing, int id) { std::vector nodes; std::vector> edges; std::vector colors; double c = 0; size_t k = 0; for(auto& polyline: polylines) { nodes.insert(nodes.end(), polyline.begin(), polyline.end()); colors.insert(colors.end(), polyline.size() - 1, c); c += 1; for(size_t j = 0; j < polyline.size() - 1; ++j) { edges.push_back({k + j, k + j + 1}); } k += polyline.size(); } std::vector nodes2; std::vector> edges2; double distance = 0; for(size_t i = 0; i < polylines.size() - 1; ++i) { nodes2.push_back(polylines[i][polylines[i].size() - 1]); nodes2.push_back(polylines[i + 1][0]); edges2.push_back({2 * i, 2 * i + 1}); distance += norm(polylines[i][polylines[i].size() - 1] - polylines[i + 1][0]); } std::cout << "Layer " << id << ": total travel distance = " << distance << "mm.\n"; polyscope::registerCurveNetwork("Layer " + std::to_string(id), nodes, edges)->setRadius(spacing / 2, false); polyscope::getCurveNetwork("Layer " + std::to_string(id)) ->addEdgeScalarQuantity("order", colors) ->setColorMap("blues") ->setEnabled(true); polyscope::registerCurveNetwork("Travel " + std::to_string(id), nodes2, edges2) ->setRadius(spacing / 2, false) ->setEnabled(false); } void stripePattern(VertexPositionGeometry& geometry, const Eigen::MatrixXd& _V, Eigen::MatrixXd& _P, const Eigen::MatrixXi& _F, VertexData& vTheta2, std::string filename, double timeLimit, double layerHeight, double spacing, int nLayers) { Eigen::MatrixXd V = _V; Eigen::MatrixXi F = _F; std::vector> subdivMat; subdivideMesh(geometry, V, _P, F, subdivMat, spacing); ManifoldSurfaceMesh subdividedMesh(F); Eigen::MatrixXd P_3D(_P.rows(), 3); P_3D.leftCols(2) = _P; P_3D.col(2).setZero(); VertexPositionGeometry geometryUV(subdividedMesh, P_3D); FaceData theta1 = computeStretchAngles(subdividedMesh, V, _P, F); // convert face angles into vertex angles Eigen::VectorXd th1(V.rows()); for(int i = 0; i < V.rows(); ++i) { double sumAngles = 0; int nFaces = 0; Vertex v = subdividedMesh.vertex(i); for(Face f: v.adjacentFaces()) { // add face orientations in global coordinates if(!f.isBoundaryLoop()) { sumAngles += theta1[f]; nFaces += 1; } } th1(i) = sumAngles / nFaces; } Eigen::VectorXd th2 = vTheta2.toVector(); for(auto& mat: subdivMat) th2 = mat * th2; std::vector>> paths = generatePaths(geometryUV, th1, th2, layerHeight, nLayers, spacing, timeLimit); for(int i = 0; i < nLayers; ++i) { writePaths(filename + ".path", paths[i], (i + 1) * layerHeight); drawPathsAndTravels(paths[i], spacing, i + 1); } } std::vector>> generatePaths(EmbeddedGeometryInterface& geometry, const Eigen::VectorXd& theta1, const Eigen::VectorXd& theta2, double layerHeight, int nLayers, double spacing, double timeLimit) { std::vector>> paths; LDLTSolver solver; SparseMatrix massMatrix = computeRealVertexMassMatrix(geometry); SurfaceMesh& mesh = geometry.mesh; VertexData frequencies(mesh, 1.0 / spacing); Vector u = Vector::Random(mesh.nVertices() * 2); for(int i = 0; i < nLayers / 2; ++i) { Timer timer("Layers " + std::to_string(2 * i) + " and " + std::to_string(2 * i + 1)); geometry.requireVertexTangentBasis(); // compute direction field VertexData directionField(mesh); for(size_t j = 0; j < mesh.nVertices(); ++j) { // interpolate orientations w.r.t layer directionField[j] = Vector2::fromAngle(2 * (theta1(j) + (i / (nLayers / 2 - 1.) - 1 / 2.) * theta2(j))); // express vectors in their respective vertex local bases (the stripes algorithm expects that) auto basisVector = geometry.vertexTangentBasis[j][0]; Vector2 base{basisVector.x, basisVector.y}; directionField[j] = -directionField[j] / base.pow(2); } // build matrices and factorize solver FaceData fieldIndices = computeFaceIndex(geometry, directionField, 2); SparseMatrix energyMatrix = buildVertexEnergyMatrix(geometry, directionField, fieldIndices, 2 * PI * frequencies); if(i == 0) solver.compute(energyMatrix); else solver.factorize(energyMatrix); // solve the eigenvalue problem Vector x = u; double residual = eigenvectorResidual(energyMatrix, massMatrix, x); const double tol = 1e-5; while(residual > tol) { // Solve Vector rhs = massMatrix * u; x = solver.solve(rhs); // Re-normalize double scale = std::sqrt(std::abs(x.dot(massMatrix * x))); x /= scale; // Update u = x; residual = eigenvectorResidual(energyMatrix, massMatrix, x); } // Copy the result to a VertexData vector VertexData parameterization(mesh); for(size_t j = 0; j < mesh.nVertices(); ++j) { parameterization[j].x = u(2 * j); parameterization[j].y = u(2 * j + 1); } CornerData stripeValues; FaceData stripeIndices; std::tie(stripeValues, stripeIndices) = computeTextureCoordinates(geometry, directionField, 2 * PI * frequencies, parameterization); // extract isolines for(int j = 0; j < 2; ++j) { const auto& [points, edges] = extractPolylinesFromStripePattern(geometry, stripeValues + j * PI, stripeIndices, fieldIndices, directionField); auto polylines = edgeToPolyline(points, edges); polylines = orderPolylines(polylines, geometry, timeLimit); paths.push_back(simplifyPolylines(polylines, (2 * i + j) * layerHeight)); } } return paths; } std::vector> edgeToPolyline(const std::vector& points, const std::vector>& edges) { // build up connectivity information (neighoring indices on polyline) std::vector> connectivity(points.size()); for(auto& edge: edges) { connectivity[edge[0]].push_back(edge[1]); connectivity[edge[1]].push_back(edge[0]); } std::vector> polylines; std::vector visited(points.size(), false); for(size_t i = 0; i < points.size(); ++i) if(!visited[i]) { std::vector isoline; size_t nbOfPieces = connectivity[i].size(); if(nbOfPieces == 0) continue; // ignore isolated points size_t currIdx = connectivity[i][0]; size_t prevIdx = i; bool open = true; if(nbOfPieces == 2) // walk to the end of the line (if open) { while(connectivity[currIdx].size() == 2 && currIdx != i) { if(connectivity[currIdx][0] == prevIdx) { prevIdx = currIdx; currIdx = connectivity[currIdx][1]; } else { prevIdx = currIdx; currIdx = connectivity[currIdx][0]; } } open = currIdx != i; std::swap(prevIdx, currIdx); // revert the order to start from this end } isoline.push_back(points[prevIdx]); visited[prevIdx] = true; while((connectivity[currIdx].size() == 2 && open) || (currIdx != i && !open)) { isoline.push_back(points[currIdx]); visited[currIdx] = true; if(connectivity[currIdx][0] == prevIdx) { prevIdx = currIdx; currIdx = connectivity[currIdx][1]; } else { prevIdx = currIdx; currIdx = connectivity[currIdx][0]; } } assert(connectivity[currIdx].size() == 1 || currIdx == i && !open); isoline.push_back(points[currIdx]); visited[currIdx] = true; polylines.push_back(isoline); } return polylines; }