diff --git a/src/Mod/CAM/App/CMakeLists.txt b/src/Mod/CAM/App/CMakeLists.txt index 3eda1821b5..4ec9936d3c 100644 --- a/src/Mod/CAM/App/CMakeLists.txt +++ b/src/Mod/CAM/App/CMakeLists.txt @@ -138,3 +138,15 @@ SET_BIN_DIR(Path PathApp /Mod/CAM) SET_PYTHON_PREFIX_SUFFIX(Path) INSTALL(TARGETS Path DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +# --- TSP Solver Python module (pybind11) --- +find_package(pybind11 REQUIRED) + +pybind11_add_module(tsp_solver MODULE tsp_solver_pybind.cpp tsp_solver.cpp) +target_include_directories(tsp_solver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +SET_BIN_DIR(tsp_solver tsp_solver /Mod/CAM) +SET_PYTHON_PREFIX_SUFFIX(tsp_solver) + +INSTALL(TARGETS tsp_solver DESTINATION ${CMAKE_INSTALL_LIBDIR}) +# ------------------------------------------- diff --git a/src/Mod/CAM/App/tsp_solver.cpp b/src/Mod/CAM/App/tsp_solver.cpp new file mode 100644 index 0000000000..ea8aec161e --- /dev/null +++ b/src/Mod/CAM/App/tsp_solver.cpp @@ -0,0 +1,105 @@ +/*************************************************************************** + * Copyright (c) 2025 Billy Huddleston * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License (LGPL) * + * as published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * for detail see the LICENCE text file. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the Free Software * + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + * * + ***************************************************************************/ + +#include "tsp_solver.h" +#include +#include +#include +#include +#include + +namespace +{ +// Euclidean distance between two points +double dist(const TSPPoint& a, const TSPPoint& b) +{ + double dx = a.x - b.x; + double dy = a.y - b.y; + return std::sqrt(dx * dx + dy * dy); +} + +// Calculate total path length +double pathLength(const std::vector& points, const std::vector& path) +{ + double total = 0.0; + size_t n = path.size(); + for (size_t i = 0; i < n - 1; ++i) { + total += dist(points[path[i]], points[path[i + 1]]); + } + // Optionally, close the loop: total += dist(points[path[n-1]], points[path[0]]); + return total; +} + +// 2-Opt swap +void twoOptSwap(std::vector& path, size_t i, size_t k) +{ + std::reverse(path.begin() + static_cast(i), path.begin() + static_cast(k) + 1); +} +} // namespace + +std::vector TSPSolver::solve(const std::vector& points) +{ + size_t n = points.size(); + if (n == 0) { + return {}; + } + // Start with a simple nearest neighbor path + std::vector visited(n, false); + std::vector path; + size_t current = 0; + path.push_back(static_cast(current)); + visited[current] = true; + for (size_t step = 1; step < n; ++step) { + double min_dist = std::numeric_limits::max(); + size_t next = n; // Use n as an invalid index + for (size_t i = 0; i < n; ++i) { + if (!visited[i]) { + double d = dist(points[current], points[i]); + if (d < min_dist) { + min_dist = d; + next = i; + } + } + } + current = next; + path.push_back(static_cast(current)); + visited[current] = true; + } + + // 2-Opt optimization + bool improved = true; + while (improved) { + improved = false; + for (size_t i = 1; i < n - 1; ++i) { + for (size_t k = i + 1; k < n; ++k) { + double delta = dist(points[path[i - 1]], points[path[k]]) + + dist(points[path[i]], points[path[(k + 1) % n]]) + - dist(points[path[i - 1]], points[path[i]]) + - dist(points[path[k]], points[path[(k + 1) % n]]); + if (delta < -Base::Precision::Confusion()) { + twoOptSwap(path, i, k); + improved = true; + } + } + } + } + return path; +} diff --git a/src/Mod/CAM/App/tsp_solver.h b/src/Mod/CAM/App/tsp_solver.h new file mode 100644 index 0000000000..adf17f6497 --- /dev/null +++ b/src/Mod/CAM/App/tsp_solver.h @@ -0,0 +1,42 @@ +/*************************************************************************** + * Copyright (c) 2025 Billy Huddleston * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License (LGPL) * + * as published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * for detail see the LICENCE text file. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the Free Software * + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + * * + ***************************************************************************/ + +#pragma once +#include +#include +#include +#include + +struct TSPPoint +{ + double x, y; + TSPPoint(double x_, double y_) + : x(x_) + , y(y_) + {} +}; + +class TSPSolver +{ +public: + // Returns a vector of indices representing the visit order using 2-Opt + static std::vector solve(const std::vector& points); +}; diff --git a/src/Mod/CAM/App/tsp_solver_pybind.cpp b/src/Mod/CAM/App/tsp_solver_pybind.cpp new file mode 100644 index 0000000000..d742ccc69e --- /dev/null +++ b/src/Mod/CAM/App/tsp_solver_pybind.cpp @@ -0,0 +1,44 @@ +/*************************************************************************** + * Copyright (c) 2025 Billy Huddleston * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License (LGPL) * + * as published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * for detail see the LICENCE text file. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this program; if not, write to the Free Software * + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + * * + ***************************************************************************/ + +#include +#include +#include "tsp_solver.h" + +namespace py = pybind11; + +std::vector tspSolvePy(const std::vector>& points) +{ + std::vector pts; + for (const auto& p : points) { + pts.emplace_back(p.first, p.second); + } + return TSPSolver::solve(pts); +} + +PYBIND11_MODULE(tsp_solver, m) +{ + m.doc() = "Simple TSP solver (2-Opt) for FreeCAD"; + m.def("solve", + &tspSolvePy, + py::arg("points"), + "Solve TSP for a list of (x, y) points using 2-Opt, returns visit order"); +} diff --git a/src/Mod/CAM/PathScripts/PathUtils.py b/src/Mod/CAM/PathScripts/PathUtils.py index 02e03a0544..cfdd8f35c1 100644 --- a/src/Mod/CAM/PathScripts/PathUtils.py +++ b/src/Mod/CAM/PathScripts/PathUtils.py @@ -2,6 +2,7 @@ # *************************************************************************** # * Copyright (c) 2014 Dan Falck * +# * Copyright (c) 2025 Billy Huddleston * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * @@ -29,6 +30,7 @@ import Path import Path.Main.Job as PathJob import math from numpy import linspace +import tsp_solver # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader @@ -611,6 +613,17 @@ def sort_locations(locations, keys, attractors=None): return out +def sort_locations_tsp(locations, keys, attractors=None): + """ + Python wrapper for the C++ TSP solver. Takes a list of dicts (locations), + a list of keys (e.g. ['x', 'y']), and optional attractors. + Returns the sorted list of locations in TSP order, starting at (0,0). + """ + points = [(loc[keys[0]], loc[keys[1]]) for loc in locations] + order = tsp_solver.solve(points) + return [locations[i] for i in order] + + def guessDepths(objshape, subs=None): """ takes an object shape and optional list of subobjects and returns a depth_params