diff --git a/.github/workflows/deploy-linux-appimage.yml b/.github/workflows/deploy-linux-appimage.yml index 13324ef..b63e423 100644 --- a/.github/workflows/deploy-linux-appimage.yml +++ b/.github/workflows/deploy-linux-appimage.yml @@ -8,17 +8,19 @@ on: jobs: deploy: runs-on: ubuntu-latest + container: + image: ghcr.io/${{ github.repository }}/ubuntu:feature-docker_build_appimage + options: --cap-add SYS_ADMIN --device /dev/fuse --security-opt apparmor:unconfined steps: - name: Checkout code uses: actions/checkout@v2 with: submodules: true - - name: Install package - run: | - sudo apt-get update - sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev + fetch-depth: 0 - name: Build and test run: ci/buildappimage.sh + env: + LD_LIBRARY_PATH: /opt/lib - name: Create Release uses: softprops/action-gh-release@v1 with: diff --git a/.github/workflows/docker-ubuntu.yml b/.github/workflows/docker-ubuntu.yml new file mode 100644 index 0000000..3369992 --- /dev/null +++ b/.github/workflows/docker-ubuntu.yml @@ -0,0 +1,42 @@ +on: + workflow_dispatch: + push: + paths: + - "ci/docker/ubuntu/**" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/ubuntu + +jobs: + build-ubuntu-docker: + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: ci/docker/ubuntu/ + file: ci/docker/ubuntu/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e0a0bff..3d9a5f1 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -7,23 +7,13 @@ on: jobs: static_analysis: runs-on: ubuntu-latest + container: + image: ghcr.io/${{ github.repository }}/ubuntu:develop steps: - name: Checkout uses: actions/checkout@v2 with: submodules: true - - name: Install package - run: | - sudo apt-get update - sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev lcov - - name: Install build wrapper - run: | - wget http://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip - unzip build-wrapper-linux-x86.zip - - name: Install sonar scanner - run: | - wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.6.2.2472-linux.zip - unzip sonar-scanner-cli-4.6.2.2472-linux.zip - name: Build and scan run: ci/buildsonarcloud.sh env: diff --git a/.gitmodules b/.gitmodules index c4252ce..7b93189 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "thirdparty/fmt"] path = thirdparty/fmt url = https://github.com/fmtlib/fmt.git +[submodule "thirdparty/or-tools"] + path = thirdparty/or-tools + url = https://github.com/google/or-tools.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 62fd227..9b8a036 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.8) project(dxfplotter) @@ -18,6 +18,12 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(BUILD_TESTS OFF) +set(BUILD_SAMPLES OFF) +set(BUILD_EXAMPLES OFF) +set(USE_SCIP OFF) +set(USE_COINOR OFF) +set(BUILD_FLATZINC OFF) +set(USE_HIGHS OFF) set(JUST_INSTALL_CEREAL ON) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") @@ -26,7 +32,7 @@ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") if(MSVC) add_compile_options(/W4) else() - add_compile_options(-fPIC -Wall -Wextra) + add_compile_options(-fPIC -Wall -Wextra -DNDEBUG) endif() set(CMAKE_CXX_STANDARD 17) @@ -37,8 +43,9 @@ set(TEMPLATE_DIR ${PROJECT_SOURCE_DIR}/template) find_package(codecov) find_package(PythonInterp REQUIRED) +find_package(ortools CONFIG REQUIRED) -find_package(Qt5 COMPONENTS REQUIRED +find_package(Qt6 COMPONENTS REQUIRED Core Widgets Gui @@ -47,7 +54,7 @@ find_package(Qt5 COMPONENTS REQUIRED 3DExtras ) -#qt_standard_project_setup() +qt_standard_project_setup() set(INCLUDE_DIRS src @@ -58,13 +65,14 @@ set(INCLUDE_DIRS thirdparty/nanoflann/include thirdparty/units/include thirdparty/yaml-cpp/include + thirdparty/or-tools template ${CMAKE_BINARY_DIR}/src ${CMAKE_BINARY_DIR}/template - ${Qt5Widgets_INCLUDE_DIRS} - ${Qt5Gui_INCLUDE_DIRS} - ${Qt53DCore_INCLUDE_DIRS} - ${Qt53DExtras_INCLUDE_DIRS} + ${Qt6Widgets_INCLUDE_DIRS} + ${Qt6Gui_INCLUDE_DIRS} + ${Qt63DCore_INCLUDE_DIRS} + ${Qt63DExtras_INCLUDE_DIRS} ) set(LINK_LIBRARIES @@ -87,10 +95,10 @@ set(LINK_LIBRARIES geometry-filter libdxfrw fmt::fmt - Qt5::Widgets - Qt5::Svg - Qt5::3DCore - Qt5::3DExtras + Qt6::Widgets + Qt6::Svg + Qt6::3DCore + Qt6::3DExtras yaml-cpp ) diff --git a/ci/buildappimage.sh b/ci/buildappimage.sh index 2a7e075..c84a546 100755 --- a/ci/buildappimage.sh +++ b/ci/buildappimage.sh @@ -25,6 +25,8 @@ trap cleanup EXIT REPO_ROOT=$(readlink -f $(dirname $(dirname $0))) OLD_CWD=$(readlink -f .) +git config --global --add safe.directory $REPO_ROOT + # generate release name COMMIT=$(git rev-parse --short HEAD) TAG=$(git describe --tags) @@ -35,7 +37,7 @@ pushd "$BUILD_DIR" # configure build files with CMake # we need to explicitly set the install prefix, as CMake's default is /usr/local for some reason... -cmake "$REPO_ROOT" -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_TESTING=OFF +cmake "$REPO_ROOT" -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_TESTING=OFF -DQt6_DIR=/opt/qt/6.8.2/gcc_64/lib/cmake/Qt6 # build project and install files into AppDir make -j$(nproc) diff --git a/ci/buildsonarcloud.sh b/ci/buildsonarcloud.sh index 3a2b175..f73d439 100755 --- a/ci/buildsonarcloud.sh +++ b/ci/buildsonarcloud.sh @@ -30,11 +30,11 @@ pushd "$BUILD_DIR" # configure build files with CMake # we need to explicitly set the install prefix, as CMake's default is /usr/local for some reason... -cmake "$REPO_ROOT" -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON +cmake "$REPO_ROOT" -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON -DQt6_DIR=/opt/qt/6.8.2/gcc_64/lib/cmake/Qt6 # Wraps the compilation with the Build Wrapper to generate configuration (used # later by the SonarQube Scanner) into the "bw-output" folder -"$REPO_ROOT"/build-wrapper-linux-x86/build-wrapper-linux-x86-64 \ +/opt/build-wrapper-linux-x86/build-wrapper-linux-x86-64 \ --out-dir bw-output cmake \ --build . # Test project @@ -44,6 +44,6 @@ ctest -VV make gcov # Scan project -"$REPO_ROOT"/sonar-scanner-4.6.2.2472-linux/bin/sonar-scanner -Dsonar.host.url=https://sonarcloud.io -Dproject.settings="$REPO_ROOT"/sonar-project.properties -Dsonar.projectBaseDir="$REPO_ROOT" -Dsonar.cfamily.gcov.reportsPath="$BUILD_DIR" +/opt/sonar-scanner-6.2.1.4610-linux-x64/bin/sonar-scanner -Dsonar.host.url=https://sonarcloud.io -Dproject.settings="$REPO_ROOT"/sonar-project.properties -Dsonar.projectBaseDir="$REPO_ROOT" -Dsonar.cfamily.gcov.reportsPath="$BUILD_DIR" diff --git a/ci/docker/ubuntu/Dockerfile b/ci/docker/ubuntu/Dockerfile new file mode 100644 index 0000000..41e0224 --- /dev/null +++ b/ci/docker/ubuntu/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:noble + +RUN apt update && apt install -y libglib2.0-bin libdbus-1-3 libgtk-3-0 \ + libxcb* libxkbcommon* \ + freeglut3-dev lcov \ + build-essential cmake \ + fuse file + +RUN apt update && apt install -y wget unzip git python3-jinja2 python3-pip +RUN pip install aqtinstall --break-system-packages + +RUN aqt install-qt -O /opt/qt linux desktop 6.8.2 -m qt3d qtshadertools +ENV PATH="/opt/qt/6.8.2/gcc_64/bin/:$PATH" + +RUN wget http://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip +RUN unzip build-wrapper-linux-x86.zip -d /opt + +RUN wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-6.2.1.4610-linux-x64.zip +RUN unzip sonar-scanner-cli-6.2.1.4610-linux-x64.zip -d /opt + +RUN wget https://github.com/google/or-tools/releases/download/v9.10/or-tools_amd64_ubuntu-24.04_cpp_v9.10.4067.tar.gz +RUN tar -C /opt --strip-components=1 -xvf or-tools_amd64_ubuntu-24.04_cpp_v9.10.4067.tar.gz + +RUN git config --global --add safe.directory '*' diff --git a/cmake/FindLcov.cmake b/cmake/FindLcov.cmake index 244d1c2..d4e0188 100644 --- a/cmake/FindLcov.cmake +++ b/cmake/FindLcov.cmake @@ -169,7 +169,7 @@ function (lcov_capture_initial_tgt TNAME) list(APPEND GENINFO_FILES ${OUTFILE}) add_custom_command(OUTPUT ${OUTFILE} COMMAND ${GCOV_ENV} ${GENINFO_BIN} - --quiet --base-directory ${PROJECT_SOURCE_DIR} --initial + --quiet --ignore-errors mismatch --base-directory ${PROJECT_SOURCE_DIR} --initial --gcov-tool ${GCOV_BIN} --output-filename ${OUTFILE} ${GENINFO_EXTERN_FLAG} ${TDIR}/${FILE}.gcno DEPENDS ${TNAME} @@ -269,7 +269,7 @@ function (lcov_capture_tgt TNAME) add_custom_command(OUTPUT ${OUTFILE} COMMAND test -s "${TDIR}/${FILE}.gcda" - && ${GCOV_ENV} ${GENINFO_BIN} --quiet --base-directory + && ${GCOV_ENV} ${GENINFO_BIN} --quiet --ignore-errors mismatch --base-directory ${PROJECT_SOURCE_DIR} --gcov-tool ${GCOV_BIN} --output-filename ${OUTFILE} ${GENINFO_EXTERN_FLAG} ${TDIR}/${FILE}.gcda diff --git a/resource/CMakeLists.txt b/resource/CMakeLists.txt index 70b1618..c4eb371 100644 --- a/resource/CMakeLists.txt +++ b/resource/CMakeLists.txt @@ -1,4 +1,4 @@ -qt5_add_resources(SOURCES resource.qrc) +qt_add_resources(SOURCES resource.qrc) add_library(resource ${SOURCES}) diff --git a/resource/icons/layer-bottom.svg b/resource/icons/layer-bottom.svg new file mode 100644 index 0000000..709d1cc --- /dev/null +++ b/resource/icons/layer-bottom.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/resource/icons/layer-top.svg b/resource/icons/layer-top.svg new file mode 100644 index 0000000..bb9952d --- /dev/null +++ b/resource/icons/layer-top.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/resource/icons/optimize-order.svg b/resource/icons/optimize-order.svg new file mode 100644 index 0000000..ecf9eaa --- /dev/null +++ b/resource/icons/optimize-order.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/resource/resource.qrc b/resource/resource.qrc index 3d4bac2..1fa56d7 100644 --- a/resource/resource.qrc +++ b/resource/resource.qrc @@ -13,12 +13,15 @@ icons/edit-copy.svg icons/go-down.svg icons/go-up.svg + icons/layer-bottom.svg icons/layer-lower.svg icons/layer-raise.svg + icons/layer-top.svg icons/layer-visible-off.svg icons/layer-visible-on.svg icons/list-add.svg icons/list-remove.svg + icons/optimize-order.svg icons/path-inset.svg icons/path-outset.svg icons/playback-pause.svg diff --git a/src/common/copy.h b/src/common/copy.h new file mode 100644 index 0000000..3c8dd67 --- /dev/null +++ b/src/common/copy.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +namespace common +{ + +template > +std::vector deepcopy(const std::vector &other) +{ + std::vector duplicated(other.size()); + std::transform(other.begin(), other.end(), duplicated.begin(), [](const ItemUPtr &item){ + return std::make_unique(*item); + }); + + return duplicated; +} + +} diff --git a/src/config/config.cpp b/src/config/config.cpp index 35ca952..f40ac7a 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -30,7 +30,6 @@ Config::Config(const Config &other) Config::~Config() { - save(); } Root &Config::root() diff --git a/src/exporter/gcode/metadata.h b/src/exporter/gcode/metadata.h index 10ff84a..8161304 100644 --- a/src/exporter/gcode/metadata.h +++ b/src/exporter/gcode/metadata.h @@ -13,6 +13,8 @@ class Document; } class QJsonObject; +class QJsonArray; +class QJsonDocument; namespace exporter::gcode { diff --git a/src/exporter/renderer/renderer.h b/src/exporter/renderer/renderer.h index 228906b..8948354 100644 --- a/src/exporter/renderer/renderer.h +++ b/src/exporter/renderer/renderer.h @@ -12,7 +12,9 @@ class Renderer private: const config::Tools::Tool &m_tool; const config::Profiles::Profile &m_profile; + const bool m_laser; const float m_depthPerCut; + const float m_maximumStartDepth; const float m_depthToRetract; Visitor &m_visitor; @@ -49,7 +51,6 @@ class Renderer void render(const geometry::Polyline &polyline, const model::PathSettings &settings, geometry::CuttingDirection cuttingDirection) const { - const float maxDepth = settings.depth(); const float intensity = settings.intensity(); const float planeFeedRate = settings.planeFeedRate(); const float depthFeedRate = settings.depthFeedRate(); @@ -58,10 +59,17 @@ class Renderer m_visitor.startOperation((*iterator).start(), intensity); - for (float depth = 0.0f; depth < maxDepth + m_depthPerCut; depth += m_depthPerCut, ++iterator) { - const float boundDepth = std::fminf(depth, maxDepth); + if (m_laser) { + m_visitor.processPathAtDepth(*iterator, 0.0f, planeFeedRate, depthFeedRate); + } + else { + const float maxDepth = settings.depth(); + const float startDepth = std::min(m_maximumStartDepth, maxDepth); + for (float depth = startDepth; depth < maxDepth + m_depthPerCut; depth += m_depthPerCut, ++iterator) { + const float boundDepth = std::fminf(depth, maxDepth); - m_visitor.processPathAtDepth(*iterator, -boundDepth, planeFeedRate, depthFeedRate); + m_visitor.processPathAtDepth(*iterator, -boundDepth, planeFeedRate, depthFeedRate); + } } m_visitor.endOperation(m_depthToRetract); @@ -71,8 +79,10 @@ class Renderer explicit Renderer(const config::Tools::Tool& tool, const config::Profiles::Profile& profile, Visitor& visitor) :m_tool(tool), m_profile(profile), + m_laser(m_tool.general().laser()), m_depthPerCut(m_tool.general().depthPerCut()), - m_depthToRetract(m_tool.general().retractDepth()), + m_maximumStartDepth(m_profile.cut().passAtZeroDepth() ? 0.0f : m_depthPerCut), + m_depthToRetract(m_laser ? 0.0f : m_tool.general().retractDepth()), m_visitor(visitor) { } diff --git a/src/geometry/CMakeLists.txt b/src/geometry/CMakeLists.txt index bafe5d0..c6572f3 100644 --- a/src/geometry/CMakeLists.txt +++ b/src/geometry/CMakeLists.txt @@ -8,6 +8,7 @@ set(SRC circle.cpp cubicspline.cpp line.cpp + orderoptimizer.cpp pocketer.cpp polyline.cpp quadraticspline.cpp @@ -21,6 +22,7 @@ set(SRC circle.h cubicspline.h line.h + orderoptimizer.h polyline.h quadraticspline.h spline.h @@ -29,4 +31,5 @@ set(SRC ) add_library(geometry ${SRC}) +target_link_libraries(geometry PUBLIC ortools::ortools) add_coverage(geometry) diff --git a/src/geometry/filter/CMakeLists.txt b/src/geometry/filter/CMakeLists.txt index 34aaa5b..be943ae 100644 --- a/src/geometry/filter/CMakeLists.txt +++ b/src/geometry/filter/CMakeLists.txt @@ -2,12 +2,10 @@ set(SRC assembler.cpp cleaner.cpp removeexactduplicate.cpp - sorter.cpp assembler.h cleaner.h removeexactduplicate.h - sorter.h ) add_library(geometry-filter ${SRC}) diff --git a/src/geometry/filter/assembler.h b/src/geometry/filter/assembler.h index ec552f3..001fe18 100644 --- a/src/geometry/filter/assembler.h +++ b/src/geometry/filter/assembler.h @@ -92,7 +92,7 @@ class Assembler const float coord[2] = {tip.point.x(), tip.point.y()}; // Nearest neighbour with distance. - std::array matchIndices; + std::array matchIndices; std::array matchDistances; // Search for the nearest neighbours. diff --git a/src/geometry/filter/sorter.cpp b/src/geometry/filter/sorter.cpp deleted file mode 100644 index 545e27f..0000000 --- a/src/geometry/filter/sorter.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include - -namespace geometry::filter -{ - -Sorter::Sorter(Polyline::List &&polylines) - :m_polylines(polylines.size()) -{ - struct PolylineLength - { - Polyline *polyline; - float length; - - PolylineLength() = default; - - explicit PolylineLength(Polyline &polyline) - :polyline(&polyline), - length(polyline.length()) - { - } - - bool operator<(const PolylineLength& other) const - { - return length < other.length; - } - }; - - std::vector polylinesLength(polylines.size()); - std::transform(polylines.begin(), polylines.end(), polylinesLength.begin(), - [](Polyline& polyline){ return PolylineLength(polyline); }); - - std::sort(polylinesLength.begin(), polylinesLength.end()); - - std::transform(polylinesLength.begin(), polylinesLength.end(), m_polylines.begin(), - [](PolylineLength& polylineLength){ return std::move(*polylineLength.polyline); }); -} - -Polyline::List &&Sorter::polylines() -{ - return std::move(m_polylines); -} - -} diff --git a/src/geometry/filter/sorter.h b/src/geometry/filter/sorter.h deleted file mode 100644 index 9f1253b..0000000 --- a/src/geometry/filter/sorter.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include - -namespace geometry::filter -{ - -/** @brief Sort polyline by length. - */ -class Sorter -{ -private: - Polyline::List m_polylines; - -public: - explicit Sorter(Polyline::List &&polylines); - - Polyline::List &&polylines(); -}; - -} diff --git a/src/geometry/orderoptimizer.cpp b/src/geometry/orderoptimizer.cpp new file mode 100644 index 0000000..38d2720 --- /dev/null +++ b/src/geometry/orderoptimizer.cpp @@ -0,0 +1,131 @@ +#include + +#include // TODO + +namespace geometry +{ + +int OrderOptimizer::nodeDistance(const Node& n1, const Node& n2) +{ + return (n1.position - n2.position).lengthSquared() * 1e4; // TODO mutiplier +} + +static int homeNodeId(int nbNodes) +{ + return nbNodes; +} + +OrderOptimizer::ModelBuilder::ModelBuilder(int nbNodes) + :circuit(builder.AddCircuitConstraint()), + arcsByNodes(nbNodes + 1), + nbNodes(nbNodes) +{ +} + +void OrderOptimizer::ModelBuilder::addArc(const Node& n1, const Node& n2) +{ + ortools::BoolVar literal = builder.NewBoolVar(); + circuit.AddArc(n1.id, n2.id, literal); + + const int distance = nodeDistance(n1, n2); + pathLength += literal * distance; + + arcsByNodes[n1.id].push_back({{}, n2.id, literal}); +} + +geometry::OrderOptimizer::Model OrderOptimizer::ModelBuilder::build() +{ + builder.Minimize(pathLength); + + return {builder.Build(), arcsByNodes, nbNodes}; +} + +OrderOptimizer::Model OrderOptimizer::buildModel(const NodesPerGroup& nodesPerGroup, int nbNodes) const +{ + ModelBuilder modelBuilder(nbNodes); + + const int nbGroup = nodesPerGroup.size(); + const int lastGroupId = nbGroup - 1; + + // Group intra connections + for (const Node::List& nodes : nodesPerGroup) { + for (const Node& n1 : nodes) { + for (const Node& n2 : nodes) { + if (n1 == n2) { + continue; + } + + modelBuilder.addArc(n1, n2); + } + } + } + + // Group inter connections with group of id + 1 + for (int groupId = 0; groupId < lastGroupId; ++groupId) { + const Node::List &curNodes = nodesPerGroup[groupId]; + const Node::List &nextNodes = nodesPerGroup[groupId + 1]; + + for (const Node& n1 : curNodes) { + for (const Node& n2 : nextNodes) { + modelBuilder.addArc(n1, n2); + } + } + } + + const Node home{{}, homeNodeId(nbNodes), {0.0f, 0.0f}}; + + // Home to first group nodes + const Node::List &firstGroupsNodes = nodesPerGroup.front(); + for (const Node& node : firstGroupsNodes) { + modelBuilder.addArc(home, node); + } + + // Last group nodes to home + const Node::List &lastGroupsNodes = nodesPerGroup.back(); + for (const Node& node : lastGroupsNodes) { + modelBuilder.addArc(node, home); + } + + return modelBuilder.build(); +} + +std::vector OrderOptimizer::solveAndExtractOrder(const Model& model) +{ + const ortools::CpSolverResponse response = ortools::Solve(model.proto); + + if (response.status() != ortools::CpSolverStatus::OPTIMAL && response.status() != ortools::CpSolverStatus::FEASIBLE) { + return {}; + } + + std::vector order; + + const ArcsByNodes arcsByNodes = model.arcsByNodes; + // Last node id is home + const int homeId = homeNodeId(model.nbNodes); + int curNodeId = homeId; + while (order.size() < homeId) { + for (const ArcTo& arc : arcsByNodes[curNodeId]) { + if (ortools::SolutionIntegerValue(response, arc.literal)) { + curNodeId = arc.target; + order.push_back(curNodeId); + continue; + } + } + } + + return order; +} + +OrderOptimizer::OrderOptimizer(const NodesPerGroup& nodesPerGroup, int nbNodes) +{ + const Model model = buildModel(nodesPerGroup, nbNodes); + m_order = solveAndExtractOrder(model); +} + +const std::vector &OrderOptimizer::order() const +{ + return m_order; +} + + +} diff --git a/src/geometry/orderoptimizer.h b/src/geometry/orderoptimizer.h new file mode 100644 index 0000000..484cfb7 --- /dev/null +++ b/src/geometry/orderoptimizer.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "ortools/sat/cp_model.h" + +namespace ortools = operations_research::sat; + +namespace geometry +{ + +class OrderOptimizer +{ +public: + struct Node : common::Aggregable + { + int id; + QVector2D position; + + inline bool operator==(const Node& other) const + { + return id == other.id; + } + }; + + using NodesPerGroup = std::vector; + +private: + std::vector m_order; + + static int nodeDistance(const Node& n1, const Node& n2); + + struct ArcTo : common::Aggregable + { + int target; + ortools::BoolVar literal; + }; + + using ArcsByNodes = std::vector; + + struct Model + { + ortools::CpModelProto proto; + ArcsByNodes arcsByNodes; + int nbNodes; + }; + + struct ModelBuilder + { + ortools::CpModelBuilder builder; + ortools::CircuitConstraint circuit; + ArcsByNodes arcsByNodes; + ortools::LinearExpr pathLength; + int nbNodes; + + explicit ModelBuilder(int nbNodes); + + void addArc(const Node& n1, const Node& n2); + Model build(); + }; + + Model buildModel(const NodesPerGroup& nodesPerGroup, int nbNodes) const; + std::vector solveAndExtractOrder(const Model& model); + +public: + explicit OrderOptimizer(const NodesPerGroup& nodesPerGroup, int nbNodes); + + const std::vector &order() const; +}; + +} diff --git a/src/importer/dxfplot/CMakeLists.txt b/src/importer/dxfplot/CMakeLists.txt index 9acad47..172c8b5 100644 --- a/src/importer/dxfplot/CMakeLists.txt +++ b/src/importer/dxfplot/CMakeLists.txt @@ -5,3 +5,4 @@ set(SRC ) add_library(importer-dxfplot ${SRC}) +add_dependencies(importer-dxfplot generate-config) diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt index 340dc4a..e653052 100644 --- a/src/model/CMakeLists.txt +++ b/src/model/CMakeLists.txt @@ -1,6 +1,7 @@ set(SRC application.cpp document.cpp + documenthistory.cpp layer.cpp offsettedpath.cpp path.cpp @@ -12,6 +13,7 @@ set(SRC application.h document.h + documenthistory.h layer.h path.h offsettedpath.h @@ -25,4 +27,5 @@ set(SRC add_library(model ${SRC}) add_dependencies(model generate-config) +target_link_libraries(model PUBLIC geometry) add_coverage(model) diff --git a/src/model/application.cpp b/src/model/application.cpp index 3daf544..1a1db4a 100644 --- a/src/model/application.cpp +++ b/src/model/application.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -40,6 +39,21 @@ static std::string configFilePath() return path.toStdString(); } +void Application::setOpenedDocument(Document::UPtr &&document) +{ + m_openedDocument = std::move(document); + m_documentHistory = std::make_unique(*m_openedDocument); + + emit newDocumentOpened(m_openedDocument.get()); +} + +void Application::setRestoredDocument(const Document &documentVersion) +{ + m_openedDocument = std::make_unique(documentVersion); + emit documentRestoredFromHistory(m_openedDocument.get()); +} + + QString Application::baseName(const QString& fileName) { const QFileInfo fileInfo(fileName); @@ -100,10 +114,6 @@ geometry::Polyline::List Application::postProcessImportedPolylines(geometry::Pol // Remove small bulges geometry::filter::Cleaner cleaner(assembler.polylines(), dxf.minimumPolylineLength(), dxf.minimumArcLength()); - if (dxf.sortPathByLength()) { - geometry::filter::Sorter sorter(cleaner.polylines()); - return sorter.polylines(); - } return cleaner.polylines(); } @@ -120,7 +130,14 @@ Task::UPtr Application::createTaskFromDxfImporter(const importer::dxf::Importer& layers.emplace_back(std::make_unique(layerName, std::move(children))); } - return std::make_unique(std::move(layers)); + Task::UPtr task = std::make_unique(std::move(layers)); + + const config::Import::Dxf &dxf = m_config.root().import().dxf(); + if (dxf.sortPathByLength()) { + task->sortPathsByLength(); + } + + return task; } Application::Application() @@ -151,6 +168,7 @@ bool Application::selectTool(const QString &toolName) if (tool) { if (m_openedDocument) { m_openedDocument->setToolConfig(*tool); + emit toolChanged(); } m_defaultToolConfig = tool; @@ -252,15 +270,13 @@ bool Application::loadFromDxf(const QString &fileName) try { importer::dxf::Importer importer(fileName.toStdString(), dxf.splineToArcPrecision(), dxf.minimumSplineLength(), dxf.minimumArcLength()); - m_openedDocument = std::make_unique(createTaskFromDxfImporter(importer), *m_defaultToolConfig, *m_defaultProfileConfig); + setOpenedDocument(std::make_unique(createTaskFromDxfImporter(importer), *m_defaultToolConfig, *m_defaultProfileConfig)); } catch (const common::FileCouldNotOpenException&) { qCritical() << "File not found:" << fileName; return false; } - emit documentChanged(m_openedDocument.get()); - return true; } @@ -269,14 +285,12 @@ bool Application::loadFromDxfplot(const QString &fileName) try { importer::dxfplot::Importer importer(m_config.root().tools(), m_config.root().profiles()); - m_openedDocument = importer(fileName.toStdString()); + setOpenedDocument(importer(fileName.toStdString())); } catch (const common::FileCouldNotOpenException&) { return false; } - emit documentChanged(m_openedDocument.get()); - return true; } @@ -317,17 +331,23 @@ bool Application::saveToDxfplot(const QString &fileName) void Application::leftCutterCompensation() { cutterCompensation(1.0f); + + takeDocumentSnapshot(); } void Application::rightCutterCompensation() { cutterCompensation(-1.0f); + + takeDocumentSnapshot(); } void Application::resetCutterCompensation() { Task &task = m_openedDocument->task(); task.resetCutterCompensationSelection(); + + takeDocumentSnapshot(); } void Application::pocketSelection() @@ -337,6 +357,8 @@ void Application::pocketSelection() Task &task = m_openedDocument->task(); task.pocketSelection(radius, dxf.minimumPolylineLength(), dxf.minimumArcLength()); + + takeDocumentSnapshot(); } geometry::Rect Application::selectionBoundingRect() const @@ -349,18 +371,32 @@ void Application::transformSelection(const QTransform& matrix) { Task &task = m_openedDocument->task(); task.transformSelection(matrix); + + takeDocumentSnapshot(); } void Application::hideSelection() { Task &task = m_openedDocument->task(); task.hideSelection(); + + takeDocumentSnapshot(); } void Application::showHidden() { Task &task = m_openedDocument->task(); task.showHidden(); + + takeDocumentSnapshot(); +} + +void Application::optimizeOrder() +{ + const config::Optimize &optimize = m_config.root().optimize(); + + Task &task = m_openedDocument->task(); + task.optimizeOrder(optimize.maintainPathLengthOrder(), optimize.lengthPrecision(), optimize.distancePrecision()); } Simulation Application::createSimulation() @@ -369,4 +405,19 @@ Simulation Application::createSimulation() return Simulation(*m_openedDocument, fastMoveFeedRate); } +void Application::takeDocumentSnapshot() +{ + m_documentHistory->takeSnapshot(*m_openedDocument); +} + +void Application::undoDocumentChanges() +{ + setRestoredDocument(m_documentHistory->undo()); +} + +void Application::redoDocumentChanges() +{ + setRestoredDocument(m_documentHistory->redo()); +} + } diff --git a/src/model/application.h b/src/model/application.h index ef416e5..d883616 100644 --- a/src/model/application.h +++ b/src/model/application.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -35,6 +36,10 @@ class Application : public QObject QString m_lastSavedDxfplotFileName; Document::UPtr m_openedDocument; + DocumentHistory::UPtr m_documentHistory; + + void setOpenedDocument(Document::UPtr &&document); + void setRestoredDocument(const Document &documentVersion); static QString baseName(const QString& fileName); void resetLastSavedFileNames(); @@ -105,12 +110,20 @@ class Application : public QObject void hideSelection(); void showHidden(); + void optimizeOrder(); + Simulation createSimulation(); + void takeDocumentSnapshot(); + void undoDocumentChanges(); + void redoDocumentChanges(); + Q_SIGNALS: - void documentChanged(Document *newDocument); + void newDocumentOpened(Document *newDocument); + void documentRestoredFromHistory(Document *newDocument); void titleChanged(QString title); void configChanged(config::Config &config); + void toolChanged(); void errorRaised(const QString& message) const; void fileSaved(const QString &fileName); }; diff --git a/src/model/document.cpp b/src/model/document.cpp index bf457ab..b665bfa 100644 --- a/src/model/document.cpp +++ b/src/model/document.cpp @@ -8,7 +8,13 @@ Document::Document(Task::UPtr&& task, const config::Tools::Tool &toolConfig, con m_toolConfig(&toolConfig), m_profileConfig(&profileConfig) { +} +Document::Document(const Document &other) + :m_task(std::make_unique(other.task())), + m_toolConfig(&other.toolConfig()), + m_profileConfig(&other.profileConfig()) +{ } Task &Document::task() @@ -34,13 +40,11 @@ const config::Profiles::Profile &Document::profileConfig() const void Document::setToolConfig(const config::Tools::Tool &tool) { m_toolConfig = &tool; - emit toolConfigChanged(tool); } void Document::setProfileConfig(const config::Profiles::Profile &profile) { m_profileConfig = &profile; - emit profileConfigChanged(profile); } } diff --git a/src/model/document.h b/src/model/document.h index fe40fc1..edc1839 100644 --- a/src/model/document.h +++ b/src/model/document.h @@ -6,10 +6,8 @@ namespace model { -class Document : public QObject, public common::Aggregable +class Document : public common::Aggregable { - Q_OBJECT; - private: Task::UPtr m_task; const config::Tools::Tool *m_toolConfig; @@ -18,6 +16,9 @@ class Document : public QObject, public common::Aggregable public: explicit Document(Task::UPtr&& task, const config::Tools::Tool &toolConfig, const config::Profiles::Profile &profileConfig); Document() = default; + explicit Document(const Document &other); + + Document &operator=(Document &&) = default; Task& task(); const Task& task() const; @@ -26,10 +27,6 @@ class Document : public QObject, public common::Aggregable const config::Profiles::Profile &profileConfig() const; void setToolConfig(const config::Tools::Tool &tool); void setProfileConfig(const config::Profiles::Profile &profile); - -Q_SIGNALS: - void toolConfigChanged(const config::Tools::Tool &tool); - void profileConfigChanged(const config::Profiles::Profile &profile); }; } diff --git a/src/model/documenthistory.cpp b/src/model/documenthistory.cpp new file mode 100644 index 0000000..9954724 --- /dev/null +++ b/src/model/documenthistory.cpp @@ -0,0 +1,55 @@ +#include + +#include + +namespace model +{ + +bool DocumentHistory::isCurrentDocumentLastOfHistory() const +{ + return m_currentDocumentIt == (m_documentHistory.end() - 1); +} + +bool DocumentHistory::isCurrentDocumentFirstOfHistory() const +{ + return m_currentDocumentIt == m_documentHistory.begin(); +} + +DocumentHistory::DocumentHistory(const Document& initialDocument) + :m_currentDocumentIt(m_documentHistory.insert(m_documentHistory.end(), initialDocument)) +{ +} + +void DocumentHistory::takeSnapshot(const Document& currentDocument) +{ + if (!isCurrentDocumentLastOfHistory()) { + m_documentHistory.erase(m_currentDocumentIt + 1, m_documentHistory.end()); + } + + constexpr int MaximumSnapshots = 100; + if (m_documentHistory.size() == MaximumSnapshots) { + m_documentHistory.erase(m_documentHistory.begin()); + } + + m_currentDocumentIt = m_documentHistory.insert(m_documentHistory.end(), currentDocument); +} + +const Document &DocumentHistory::undo() +{ + if (!isCurrentDocumentFirstOfHistory()) { + --m_currentDocumentIt; + } + + return *m_currentDocumentIt; +} + +const Document &DocumentHistory::redo() +{ + if (!isCurrentDocumentLastOfHistory()) { + ++m_currentDocumentIt; + } + + return *m_currentDocumentIt; +} + +} diff --git a/src/model/documenthistory.h b/src/model/documenthistory.h new file mode 100644 index 0000000..7931029 --- /dev/null +++ b/src/model/documenthistory.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace model +{ + +class Document; + +class DocumentHistory : public common::Aggregable +{ +private: + Document::List m_documentHistory; + Document::List::iterator m_currentDocumentIt; + + bool isCurrentDocumentLastOfHistory() const; + bool isCurrentDocumentFirstOfHistory() const; + +public: + explicit DocumentHistory(const Document& initialDocument); + + void takeSnapshot(const Document& currentDocument); + const Document &undo(); + const Document &redo(); +}; + +} diff --git a/src/model/documentmodelobserver.h b/src/model/documentmodelobserver.h index 4287b82..74e48a7 100644 --- a/src/model/documentmodelobserver.h +++ b/src/model/documentmodelobserver.h @@ -26,6 +26,16 @@ class DocumentModelObserver : public QtBaseObject */ virtual void documentChanged() = 0; + /// Notify the document was changed after an undo or redo operation + virtual void documentRestoredFromHistory() + { + } + + /// Notify the document was changed after an opening operation + virtual void newDocumentOpened() + { + } + protected: Document *document() const { @@ -46,11 +56,24 @@ private Q_SLOTS: documentChanged(); } + void internalDocumentRestoredFromHistory(Document *newDocument) + { + internalDocumentChanged(newDocument); + documentRestoredFromHistory(); + } + + void internalNewDocumentOpened(Document *newDocument) + { + internalDocumentChanged(newDocument); + newDocumentOpened(); + } + public: explicit DocumentModelObserver(Application &app) :m_document(nullptr) { - QObject::connect(&app, &Application::documentChanged, this, &DocumentModelObserver::internalDocumentChanged); + QObject::connect(&app, &Application::documentRestoredFromHistory, this, &DocumentModelObserver::internalDocumentRestoredFromHistory); + QObject::connect(&app, &Application::newDocumentOpened, this, &DocumentModelObserver::internalNewDocumentOpened); } }; diff --git a/src/model/layer.cpp b/src/model/layer.cpp index 88082a0..0a8531c 100644 --- a/src/model/layer.cpp +++ b/src/model/layer.cpp @@ -1,5 +1,7 @@ #include +#include + namespace model { @@ -17,6 +19,15 @@ Layer::Layer(const std::string &name, Path::ListUPtr &&children) assignSelfToChildren(); } +Layer::Layer(const Layer& other) + :Renderable(other), + m_children(common::deepcopy(other.m_children)) +{ + for (Path::UPtr &child : m_children) { + child->setLayer(*this); + } +} + int Layer::childrenCount() const { return m_children.size(); diff --git a/src/model/layer.h b/src/model/layer.h index 1e7c5e5..1cd2ef7 100644 --- a/src/model/layer.h +++ b/src/model/layer.h @@ -21,6 +21,7 @@ class Layer : public Renderable, public common::Aggregable public: explicit Layer(const std::string &name, Path::ListUPtr &&children); explicit Layer() = default; + explicit Layer(const Layer& other);; int childrenCount() const; Path& childrenAt(int index); diff --git a/src/model/offsettedpath.cpp b/src/model/offsettedpath.cpp index 25294e7..d2465f3 100644 --- a/src/model/offsettedpath.cpp +++ b/src/model/offsettedpath.cpp @@ -9,6 +9,13 @@ OffsettedPath::OffsettedPath(geometry::Polyline::List &&offsettedPolylines, Dire { } +OffsettedPath::OffsettedPath(const OffsettedPath& other) + :QObject(), + m_polylines(other.m_polylines), + m_direction(other.m_direction) +{ +} + const geometry::Polyline::List &OffsettedPath::polylines() const { return m_polylines; diff --git a/src/model/offsettedpath.h b/src/model/offsettedpath.h index 8c623e6..55e015d 100644 --- a/src/model/offsettedpath.h +++ b/src/model/offsettedpath.h @@ -33,6 +33,7 @@ class OffsettedPath : public QObject public: explicit OffsettedPath(geometry::Polyline::List &&offsettedPolylines, Direction direction); + explicit OffsettedPath(const OffsettedPath& other); explicit OffsettedPath() = default; const geometry::Polyline::List &polylines() const; diff --git a/src/model/path.cpp b/src/model/path.cpp index 89b5d53..2201997 100644 --- a/src/model/path.cpp +++ b/src/model/path.cpp @@ -27,6 +27,24 @@ Path::Path(geometry::Polyline &&basePolyline, const std::string &name, const Pat connect(this, &Path::visibilityChanged, this, &Path::updateGlobalVisibility); } +Path::Path(const Path& other) + :Renderable(other), + m_basePolyline(other.m_basePolyline), + m_settings(other.m_settings), + m_globallyVisible(other.m_globallyVisible) +{ + connect(this, &Path::visibilityChanged, this, &Path::updateGlobalVisibility); + + if (other.m_offsettedPath) { + m_offsettedPath = std::make_unique(*other.m_offsettedPath); + } +} + +Path::Path() +{ + connect(this, &Path::visibilityChanged, this, &Path::updateGlobalVisibility); +} + Path::ListUPtr Path::FromPolylines(geometry::Polyline::List &&polylines, const PathSettings &settings, const std::string &layerName) { const int size = polylines.size(); diff --git a/src/model/path.h b/src/model/path.h index cbb2d85..9729b8f 100644 --- a/src/model/path.h +++ b/src/model/path.h @@ -34,7 +34,8 @@ class Path : public Renderable, public common::Aggregable public: explicit Path(geometry::Polyline &&basePolyline, const std::string &name, const PathSettings& settings); - explicit Path() = default; + explicit Path(const Path& other); + explicit Path(); static ListUPtr FromPolylines(geometry::Polyline::List &&polylines, const PathSettings &settings, const std::string &layerName); diff --git a/src/model/renderable.cpp b/src/model/renderable.cpp index 9c7935f..06d6d6e 100644 --- a/src/model/renderable.cpp +++ b/src/model/renderable.cpp @@ -10,6 +10,14 @@ Renderable::Renderable(const std::string &name) { } +Renderable::Renderable(const Renderable &other) + :QObject(), + m_name(other.name()), + m_selected(false), + m_visible(other.visible()) +{ +} + const std::string &Renderable::name() const { return m_name; diff --git a/src/model/renderable.h b/src/model/renderable.h index 3fb5f08..0cfb557 100644 --- a/src/model/renderable.h +++ b/src/model/renderable.h @@ -30,6 +30,7 @@ class Renderable : public QObject public: explicit Renderable(const std::string &name); explicit Renderable() = default; + explicit Renderable(const Renderable &other); const std::string &name() const; diff --git a/src/model/task.cpp b/src/model/task.cpp index b2ea3d1..94f5997 100644 --- a/src/model/task.cpp +++ b/src/model/task.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include namespace model { @@ -35,6 +37,24 @@ Task::Task(Layer::ListUPtr &&layers) m_stack = m_paths; } +Task::Task(const Task &other) + :QObject(), + m_layers(common::deepcopy(other.m_layers)), + m_stack(other.m_stack.size()) +{ + initPathsFromLayers(); + + // Remap pointers of path on stack + std::unordered_map pathRemapping; + for (Path::ListPtr::const_iterator ito = other.m_paths.begin(), it = m_paths.begin(), end = m_paths.end(); it != end; ++it, ++ito) { + pathRemapping.insert({*ito, *it}); + } + + std::transform(other.m_stack.begin(), other.m_stack.end(), m_stack.begin(), [&pathRemapping](Path *path){ + return pathRemapping.find(path)->second; + }); +} + int Task::pathCount() const { return m_paths.size(); @@ -72,6 +92,58 @@ void Task::movePath(int index, MoveDirection direction) } } +void Task::movePathToTip(int index, MoveTip tip) +{ + assert(0 <= index && index < pathCount()); + + Path *path = m_stack[index]; + m_stack.erase(m_stack.begin() + index); + + switch (tip) { + case MoveTip::Bottom: + { + m_stack.push_back(path); + break; + } + case MoveTip::Top: + { + m_stack.insert(m_stack.begin(), path); + break; + } + } +} + +void Task::sortPathsByLength() +{ + struct PathLength + { + Path *path; + float length; + + PathLength() = default; + + explicit PathLength(Path *path) + :path(path), + length(path->basePolyline().length()) + { + } + + bool operator<(const PathLength& other) const + { + return length < other.length; + } + }; + + std::vector pathsLength(m_paths.size()); + std::transform(m_paths.begin(), m_paths.end(), pathsLength.begin(), + [](Path *path){ return PathLength(path); }); + + std::sort(pathsLength.begin(), pathsLength.end()); + + std::transform(pathsLength.begin(), pathsLength.end(), m_stack.begin(), + [](PathLength& pathLength){ return pathLength.path; }); +} + void Task::resetCutterCompensationSelection() { forEachSelectedPath([](model::Path &path){ path.resetOffset(); }); @@ -126,6 +198,83 @@ void Task::showHidden() }); } +geometry::OrderOptimizer::NodesPerGroup generateNodesSingleGroup(const Path::ListPtr &paths) +{ + geometry::OrderOptimizer::Node::List group(paths.size()); + + for (int i = 0; i < paths.size(); ++i) { + group[i] = {{}, i, paths[i]->basePolyline().start()}; + } + + return {group}; +} + +geometry::OrderOptimizer::NodesPerGroup generateNodesPerGroupOfLength(const Path::ListPtr &paths, float lengthPrecision) +{ + struct PathRoundedLength : common::Aggregable + { + int id; + Path *path; + int roundedLength; + + PathRoundedLength() = default; + + explicit PathRoundedLength(Path *path, int id, float lengthPrecision) + :id(id), + path(path), + roundedLength(path->basePolyline().length() / lengthPrecision) + { + } + + bool operator<(const PathRoundedLength& other) const + { + return roundedLength < other.roundedLength; + } + }; + + PathRoundedLength::List sortedPathsRoundedLength(paths.size()); + for (int pathId = 0, nbPaths = paths.size(); pathId < nbPaths; ++pathId) { + Path *path = paths[pathId]; + const float length = path->basePolyline().length(); + sortedPathsRoundedLength[pathId] = PathRoundedLength(path, pathId, lengthPrecision); + } + + std::sort(sortedPathsRoundedLength.begin(), sortedPathsRoundedLength.end()); + + geometry::OrderOptimizer::NodesPerGroup nodesPerGroup; + float currentGroupLength = -1.0f; + for (const PathRoundedLength& pathRoundedLength : sortedPathsRoundedLength) { + if (pathRoundedLength.roundedLength != currentGroupLength) { + // Create new group + nodesPerGroup.push_back({}); + currentGroupLength = pathRoundedLength.roundedLength; + } + + nodesPerGroup.back().push_back({{}, pathRoundedLength.id, pathRoundedLength.path->basePolyline().start()}); + } + + return nodesPerGroup; +} + +void Task::optimizeOrder(bool maintainPathLengthOrder, float lengthPrecision, float distancePrecision) +{ + const geometry::OrderOptimizer::NodesPerGroup nodesPerGroup = maintainPathLengthOrder ? + generateNodesPerGroupOfLength(m_paths, lengthPrecision) : + generateNodesSingleGroup(m_paths); + + const int nbPath = pathCount(); + geometry::OrderOptimizer optimizer(nodesPerGroup, nbPath); + const std::vector order = optimizer.order(); + + Path::ListPtr newPaths(m_paths.size()); + for (int i = 0; i < nbPath; ++i) { + newPaths[i] = m_paths[order[i]]; + } + + std::swap(m_stack, newPaths); + emit pathOrderChanged(); +} + geometry::Rect Task::selectionBoundingRect() const { bool isFirstPath = true; diff --git a/src/model/task.h b/src/model/task.h index 770ef3d..da654cb 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -30,8 +30,15 @@ class Task : public QObject, public common::Aggregable DOWN = 1 }; + enum class MoveTip + { + Top, + Bottom + }; + explicit Task() = default; explicit Task(Layer::ListUPtr &&layers); + explicit Task(const Task &other); int pathCount() const; const Path &pathAt(int index) const; @@ -39,6 +46,9 @@ class Task : public QObject, public common::Aggregable int pathIndexFor(const Path &path) const; void movePath(int index, MoveDirection direction); + void movePathToTip(int index, MoveTip tip); + + void sortPathsByLength(); template void forEachPathInStack(Functor &&functor) const @@ -81,6 +91,7 @@ class Task : public QObject, public common::Aggregable void transformSelection(const QTransform& matrix); void hideSelection(); void showHidden(); + void optimizeOrder(bool maintainPathLengthOrder, float lengthPrecision, float distancePrecision); geometry::Rect selectionBoundingRect() const; geometry::Rect visibleBoundingRect() const; @@ -94,6 +105,7 @@ class Task : public QObject, public common::Aggregable Q_SIGNALS: void pathSelectedChanged(Path &path, bool selected); void selectionChanged(bool empty); + void pathOrderChanged(); }; template diff --git a/src/view/mainwindow.cpp b/src/view/mainwindow.cpp index 80846d0..0cc52e7 100644 --- a/src/view/mainwindow.cpp +++ b/src/view/mainwindow.cpp @@ -12,9 +12,11 @@ #include #include +#include #include #include #include +#include #include namespace view @@ -99,6 +101,9 @@ void MainWindow::setupMenuActions() connect(actionMirrorSelection, &QAction::triggered, this, &MainWindow::mirrorSelection); connect(actionSetSelectionOrigin, &QAction::triggered, this, &MainWindow::setSelectionOrigin); connect(actionSimulate, &QAction::triggered, this, &MainWindow::simulate); + connect(actionUndo, &QAction::triggered, &m_app, &model::Application::undoDocumentChanges); + connect(actionRedo, &QAction::triggered, &m_app, &model::Application::redoDocumentChanges); + connect(actionOptimizeOrder, &QAction::triggered, this, &MainWindow::optimizeOrder); } void MainWindow::setupOpenedDocumentActions() @@ -117,6 +122,9 @@ void MainWindow::setupOpenedDocumentActions() m_openedDocumentActions.addAction(actionMirrorSelection); m_openedDocumentActions.addAction(actionSetSelectionOrigin); m_openedDocumentActions.addAction(actionSimulate); + m_openedDocumentActions.addAction(actionUndo); + m_openedDocumentActions.addAction(actionRedo); + m_openedDocumentActions.addAction(actionOptimizeOrder); m_openedDocumentActions.setExclusive(true); } @@ -144,7 +152,7 @@ MainWindow::MainWindow(model::Application &app) setDocumentToolsEnabled(false); connect(&m_app, &model::Application::titleChanged, this, &MainWindow::setWindowTitle); - connect(&m_app, &model::Application::documentChanged, this, &MainWindow::documentChanged); + connect(&m_app, &model::Application::newDocumentOpened, this, &MainWindow::newDocumentOpened); connect(&m_app, &model::Application::errorRaised, this, &MainWindow::displayError); } @@ -208,6 +216,7 @@ void MainWindow::openSettings() }(); if (accepted) { + newConfig.save(); m_app.setConfig(std::move(newConfig)); } } @@ -216,6 +225,7 @@ void MainWindow::transformSelection() { dialogs::Transform transform; if (transform.exec() == QDialog::Accepted) { + m_app.takeDocumentSnapshot(); m_app.transformSelection(transform.matrix()); } } @@ -224,6 +234,7 @@ void MainWindow::mirrorSelection() { dialogs::Mirror mirror; if (mirror.exec() == QDialog::Accepted) { + m_app.takeDocumentSnapshot(); m_app.transformSelection(mirror.matrix()); } } @@ -234,11 +245,12 @@ void MainWindow::setSelectionOrigin() dialogs::SetOrigin setOrigin(selectionBoundingRect); if (setOrigin.exec() == QDialog::Accepted) { + m_app.takeDocumentSnapshot(); m_app.transformSelection(setOrigin.matrix()); // TODO common function } } -void MainWindow::documentChanged(model::Document *newDocument) +void MainWindow::newDocumentOpened(model::Document *newDocument) { setDocumentToolsEnabled((newDocument != nullptr)); @@ -258,4 +270,9 @@ void MainWindow::simulate() m_simulation->show(); } +void MainWindow::optimizeOrder() +{ + m_app.optimizeOrder(); +} + } diff --git a/src/view/mainwindow.h b/src/view/mainwindow.h index f3a85b3..51259cc 100644 --- a/src/view/mainwindow.h +++ b/src/view/mainwindow.h @@ -53,11 +53,10 @@ protected Q_SLOTS: void transformSelection(); void mirrorSelection(); void setSelectionOrigin(); - void documentChanged(model::Document *newDocument); + void newDocumentOpened(model::Document *newDocument); void displayError(const QString &message); - -signals: void simulate(); + void optimizeOrder(); }; } diff --git a/src/view/profile.cpp b/src/view/profile.cpp index 18750be..2546baa 100644 --- a/src/view/profile.cpp +++ b/src/view/profile.cpp @@ -14,9 +14,7 @@ void Profile::updateAllComboBoxesItems() Profile::Profile(model::Application& app) :DocumentModelObserver(app), - m_app(app), - m_outsideToolChangeBlocked(false), - m_outsideProfileChangeBlocked(false) + m_app(app) { setupUi(this); @@ -29,9 +27,6 @@ Profile::Profile(model::Application& app) void Profile::documentChanged() { - connect(document(), &model::Document::toolConfigChanged, this, &Profile::toolConfigChanged); - connect(document(), &model::Document::profileConfigChanged, this, &Profile::profileConfigChanged); - toolComboBox->setCurrentText(QString::fromStdString(document()->toolConfig().name())); profileComboBox->setCurrentText(QString::fromStdString(document()->profileConfig().name())); // TODO updateTextFromProfileConfig } @@ -41,36 +36,14 @@ void Profile::configChanged([[maybe_unused]] const config::Config &config) updateAllComboBoxesItems(); } -void Profile::toolConfigChanged(const config::Tools::Tool& tool) -{ - if (!m_outsideToolChangeBlocked) { - toolComboBox->setCurrentText(QString::fromStdString(tool.name())); - } -} - void Profile::currentToolTextChanged(const QString& toolName) { - m_outsideToolChangeBlocked = true; - m_app.selectTool(toolName); - - m_outsideToolChangeBlocked = false; -} - -void Profile::profileConfigChanged(const config::Profiles::Profile& profile) -{ - if (!m_outsideProfileChangeBlocked) { - profileComboBox->setCurrentText(QString::fromStdString(profile.name())); - } } void Profile::currentProfileTextChanged(const QString& profileName) { - m_outsideProfileChangeBlocked = true; - m_app.selectProfile(profileName); - - m_outsideProfileChangeBlocked = false; } } diff --git a/src/view/profile.h b/src/view/profile.h index 8df40e3..eb8d35a 100644 --- a/src/view/profile.h +++ b/src/view/profile.h @@ -13,9 +13,6 @@ class Profile : public model::DocumentModelObserver, public Ui::Profile private: model::Application &m_app; - bool m_outsideToolChangeBlocked; - bool m_outsideProfileChangeBlocked; - template void updateComboBoxItems(const ConfigList &list, QComboBox *comboBox) { @@ -43,9 +40,7 @@ class Profile : public model::DocumentModelObserver, public Ui::Profile public Q_SLOTS: void configChanged(const config::Config &config); - void toolConfigChanged(const config::Tools::Tool &tool); void currentToolTextChanged(const QString &toolName); - void profileConfigChanged(const config::Profiles::Profile &profile); void currentProfileTextChanged(const QString &profileName); }; diff --git a/src/view/simulation/internal/CMakeLists.txt b/src/view/simulation/internal/CMakeLists.txt index 7637aa4..e58229a 100644 --- a/src/view/simulation/internal/CMakeLists.txt +++ b/src/view/simulation/internal/CMakeLists.txt @@ -11,3 +11,4 @@ set(SRC ) add_library(view-simulation-internal ${SRC}) +target_link_libraries(view-simulation-internal Qt6::3DCore Qt6::Widgets Qt6::3DRender) \ No newline at end of file diff --git a/src/view/simulation/internal/tool.h b/src/view/simulation/internal/tool.h index 2e0b8cb..a5060cc 100644 --- a/src/view/simulation/internal/tool.h +++ b/src/view/simulation/internal/tool.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/src/view/simulation/internal/toolpath.cpp b/src/view/simulation/internal/toolpath.cpp index 3e9a4e7..4c99938 100644 --- a/src/view/simulation/internal/toolpath.cpp +++ b/src/view/simulation/internal/toolpath.cpp @@ -1,9 +1,9 @@ #include -#include -#include -#include -#include +#include +#include +#include +#include #include @@ -51,28 +51,28 @@ void ToolPath::createPolylineFromPoints(const model::Simulation::ToolPathPoint3D m_indices[i] = i; } - Qt3DRender::QGeometry *geometry = new Qt3DRender::QGeometry(this); + Qt3DCore::QGeometry *geometry = new Qt3DCore::QGeometry(this); const QByteArray vertexData = QByteArray::fromRawData((const char *)m_packedPoints.get(), sizeof(PackedVector3D) * nbPoints); - Qt3DRender::QBuffer *vertexBuffer = new Qt3DRender::QBuffer(geometry); + Qt3DCore::QBuffer *vertexBuffer = new Qt3DCore::QBuffer(geometry); vertexBuffer->setData(vertexData); const QByteArray colorData = QByteArray::fromRawData((const char *)m_colors.get(), sizeof(uint32_t) * nbPoints); - Qt3DRender::QBuffer *colorBuffer = new Qt3DRender::QBuffer(geometry); + Qt3DCore::QBuffer *colorBuffer = new Qt3DCore::QBuffer(geometry); colorBuffer->setData(colorData); const QByteArray indexData = QByteArray::fromRawData((const char *)m_indices.get(), sizeof(uint32_t) * nbPoints); - Qt3DRender::QBuffer *indexBuffer = new Qt3DRender::QBuffer(geometry); + Qt3DCore::QBuffer *indexBuffer = new Qt3DCore::QBuffer(geometry); indexBuffer->setData(indexData); - Qt3DRender::QAttribute *vertexAttribute = new Qt3DRender::QAttribute(vertexBuffer, Qt3DRender::QAttribute::defaultPositionAttributeName(), Qt3DRender::QAttribute::Float, 3, nbPoints); - vertexAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute); + Qt3DCore::QAttribute *vertexAttribute = new Qt3DCore::QAttribute(vertexBuffer, Qt3DCore::QAttribute::defaultPositionAttributeName(), Qt3DCore::QAttribute::Float, 3, nbPoints); + vertexAttribute->setAttributeType(Qt3DCore::QAttribute::VertexAttribute); - Qt3DRender::QAttribute *colorAttribute = new Qt3DRender::QAttribute(colorBuffer, Qt3DRender::QAttribute::defaultColorAttributeName(), Qt3DRender::QAttribute::UnsignedByte, 4, nbPoints); - colorAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute); + Qt3DCore::QAttribute *colorAttribute = new Qt3DCore::QAttribute(colorBuffer, Qt3DCore::QAttribute::defaultColorAttributeName(), Qt3DCore::QAttribute::UnsignedByte, 4, nbPoints); + colorAttribute->setAttributeType(Qt3DCore::QAttribute::VertexAttribute); - Qt3DRender::QAttribute *indexAttribute = new Qt3DRender::QAttribute(indexBuffer, Qt3DRender::QAttribute::UnsignedInt, 3, nbPoints); - indexAttribute->setAttributeType(Qt3DRender::QAttribute::IndexAttribute); + Qt3DCore::QAttribute *indexAttribute = new Qt3DCore::QAttribute(indexBuffer, Qt3DCore::QAttribute::UnsignedInt, 3, nbPoints); + indexAttribute->setAttributeType(Qt3DCore::QAttribute::IndexAttribute); geometry->addAttribute(vertexAttribute); geometry->addAttribute(colorAttribute); diff --git a/src/view/task/layertreemodel.cpp b/src/view/task/layertreemodel.cpp index 61eef27..4badfe4 100644 --- a/src/view/task/layertreemodel.cpp +++ b/src/view/task/layertreemodel.cpp @@ -7,7 +7,8 @@ namespace view::task LayerTreeModel::LayerTreeModel(model::Task &task, QObject *parent) :QAbstractItemModel(parent), - m_task(task) + m_task(task), + m_ignoreSelectionChanged(false) { } @@ -117,21 +118,40 @@ void LayerTreeModel::itemClicked(const QModelIndex& index) model::Renderable *item = static_cast(index.internalPointer()); item->toggleVisible(); + emit documentVisibilityChanged(); + emit dataChanged(index, index); } } } +void LayerTreeModel::clearSelection(QItemSelectionModel *selectionModel) +{ + m_ignoreSelectionChanged = true; + + selectionModel->clear(); + + m_ignoreSelectionChanged = false; +} + void LayerTreeModel::updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel) { + m_ignoreSelectionChanged = true; + const std::pair indices = m_task.layerAndPathIndexFor(path); const QModelIndex parentIndex = index(indices.first, 0); const QModelIndex childIndex = index(indices.second, 0, parentIndex); selectionModel->select(childIndex, flag); + + m_ignoreSelectionChanged = false; } void LayerTreeModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { + if (m_ignoreSelectionChanged) { + return; + } + for (const QModelIndex &index : selected.indexes()) { model::Renderable &renderable = *static_cast(index.internalPointer()); renderable.setSelected(true); diff --git a/src/view/task/layertreemodel.h b/src/view/task/layertreemodel.h index d8863c7..d215c81 100644 --- a/src/view/task/layertreemodel.h +++ b/src/view/task/layertreemodel.h @@ -16,6 +16,7 @@ class LayerTreeModel: public QAbstractItemModel private: model::Task &m_task; + bool m_ignoreSelectionChanged; public: explicit LayerTreeModel(model::Task &task, QObject *parent); @@ -28,8 +29,12 @@ class LayerTreeModel: public QAbstractItemModel Qt::ItemFlags flags(const QModelIndex &index) const override; void itemClicked(const QModelIndex &index); + void clearSelection(QItemSelectionModel *selectionModel); void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel); void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +signals: + void documentVisibilityChanged(); }; } diff --git a/src/view/task/path.cpp b/src/view/task/path.cpp index 440a030..3572164 100644 --- a/src/view/task/path.cpp +++ b/src/view/task/path.cpp @@ -1,3 +1,4 @@ +#include "model/application.h" #include namespace view::task @@ -7,7 +8,7 @@ void Path::setupModel() { m_groupSettings.reset(new model::PathGroupSettings(task())); - hide(); + stackedWidget->setCurrentWidget(pageNoSelection); connect(&task(), &model::Task::selectionChanged, this, &Path::selectionChanged); @@ -23,24 +24,44 @@ void Path::documentChanged() } Path::Path(model::Application &app) - :DocumentModelObserver(app) + :DocumentModelObserver(app), + m_app(app) { setupUi(this); + connect(&m_app, &model::Application::toolChanged, this, &Path::toolChanged); + connect(&m_app, &model::Application::configChanged, this, &Path::configChanged); } void Path::selectionChanged(bool empty) { if (empty) { - hide(); + stackedWidget->setCurrentWidget(pageNoSelection); } else { - show(); - updateFieldValue(planeFeedRate, m_groupSettings->planeFeedRate()); updateFieldValue(depthFeedRate, m_groupSettings->depthFeedRate()); updateFieldValue(intensity, m_groupSettings->intensity()); updateFieldValue(Ui::Path::depth, m_groupSettings->depth()); + + stackedWidget->setCurrentWidget(pagePathSelected); } } +void Path::toolChanged() +{ + updateFieldVisibility(document()->toolConfig()); +} + +void Path::configChanged() +{ + updateFieldVisibility(document()->toolConfig()); +} + +void Path::updateFieldVisibility(const config::Tools::Tool& tool) +{ + const bool toolHasDepth = !tool.general().laser(); + Ui::Path::depth->setVisible(toolHasDepth); + depthLabel->setVisible(toolHasDepth); +} + } diff --git a/src/view/task/path.h b/src/view/task/path.h index ddc7d27..779ac18 100644 --- a/src/view/task/path.h +++ b/src/view/task/path.h @@ -14,14 +14,26 @@ namespace view::task class Path : public model::DocumentModelObserver, private Ui::Path { private: + model::Application &m_app; + std::unique_ptr m_groupSettings; void selectionChanged(bool empty); + void toolChanged(); + void configChanged(); + + void updateFieldVisibility(const config::Tools::Tool& tool); template void connectOnFieldChanged(Field *field, std::function &&func) { - connect(field, static_cast(&Field::valueChanged), this, func); + disconnect(field, static_cast(&Field::valueChanged), nullptr, nullptr); + + connect(field, static_cast(&Field::valueChanged), [this, func](ValueType value){ + func(value); + + m_app.takeDocumentSnapshot(); + }); } template diff --git a/src/view/task/pathlistmodel.cpp b/src/view/task/pathlistmodel.cpp index f9ca39e..b6aab28 100644 --- a/src/view/task/pathlistmodel.cpp +++ b/src/view/task/pathlistmodel.cpp @@ -8,8 +8,12 @@ namespace view::task PathListModel::PathListModel(model::Task &task, QObject *parent) :QAbstractListModel(parent), - m_task(task) + m_task(task), + m_ignoreSelectionChanged(false) { + connect(&m_task, &model::Task::pathOrderChanged, [this]{ + emit layoutChanged(); + }); } QVariant PathListModel::data(const QModelIndex &index, int role) const @@ -74,7 +78,7 @@ Qt::ItemFlags PathListModel::flags(const QModelIndex &index) const return (path.layer().visible()) ? Qt::ItemIsEnabled : Qt::NoItemFlags; } -QModelIndex PathListModel::movePath(const QModelIndex &index, model::Task::MoveDirection direction) +QModelIndex PathListModel::movePathToDirection(const QModelIndex &index, model::Task::MoveDirection direction) { const int row = index.row(); const int newRow = row + direction; @@ -96,6 +100,23 @@ QModelIndex PathListModel::movePath(const QModelIndex &index, model::Task::MoveD return index; } +QModelIndex PathListModel::movePathToTip(const QModelIndex &index, model::Task::MoveTip tip) +{ + const int row = index.row(); + const int newRow = (tip == model::Task::MoveTip::Top) ? 0 : (rowCount(index) - 1); + + if (index.isValid()) { + m_task.movePathToTip(row, tip); + + const QModelIndex newIndex = this->index(newRow); + emit dataChanged(index, newIndex); + + return newIndex; + } + + return index; +} + void PathListModel::itemClicked(const QModelIndex& index) { if ((index.flags() & Qt::ItemIsEnabled) == 0) { @@ -113,14 +134,31 @@ void PathListModel::itemClicked(const QModelIndex& index) } } +void PathListModel::clearSelection(QItemSelectionModel *selectionModel) +{ + m_ignoreSelectionChanged = true; + + selectionModel->clear(); + + m_ignoreSelectionChanged = false; +} + void PathListModel::updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel) { + m_ignoreSelectionChanged = true; + const int row = m_task.pathIndexFor(path); selectionModel->select(index(row, 0), flag); + + m_ignoreSelectionChanged = false; } void PathListModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { + if (m_ignoreSelectionChanged) { + return; + } + for (const QModelIndex &index : selected.indexes()) { model::Path &path = m_task.pathAt(index.row()); path.setSelected(true); diff --git a/src/view/task/pathlistmodel.h b/src/view/task/pathlistmodel.h index 8652e4a..88f155c 100644 --- a/src/view/task/pathlistmodel.h +++ b/src/view/task/pathlistmodel.h @@ -16,6 +16,7 @@ class PathListModel : public QAbstractListModel private: model::Task &m_task; + bool m_ignoreSelectionChanged; public: explicit PathListModel(model::Task &task, QObject *parent); @@ -25,11 +26,16 @@ class PathListModel : public QAbstractListModel int columnCount(const QModelIndex &parent = QModelIndex()) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; - QModelIndex movePath(const QModelIndex &index, model::Task::MoveDirection direction); + QModelIndex movePathToDirection(const QModelIndex &index, model::Task::MoveDirection direction); + QModelIndex movePathToTip(const QModelIndex &index, model::Task::MoveTip tip); void itemClicked(const QModelIndex &index); + void clearSelection(QItemSelectionModel *selectionModel); void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel); void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +signals: + void documentVisibilityChanged(); }; } diff --git a/src/view/task/task.cpp b/src/view/task/task.cpp index 6ad375b..3067af9 100644 --- a/src/view/task/task.cpp +++ b/src/view/task/task.cpp @@ -10,7 +10,8 @@ namespace view::task { Task::Task(model::Application &app) - :DocumentModelObserver(app) + :DocumentModelObserver(app), + m_app(app) { setupUi(this); } @@ -31,8 +32,10 @@ void Task::setupController() setupTreeViewController(m_pathListModel, pathsTreeView); setupTreeViewController(m_layerTreeModel, layersTreeView); - connect(moveUp, &QPushButton::pressed, [this](){ moveCurrentPath(model::Task::MoveDirection::UP); }); - connect(moveDown, &QPushButton::pressed, [this](){ moveCurrentPath(model::Task::MoveDirection::DOWN); }); + connect(moveUp, &QPushButton::pressed, [this](){ moveCurrentPathToDirection(model::Task::MoveDirection::UP); }); + connect(moveDown, &QPushButton::pressed, [this](){ moveCurrentPathToDirection(model::Task::MoveDirection::DOWN); }); + connect(moveTop, &QPushButton::pressed, [this](){ moveCurrentPathToTip(model::Task::MoveTip::Top); }); + connect(moveBottom, &QPushButton::pressed, [this](){ moveCurrentPathToTip(model::Task::MoveTip::Bottom); }); } void Task::updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag) @@ -53,12 +56,39 @@ void Task::pathSelectedChanged(model::Path &path, bool selected) selected ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); } -void Task::moveCurrentPath(model::Task::MoveDirection direction) +void Task::moveCurrentPathToDirection(model::Task::MoveDirection direction) { - QItemSelectionModel *selectionModel = pathsTreeView->selectionModel(); - const QModelIndex currentSelectedIndex = selectionModel->currentIndex(); - const QModelIndex newSelectedIndex = m_pathListModel->movePath(currentSelectedIndex, direction); - selectionModel->setCurrentIndex(newSelectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); + moveCurrentPath([this, direction](const QModelIndex& index){ + m_pathListModel->movePathToDirection(index, direction); + }); +} + +void Task::documentVisibilityChanged() +{ + m_app.takeDocumentSnapshot(); +} + +void Task::moveCurrentPathToTip(model::Task::MoveTip tip) +{ + moveCurrentPath([this, tip](const QModelIndex& index){ + m_pathListModel->movePathToTip(index, tip); + }); +} + +void Task::rebuildSelectionFromTask() +{ + QItemSelectionModel *pathsTreeSelectionModel = pathsTreeView->selectionModel(); + QItemSelectionModel *layersTreeSelectionModel = layersTreeView->selectionModel(); + + m_pathListModel->clearSelection(pathsTreeSelectionModel); + m_layerTreeModel->clearSelection(layersTreeSelectionModel); + + task().forEachSelectedPath([this, pathsTreeSelectionModel, layersTreeSelectionModel](const model::Path& path){ + constexpr QItemSelectionModel::SelectionFlag flag = QItemSelectionModel::Select; + + m_pathListModel->updateItemSelection(path, flag, pathsTreeSelectionModel); + m_layerTreeModel->updateItemSelection(path, flag, layersTreeSelectionModel); + }); } } diff --git a/src/view/task/task.h b/src/view/task/task.h index ba35ea2..a532270 100644 --- a/src/view/task/task.h +++ b/src/view/task/task.h @@ -17,6 +17,8 @@ class LayerTreeModel; class Task : public model::DocumentModelObserver, private Ui::Task { private: + model::Application &m_app; + std::unique_ptr m_pathListModel; std::unique_ptr m_layerTreeModel; @@ -42,6 +44,8 @@ class Task : public model::DocumentModelObserver, private Ui::Task connect(selectionModel, &QItemSelectionModel::selectionChanged, model.get(), &Model::selectionChanged); connect(treeView, &QTreeView::clicked, model.get(), &Model::itemClicked); + + connect(model.get(), &Model::documentVisibilityChanged, this, &Task::documentVisibilityChanged); } void setupModel(); @@ -49,6 +53,26 @@ class Task : public model::DocumentModelObserver, private Ui::Task void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag); + void moveCurrentPathToDirection(model::Task::MoveDirection direction); + void moveCurrentPathToTip(model::Task::MoveTip tip); + + template + void moveCurrentPath(Func &&movement) + { + QItemSelectionModel *selectionModel = pathsTreeView->selectionModel(); + + const QModelIndexList selectedItems = selectionModel->selectedIndexes(); + for (const QModelIndex& selectedIndex : selectedItems) { + movement(selectedIndex); + } + + rebuildSelectionFromTask(); + + m_app.takeDocumentSnapshot(); + } + + void rebuildSelectionFromTask(); + public: explicit Task(model::Application &app); @@ -56,9 +80,8 @@ class Task : public model::DocumentModelObserver, private Ui::Task void documentChanged(); protected Q_SLOTS: - void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void pathSelectedChanged(model::Path &path, bool selected); - void moveCurrentPath(model::Task::MoveDirection direction); + void documentVisibilityChanged(); }; } diff --git a/src/view/view2d/viewport.cpp b/src/view/view2d/viewport.cpp index 2c41469..cb63a11 100644 --- a/src/view/view2d/viewport.cpp +++ b/src/view/view2d/viewport.cpp @@ -250,7 +250,11 @@ class BackgroundPainter void Viewport::documentChanged() { setupModel(); - fitItemsInView(); +} + +void Viewport::newDocumentOpened() +{ + fitItemsInView(); // TODO delay after UI update } void Viewport::wheelEvent(QWheelEvent *event) diff --git a/src/view/view2d/viewport.h b/src/view/view2d/viewport.h index e67e807..51c6f18 100644 --- a/src/view/view2d/viewport.h +++ b/src/view/view2d/viewport.h @@ -42,6 +42,7 @@ class Viewport : public model::DocumentModelObserver protected: void documentChanged() override; + void newDocumentOpened() override; void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; diff --git a/template/config.xml b/template/config.xml index 6173e73..3c2e68c 100644 --- a/template/config.xml +++ b/template/config.xml @@ -9,6 +9,11 @@ + + + + + @@ -23,6 +28,7 @@ + @@ -35,6 +41,7 @@ + diff --git a/template/uic/CMakeLists.txt b/template/uic/CMakeLists.txt index 3b5343f..620b9f2 100644 --- a/template/uic/CMakeLists.txt +++ b/template/uic/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(dialogs) add_subdirectory(simulation) -qt5_wrap_ui(UIC_HEADERS +qt_wrap_ui(UIC_HEADERS info.ui mainwindow.ui path.ui diff --git a/template/uic/dialogs/CMakeLists.txt b/template/uic/dialogs/CMakeLists.txt index 8237e21..3e6171a 100644 --- a/template/uic/dialogs/CMakeLists.txt +++ b/template/uic/dialogs/CMakeLists.txt @@ -1,6 +1,6 @@ add_subdirectory(settings) -qt5_wrap_ui(UIC_HEADERS +qt_wrap_ui(UIC_HEADERS transform.ui mirror.ui setorigin.ui diff --git a/template/uic/dialogs/settings/CMakeLists.txt b/template/uic/dialogs/settings/CMakeLists.txt index 90acc95..cd3a43c 100644 --- a/template/uic/dialogs/settings/CMakeLists.txt +++ b/template/uic/dialogs/settings/CMakeLists.txt @@ -1,4 +1,4 @@ -qt5_wrap_ui(UIC_HEADERS +qt_wrap_ui(UIC_HEADERS group.ui list.ui settings.ui diff --git a/template/uic/mainwindow.ui b/template/uic/mainwindow.ui index 98ffe7b..3b98e2d 100644 --- a/template/uic/mainwindow.ui +++ b/template/uic/mainwindow.ui @@ -63,6 +63,9 @@ + + + @@ -89,6 +92,8 @@ + + @@ -310,6 +315,37 @@ Ctrl+R + + + Redo + + + Ctrl+Y + + + + + Undo + + + Ctrl+Z + + + + + + :/icons/optimize-order.svg:/icons/optimize-order.svg + + + Optimize Order + + + Optimize Order + + + Ctrl+Shift+O + + diff --git a/template/uic/path.ui b/template/uic/path.ui index 07d23c6..27222b0 100644 --- a/template/uic/path.ui +++ b/template/uic/path.ui @@ -7,7 +7,7 @@ 0 0 230 - 168 + 176 @@ -15,76 +15,117 @@ - - - QLayout::SetDefaultConstraint + + + 0 - - QFormLayout::AllNonFixedFieldsGrow - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - Plane Feed Rate - - - - - - - 9999.989999999999782 - - - - - - - Intensity - - - - - - - 9999.989999999999782 - - - - - - - Depth + + + + 0 - - - - - - 9999.989999999999782 + + 0 - - 0.100000000000000 + + 0 - - - - - - Depth Feed Rate + + 0 - - - - - - 9999.989999999999782 + + 0 - - - + + + + QLayout::SetDefaultConstraint + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + Plane Feed Rate + + + + + + + false + + + 9999.989999999999782 + + + + + + + Intensity + + + + + + + false + + + 9999.989999999999782 + + + + + + + Depth + + + + + + + false + + + 9999.989999999999782 + + + 0.100000000000000 + + + + + + + Depth Feed Rate + + + + + + + false + + + 9999.989999999999782 + + + + + + + + + + + diff --git a/template/uic/simulation/CMakeLists.txt b/template/uic/simulation/CMakeLists.txt index 51d2061..61ba327 100644 --- a/template/uic/simulation/CMakeLists.txt +++ b/template/uic/simulation/CMakeLists.txt @@ -1,4 +1,4 @@ -qt5_wrap_ui(UIC_HEADERS +qt_wrap_ui(UIC_HEADERS simulation.ui ) diff --git a/template/uic/simulation/simulation.ui b/template/uic/simulation/simulation.ui index 0007ab7..f0a7810 100644 --- a/template/uic/simulation/simulation.ui +++ b/template/uic/simulation/simulation.ui @@ -67,6 +67,12 @@ 0 + + 100 + + + 1000 + Qt::Horizontal diff --git a/template/uic/task.ui b/template/uic/task.ui index 55e9a31..b92ab9a 100644 --- a/template/uic/task.ui +++ b/template/uic/task.ui @@ -92,6 +92,28 @@ + + + + + + + + :/icons/layer-top.svg:/icons/layer-top.svg + + + + + + + + + + + :/icons/layer-bottom.svg:/icons/layer-bottom.svg + + + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 01c4373..4e550fe 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -9,7 +9,7 @@ FetchContent_MakeAvailable(googletest) include(GoogleTest) -find_package(Qt5 COMPONENTS REQUIRED +find_package(Qt6 COMPONENTS REQUIRED Test ) @@ -20,7 +20,9 @@ set(SRC dxfplotexporter.cpp dxfplotimporter.cpp exporterfixture.cpp + exporterrenderer.cpp gcodeexporter.cpp + orderoptimizer.cpp path.cpp pathsettings.cpp pocketer.cpp @@ -38,7 +40,7 @@ set(SRC add_executable(dxfplotter-test ${SRC} main.cpp) target_include_directories(dxfplotter-test PRIVATE ${Qt5Test_INCLUDE_DIRS}) -target_link_libraries(dxfplotter-test ${LINK_LIBRARIES} Qt5::Test gtest_main) +target_link_libraries(dxfplotter-test ${LINK_LIBRARIES} Qt6::Test gtest_main) add_coverage(dxfplotter-test) diff --git a/test/exporterrenderer.cpp b/test/exporterrenderer.cpp new file mode 100644 index 0000000..b17ad0f --- /dev/null +++ b/test/exporterrenderer.cpp @@ -0,0 +1,104 @@ +#include + +#include + +class DepthTrackerVisitor +{ +public: + std::vector depths; + + void start(const QVector2D& from, float safetyDepth) + { + } + + void end(const QVector2D& to, float safetyDepth) + { + } + + void startOperation(const QVector2D& to, float intensity) + { + } + + void endOperation(float safetyDepth) + { + } + + void processPathAtDepth(const geometry::Polyline& polyline, float depth, float planeFeedRate, float depthFeedRate) + { + depths.push_back(depth) ; + } +}; + +class ExporterRendererFixture : public ExporterFixture +{ +}; + +TEST_F(ExporterRendererFixture, shouldCutFirstPassAtZeroDepth) +{ + const geometry::Bulge bulge(QVector2D(0, 0), QVector2D(1, 1), 0); + geometry::Polyline polyline({bulge}); + + createTaskFromPolyline(std::move(polyline)); + + config::Profiles::Profile profile = m_profile; + profile.cut().passAtZeroDepth() = true; + + config::Tools::Tool tool = m_tool; + tool.general().depthPerCut() = 0.02f; + + DepthTrackerVisitor visitor; + exporter::renderer::Renderer renderer(tool, profile, visitor); + renderer.render(*m_document); + + const int nbCut = 0.1f / 0.02f + 2; + EXPECT_EQ(visitor.depths.size(), nbCut); + + EXPECT_FLOAT_EQ(visitor.depths.front(), 0.0f); + EXPECT_FLOAT_EQ(visitor.depths.back(), -0.1f); +} + +TEST_F(ExporterRendererFixture, shouldCutFirstPassAtToolDepth) +{ + const geometry::Bulge bulge(QVector2D(0, 0), QVector2D(1, 1), 0); + geometry::Polyline polyline({bulge}); + + createTaskFromPolyline(std::move(polyline)); + + config::Profiles::Profile profile = m_profile; + profile.cut().passAtZeroDepth() = false; + + config::Tools::Tool tool = m_tool; + tool.general().depthPerCut() = 0.02f; + + DepthTrackerVisitor visitor; + exporter::renderer::Renderer renderer(tool, profile, visitor); + renderer.render(*m_document); + + const int nbCut = 0.1f / 0.02f + 1; + EXPECT_EQ(visitor.depths.size(), nbCut); + + EXPECT_FLOAT_EQ(visitor.depths.front(), -0.02f); + EXPECT_FLOAT_EQ(visitor.depths.back(), -0.1f); +} + +TEST_F(ExporterRendererFixture, shouldCutPathSingleDepth) +{ + const geometry::Bulge bulge(QVector2D(0, 0), QVector2D(1, 1), 0); + geometry::Polyline polyline({bulge}); + + createTaskFromPolyline(std::move(polyline)); + + config::Tools::Tool tool = m_tool; + tool.general().depthPerCut() = 0.2f; + + DepthTrackerVisitor visitor; + exporter::renderer::Renderer renderer(tool, m_profile, visitor); + renderer.render(*m_document); + + const int nbCut = 1; + EXPECT_EQ(visitor.depths.size(), nbCut); + + EXPECT_FLOAT_EQ(visitor.depths.front(), -0.1f); +} + + diff --git a/test/gcodeexporter.cpp b/test/gcodeexporter.cpp index a418674..b7b4660 100644 --- a/test/gcodeexporter.cpp +++ b/test/gcodeexporter.cpp @@ -17,10 +17,8 @@ TEST_F(ExporterFixture, shouldRenderAllPathsWhenAllVisible) EXPECT_EQ(R"(G0 Z 1.000 G0 X 0.000 Y 0.000 M4 S 10.000 -G1 Z -0.000 F 10.000 -G1 X 1.000 Y 1.000 F 10.000 G1 Z -0.100 F 10.000 -G1 X 0.000 Y 0.000 F 10.000 +G1 X 1.000 Y 1.000 F 10.000 G0 Z 1.000 M5 G0 X 0.000 Y 0.000 @@ -48,10 +46,6 @@ TEST_F(ExporterFixture, shouldRenderOffsetedRightCwTriangleBeCutBackward) EXPECT_EQ(R"(G0 Z 1.000 G0 X 0.483 Y 0.200 M4 S 10.000 -G1 Z -0.000 F 10.000 -G1 X 0.800 Y 0.200 F 10.000 -G1 X 0.800 Y 0.517 F 10.000 -G1 X 0.483 Y 0.200 F 10.000 G1 Z -0.100 F 10.000 G1 X 0.800 Y 0.200 F 10.000 G1 X 0.800 Y 0.517 F 10.000 @@ -83,13 +77,6 @@ TEST_F(ExporterFixture, shouldRenderOffsetedLeftCwTriangleBeCutForward) EXPECT_EQ(R"(G0 Z 1.000 G0 X -0.141 Y 0.141 M4 S 10.000 -G1 Z -0.000 F 10.000 -G1 X 0.859 Y 1.141 F 10.000 -G2 X 1.200 Y 1.000 I 0.141 J -0.141 F 10.000 -G1 X 1.200 Y 0.000 F 10.000 -G2 X 1.000 Y -0.200 I -0.200 J 0.000 F 10.000 -G1 X 0.000 Y -0.200 F 10.000 -G2 X -0.141 Y 0.141 I 0.000 J 0.200 F 10.000 G1 Z -0.100 F 10.000 G1 X 0.859 Y 1.141 F 10.000 G2 X 1.200 Y 1.000 I 0.141 J -0.141 F 10.000 diff --git a/test/orderoptimizer.cpp b/test/orderoptimizer.cpp new file mode 100644 index 0000000..df906e9 --- /dev/null +++ b/test/orderoptimizer.cpp @@ -0,0 +1,60 @@ +#include + +#include + +#include + +TEST(OrderOptimizer, shouldRespectGroups) +{ + geometry::OrderOptimizer::NodesPerGroup groups = { + { + {{}, 1, {1, 4}}, + {{}, 3, {1, 2}}, + {{}, 2, {2, 3}} + }, + { + {{}, 0, {4, 1}}, + {{}, 5, {5, 3}} + }, + { + {{}, 4, {2, 2}} + } + }; + + std::vector groupById(6); + for (int groupId = 0, nbGroup = groups.size(); groupId < nbGroup; ++groupId) { + for (const geometry::OrderOptimizer::Node& node : groups[groupId]) { + groupById[node.id] = groupId; + } + } + + geometry::OrderOptimizer optimizer(groups, 6); + const std::vector order = optimizer.order(); + + for (int id : order) { + std::cout << id << " "; + } + std::cout << std::endl; + + for (int i = 0; i < 5; ++i) { + const int group1 = groupById[order[i]]; + const int group2 = groupById[order[i + 1]]; + EXPECT_LE(group1, group2); + } +} + + +TEST(OrderOptimizer, shouldOrderSingleGroup) +{ + geometry::OrderOptimizer::NodesPerGroup groups = { + { + {{}, 0, {1, 4}}, + {{}, 1, {1, 2}}, + {{}, 2, {2, 3}}, + {{}, 3, {4, 1}} + } + }; + + geometry::OrderOptimizer optimizer(groups, 4); + const std::vector order = optimizer.order(); +} diff --git a/test/simulation.cpp b/test/simulation.cpp index aa67658..c18af09 100644 --- a/test/simulation.cpp +++ b/test/simulation.cpp @@ -37,7 +37,7 @@ TEST(SimulationTest, shouldHasMultiLayerDepth) const geometry::Bulge bulge(QVector2D(1, 0), QVector2D(1, 1), 0); geometry::Polyline polyline({bulge}); - const model::PathSettings settings{10, 10, 10, nbCut - 1}; + const model::PathSettings settings{10, 10, 10, nbCut}; model::Document::UPtr document = documentFromPolylines(std::move(polyline), settings); model::Simulation simulation(*document, 100.0f); diff --git a/thirdparty/cavaliercontours b/thirdparty/cavaliercontours index 7a35376..31a0129 160000 --- a/thirdparty/cavaliercontours +++ b/thirdparty/cavaliercontours @@ -1 +1 @@ -Subproject commit 7a35376eb4c2d5f917d3e0564ea630c94137255e +Subproject commit 31a012947aa2e7e9474e2ec90502825afe8b99a4 diff --git a/thirdparty/cereal b/thirdparty/cereal index 02eace1..ebef1e9 160000 --- a/thirdparty/cereal +++ b/thirdparty/cereal @@ -1 +1 @@ -Subproject commit 02eace19a99ce3cd564ca4e379753d69af08c2c8 +Subproject commit ebef1e929807629befafbb2918ea1a08c7194554 diff --git a/thirdparty/nanoflann b/thirdparty/nanoflann index 19f4b91..9a653cb 160000 --- a/thirdparty/nanoflann +++ b/thirdparty/nanoflann @@ -1 +1 @@ -Subproject commit 19f4b910c721088cb302283093fcdd4c1d62d4ac +Subproject commit 9a653cb243db6a09c94f833b28732b62f033e2b5 diff --git a/thirdparty/yaml-cpp b/thirdparty/yaml-cpp index 0579ae3..2f86d13 160000 --- a/thirdparty/yaml-cpp +++ b/thirdparty/yaml-cpp @@ -1 +1 @@ -Subproject commit 0579ae3d976091d7d664aa9d2527e0d0cff25763 +Subproject commit 2f86d13775d119edbb69af52e5f566fd65c6953b