From 379a5681e1fcccbd5658ec08824067f523753613 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 17:21:51 +0200 Subject: [PATCH 01/30] feat: move path top or bottom with two buttons --- resource/resource.qrc | 2 ++ src/model/task.cpp | 21 +++++++++++++++++++++ src/model/task.h | 7 +++++++ src/view/task/pathlistmodel.cpp | 17 +++++++++++++++++ src/view/task/pathlistmodel.h | 1 + src/view/task/task.cpp | 10 ++++++++++ src/view/task/task.h | 4 +++- template/uic/task.ui | 22 ++++++++++++++++++++++ 8 files changed, 83 insertions(+), 1 deletion(-) diff --git a/resource/resource.qrc b/resource/resource.qrc index 3d4bac2..1c0939e 100644 --- a/resource/resource.qrc +++ b/resource/resource.qrc @@ -13,8 +13,10 @@ 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 diff --git a/src/model/task.cpp b/src/model/task.cpp index b2ea3d1..07a538c 100644 --- a/src/model/task.cpp +++ b/src/model/task.cpp @@ -72,6 +72,27 @@ 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::resetCutterCompensationSelection() { forEachSelectedPath([](model::Path &path){ path.resetOffset(); }); diff --git a/src/model/task.h b/src/model/task.h index 770ef3d..57fcc69 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -30,6 +30,12 @@ class Task : public QObject, public common::Aggregable DOWN = 1 }; + enum class MoveTip + { + Top, + Bottom + }; + explicit Task() = default; explicit Task(Layer::ListUPtr &&layers); @@ -39,6 +45,7 @@ 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); template void forEachPathInStack(Functor &&functor) const diff --git a/src/view/task/pathlistmodel.cpp b/src/view/task/pathlistmodel.cpp index f9ca39e..0f50fc6 100644 --- a/src/view/task/pathlistmodel.cpp +++ b/src/view/task/pathlistmodel.cpp @@ -96,6 +96,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) { diff --git a/src/view/task/pathlistmodel.h b/src/view/task/pathlistmodel.h index 8652e4a..724cd1d 100644 --- a/src/view/task/pathlistmodel.h +++ b/src/view/task/pathlistmodel.h @@ -26,6 +26,7 @@ class PathListModel : public QAbstractListModel Qt::ItemFlags flags(const QModelIndex &index) const override; QModelIndex movePath(const QModelIndex &index, model::Task::MoveDirection direction); + QModelIndex movePathToTip(const QModelIndex &index, model::Task::MoveTip tip); void itemClicked(const QModelIndex &index); void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel); diff --git a/src/view/task/task.cpp b/src/view/task/task.cpp index 6ad375b..abf61e3 100644 --- a/src/view/task/task.cpp +++ b/src/view/task/task.cpp @@ -33,6 +33,8 @@ void Task::setupController() connect(moveUp, &QPushButton::pressed, [this](){ moveCurrentPath(model::Task::MoveDirection::UP); }); connect(moveDown, &QPushButton::pressed, [this](){ moveCurrentPath(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) @@ -61,4 +63,12 @@ void Task::moveCurrentPath(model::Task::MoveDirection direction) selectionModel->setCurrentIndex(newSelectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); } +void Task::moveCurrentPathToTip(model::Task::MoveTip tip) +{ + QItemSelectionModel *selectionModel = pathsTreeView->selectionModel(); + const QModelIndex currentSelectedIndex = selectionModel->currentIndex(); + const QModelIndex newSelectedIndex = m_pathListModel->movePathToTip(currentSelectedIndex, tip); + selectionModel->setCurrentIndex(newSelectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); +} + } diff --git a/src/view/task/task.h b/src/view/task/task.h index ba35ea2..6f4af87 100644 --- a/src/view/task/task.h +++ b/src/view/task/task.h @@ -49,6 +49,9 @@ class Task : public model::DocumentModelObserver, private Ui::Task void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag); + void moveCurrentPath(model::Task::MoveDirection direction); + void moveCurrentPathToTip(model::Task::MoveTip tip); + public: explicit Task(model::Application &app); @@ -58,7 +61,6 @@ class Task : public model::DocumentModelObserver, private Ui::Task protected Q_SLOTS: void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void pathSelectedChanged(model::Path &path, bool selected); - void moveCurrentPath(model::Task::MoveDirection direction); }; } 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 + + + From f66f04170229c80ae28a15f011292382cd684e04 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 16:24:15 +0200 Subject: [PATCH 02/30] feat: Implement undo and redo actions --- src/model/CMakeLists.txt | 2 ++ src/model/application.cpp | 54 +++++++++++++++++++++++++++---- src/model/application.h | 12 ++++++- src/model/document.cpp | 8 +++-- src/model/document.h | 11 +++---- src/model/documentmodelobserver.h | 25 +++++++++++++- src/model/layer.cpp | 11 +++++++ src/model/layer.h | 1 + src/model/offsettedpath.cpp | 7 ++++ src/model/offsettedpath.h | 1 + src/model/path.cpp | 13 ++++++++ src/model/path.h | 1 + src/model/renderable.cpp | 8 +++++ src/model/renderable.h | 1 + src/model/task.cpp | 19 +++++++++++ src/model/task.h | 1 + src/view/mainwindow.cpp | 22 +++++++++++-- src/view/mainwindow.h | 6 ++-- src/view/profile.cpp | 29 +---------------- src/view/profile.h | 5 --- src/view/task/layertreemodel.cpp | 2 ++ src/view/task/layertreemodel.h | 3 ++ src/view/task/path.cpp | 3 +- src/view/task/path.h | 10 +++++- src/view/task/pathlistmodel.h | 3 ++ src/view/task/task.cpp | 10 +++++- src/view/task/task.h | 5 +++ src/view/view2d/viewport.cpp | 6 +++- src/view/view2d/viewport.h | 1 + template/uic/mainwindow.ui | 19 +++++++++++ template/uic/path.ui | 14 +++++++- 31 files changed, 252 insertions(+), 61 deletions(-) diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt index 340dc4a..4fd7a2e 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 diff --git a/src/model/application.cpp b/src/model/application.cpp index 3daf544..2088489 100644 --- a/src/model/application.cpp +++ b/src/model/application.cpp @@ -40,6 +40,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,7 +115,7 @@ geometry::Polyline::List Application::postProcessImportedPolylines(geometry::Pol // Remove small bulges geometry::filter::Cleaner cleaner(assembler.polylines(), dxf.minimumPolylineLength(), dxf.minimumArcLength()); - if (dxf.sortPathByLength()) { + if (dxf.sortPathByLength()) { // TODO sort between all paths geometry::filter::Sorter sorter(cleaner.polylines()); return sorter.polylines(); } @@ -252,15 +267,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 +282,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 +328,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 +354,8 @@ void Application::pocketSelection() Task &task = m_openedDocument->task(); task.pocketSelection(radius, dxf.minimumPolylineLength(), dxf.minimumArcLength()); + + takeDocumentSnapshot(); } geometry::Rect Application::selectionBoundingRect() const @@ -349,18 +368,24 @@ 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(); } Simulation Application::createSimulation() @@ -369,4 +394,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..d3ad8db 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(); @@ -107,8 +112,13 @@ class Application : public QObject 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 errorRaised(const QString& message) const; 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/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..526688d 100644 --- a/src/model/path.cpp +++ b/src/model/path.cpp @@ -27,6 +27,19 @@ 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::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..41e0dce 100644 --- a/src/model/path.h +++ b/src/model/path.h @@ -34,6 +34,7 @@ class Path : public Renderable, public common::Aggregable public: explicit Path(geometry::Polyline &&basePolyline, const std::string &name, const PathSettings& settings); + explicit Path(const Path& other); explicit Path() = default; 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 07a538c..0ca106c 100644 --- a/src/model/task.cpp +++ b/src/model/task.cpp @@ -1,6 +1,7 @@ #include #include +#include namespace model { @@ -35,6 +36,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(); diff --git a/src/model/task.h b/src/model/task.h index 57fcc69..9f04a3d 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -38,6 +38,7 @@ class Task : public QObject, public common::Aggregable explicit Task() = default; explicit Task(Layer::ListUPtr &&layers); + explicit Task(const Task &other); int pathCount() const; const Path &pathAt(int index) const; diff --git a/src/view/mainwindow.cpp b/src/view/mainwindow.cpp index 80846d0..69d0fe9 100644 --- a/src/view/mainwindow.cpp +++ b/src/view/mainwindow.cpp @@ -99,6 +99,8 @@ 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, this, &MainWindow::undo); + connect(actionRedo, &QAction::triggered, this, &MainWindow::redo); } void MainWindow::setupOpenedDocumentActions() @@ -117,6 +119,8 @@ 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.setExclusive(true); } @@ -144,7 +148,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); } @@ -216,6 +220,7 @@ void MainWindow::transformSelection() { dialogs::Transform transform; if (transform.exec() == QDialog::Accepted) { + m_app.takeDocumentSnapshot(); m_app.transformSelection(transform.matrix()); } } @@ -224,6 +229,7 @@ void MainWindow::mirrorSelection() { dialogs::Mirror mirror; if (mirror.exec() == QDialog::Accepted) { + m_app.takeDocumentSnapshot(); m_app.transformSelection(mirror.matrix()); } } @@ -234,11 +240,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 +265,15 @@ void MainWindow::simulate() m_simulation->show(); } +void MainWindow::undo() +{ + m_app.undoDocumentChanges(); +} + +void MainWindow::redo() +{ + m_app.redoDocumentChanges(); +} + + } diff --git a/src/view/mainwindow.h b/src/view/mainwindow.h index f3a85b3..e0df23d 100644 --- a/src/view/mainwindow.h +++ b/src/view/mainwindow.h @@ -53,11 +53,11 @@ 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 undo(); + void redo(); }; } 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/task/layertreemodel.cpp b/src/view/task/layertreemodel.cpp index 61eef27..1253219 100644 --- a/src/view/task/layertreemodel.cpp +++ b/src/view/task/layertreemodel.cpp @@ -117,6 +117,8 @@ void LayerTreeModel::itemClicked(const QModelIndex& index) model::Renderable *item = static_cast(index.internalPointer()); item->toggleVisible(); + emit documentVisibilityChanged(); + emit dataChanged(index, index); } } diff --git a/src/view/task/layertreemodel.h b/src/view/task/layertreemodel.h index d8863c7..9fb250d 100644 --- a/src/view/task/layertreemodel.h +++ b/src/view/task/layertreemodel.h @@ -30,6 +30,9 @@ class LayerTreeModel: public QAbstractItemModel void itemClicked(const QModelIndex &index); 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..396759c 100644 --- a/src/view/task/path.cpp +++ b/src/view/task/path.cpp @@ -23,7 +23,8 @@ void Path::documentChanged() } Path::Path(model::Application &app) - :DocumentModelObserver(app) + :DocumentModelObserver(app), + m_app(app) { setupUi(this); } diff --git a/src/view/task/path.h b/src/view/task/path.h index ddc7d27..51b5ad4 100644 --- a/src/view/task/path.h +++ b/src/view/task/path.h @@ -14,6 +14,8 @@ 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); @@ -21,7 +23,13 @@ class Path : public model::DocumentModelObserver, private Ui::Path 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, field, func](ValueType value){ + func(value); + + m_app.takeDocumentSnapshot(); + }); } template diff --git a/src/view/task/pathlistmodel.h b/src/view/task/pathlistmodel.h index 724cd1d..3c92dc1 100644 --- a/src/view/task/pathlistmodel.h +++ b/src/view/task/pathlistmodel.h @@ -31,6 +31,9 @@ class PathListModel : public QAbstractListModel 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 abf61e3..6ec27ee 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); } @@ -61,6 +62,13 @@ void Task::moveCurrentPath(model::Task::MoveDirection direction) const QModelIndex currentSelectedIndex = selectionModel->currentIndex(); const QModelIndex newSelectedIndex = m_pathListModel->movePath(currentSelectedIndex, direction); selectionModel->setCurrentIndex(newSelectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); + + m_app.takeDocumentSnapshot(); +} + +void Task::documentVisibilityChanged() +{ + m_app.takeDocumentSnapshot(); } void Task::moveCurrentPathToTip(model::Task::MoveTip tip) diff --git a/src/view/task/task.h b/src/view/task/task.h index 6f4af87..367d02b 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(); @@ -61,6 +65,7 @@ class Task : public model::DocumentModelObserver, private Ui::Task protected Q_SLOTS: void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void pathSelectedChanged(model::Path &path, bool selected); + 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/uic/mainwindow.ui b/template/uic/mainwindow.ui index 98ffe7b..dfe9ad4 100644 --- a/template/uic/mainwindow.ui +++ b/template/uic/mainwindow.ui @@ -63,6 +63,9 @@ + + + @@ -310,6 +313,22 @@ Ctrl+R + + + Redo + + + Ctrl+Y + + + + + Undo + + + Ctrl+Z + + diff --git a/template/uic/path.ui b/template/uic/path.ui index 07d23c6..83882f4 100644 --- a/template/uic/path.ui +++ b/template/uic/path.ui @@ -7,7 +7,7 @@ 0 0 230 - 168 + 176 @@ -34,6 +34,9 @@ + + false + 9999.989999999999782 @@ -48,6 +51,9 @@ + + false + 9999.989999999999782 @@ -62,6 +68,9 @@ + + false + 9999.989999999999782 @@ -79,6 +88,9 @@ + + false + 9999.989999999999782 From 103e566cade5d57d99f3e65b3146af2c1e3abdc7 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 17:29:18 +0200 Subject: [PATCH 03/30] chore: track modification for move top/bottom path --- src/view/task/task.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/view/task/task.cpp b/src/view/task/task.cpp index 6ec27ee..2cfd758 100644 --- a/src/view/task/task.cpp +++ b/src/view/task/task.cpp @@ -77,6 +77,8 @@ void Task::moveCurrentPathToTip(model::Task::MoveTip tip) const QModelIndex currentSelectedIndex = selectionModel->currentIndex(); const QModelIndex newSelectedIndex = m_pathListModel->movePathToTip(currentSelectedIndex, tip); selectionModel->setCurrentIndex(newSelectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); + + m_app.takeDocumentSnapshot(); } } From d383069ebcb69d6652f767a11e498467d17b1586 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 17:35:34 +0200 Subject: [PATCH 04/30] fix: missing files --- resource/icons/layer-bottom.svg | 22 +++++++++++++ resource/icons/layer-top.svg | 22 +++++++++++++ src/common/copy.h | 21 +++++++++++++ src/model/documenthistory.cpp | 55 +++++++++++++++++++++++++++++++++ src/model/documenthistory.h | 27 ++++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 resource/icons/layer-bottom.svg create mode 100644 resource/icons/layer-top.svg create mode 100644 src/common/copy.h create mode 100644 src/model/documenthistory.cpp create mode 100644 src/model/documenthistory.h 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/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/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(); +}; + +} From 26aabad3075c3bc42ab3e8c010fb3084b8c47cd6 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 18:21:14 +0200 Subject: [PATCH 05/30] chore: cleanup --- src/view/task/path.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/task/path.h b/src/view/task/path.h index 51b5ad4..bfb35f3 100644 --- a/src/view/task/path.h +++ b/src/view/task/path.h @@ -25,7 +25,7 @@ class Path : public model::DocumentModelObserver, private Ui::Path { disconnect(field, static_cast(&Field::valueChanged), nullptr, nullptr); - connect(field, static_cast(&Field::valueChanged), [this, field, func](ValueType value){ + connect(field, static_cast(&Field::valueChanged), [this, func](ValueType value){ func(value); m_app.takeDocumentSnapshot(); From 7c02524e1e3131511ec40ca689cbf588d0bfbef2 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 19:31:18 +0200 Subject: [PATCH 06/30] fix: selection in path and layer list --- src/view/task/layertreemodel.cpp | 20 ++++++++++++++++- src/view/task/layertreemodel.h | 2 ++ src/view/task/pathlistmodel.cpp | 22 ++++++++++++++++-- src/view/task/pathlistmodel.h | 4 +++- src/view/task/task.cpp | 38 ++++++++++++++++++++------------ src/view/task/task.h | 20 +++++++++++++++-- 6 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/view/task/layertreemodel.cpp b/src/view/task/layertreemodel.cpp index 1253219..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) { } @@ -124,16 +125,33 @@ void LayerTreeModel::itemClicked(const QModelIndex& 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 9fb250d..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,6 +29,7 @@ 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); diff --git a/src/view/task/pathlistmodel.cpp b/src/view/task/pathlistmodel.cpp index 0f50fc6..18e0e37 100644 --- a/src/view/task/pathlistmodel.cpp +++ b/src/view/task/pathlistmodel.cpp @@ -8,7 +8,8 @@ namespace view::task PathListModel::PathListModel(model::Task &task, QObject *parent) :QAbstractListModel(parent), - m_task(task) + m_task(task), + m_ignoreSelectionChanged(false) { } @@ -74,7 +75,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; @@ -130,14 +131,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 3c92dc1..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,10 +26,11 @@ 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); diff --git a/src/view/task/task.cpp b/src/view/task/task.cpp index 2cfd758..3067af9 100644 --- a/src/view/task/task.cpp +++ b/src/view/task/task.cpp @@ -32,8 +32,8 @@ 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); }); } @@ -56,14 +56,11 @@ 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); - - m_app.takeDocumentSnapshot(); + moveCurrentPath([this, direction](const QModelIndex& index){ + m_pathListModel->movePathToDirection(index, direction); + }); } void Task::documentVisibilityChanged() @@ -73,12 +70,25 @@ void Task::documentVisibilityChanged() void Task::moveCurrentPathToTip(model::Task::MoveTip tip) { - QItemSelectionModel *selectionModel = pathsTreeView->selectionModel(); - const QModelIndex currentSelectedIndex = selectionModel->currentIndex(); - const QModelIndex newSelectedIndex = m_pathListModel->movePathToTip(currentSelectedIndex, tip); - selectionModel->setCurrentIndex(newSelectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); + moveCurrentPath([this, tip](const QModelIndex& index){ + m_pathListModel->movePathToTip(index, tip); + }); +} - m_app.takeDocumentSnapshot(); +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 367d02b..a532270 100644 --- a/src/view/task/task.h +++ b/src/view/task/task.h @@ -53,9 +53,26 @@ class Task : public model::DocumentModelObserver, private Ui::Task void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag); - void moveCurrentPath(model::Task::MoveDirection direction); + 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); @@ -63,7 +80,6 @@ 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 documentVisibilityChanged(); }; From 3dc3f95ded8d2814ffb753e695a8817ea62ffc4b Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 19:44:05 +0200 Subject: [PATCH 07/30] fix: sort paths by lengths between all layers and not only inside layers --- src/geometry/filter/CMakeLists.txt | 2 -- src/geometry/filter/sorter.cpp | 43 ------------------------------ src/geometry/filter/sorter.h | 21 --------------- src/model/application.cpp | 13 +++++---- src/model/task.cpp | 31 +++++++++++++++++++++ src/model/task.h | 2 ++ 6 files changed, 41 insertions(+), 71 deletions(-) delete mode 100644 src/geometry/filter/sorter.cpp delete mode 100644 src/geometry/filter/sorter.h 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/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/model/application.cpp b/src/model/application.cpp index 2088489..37bd8e6 100644 --- a/src/model/application.cpp +++ b/src/model/application.cpp @@ -115,10 +115,6 @@ geometry::Polyline::List Application::postProcessImportedPolylines(geometry::Pol // Remove small bulges geometry::filter::Cleaner cleaner(assembler.polylines(), dxf.minimumPolylineLength(), dxf.minimumArcLength()); - if (dxf.sortPathByLength()) { // TODO sort between all paths - geometry::filter::Sorter sorter(cleaner.polylines()); - return sorter.polylines(); - } return cleaner.polylines(); } @@ -135,7 +131,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() diff --git a/src/model/task.cpp b/src/model/task.cpp index 0ca106c..f163b7e 100644 --- a/src/model/task.cpp +++ b/src/model/task.cpp @@ -112,6 +112,37 @@ void Task::movePathToTip(int index, MoveTip tip) } } +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(); }); diff --git a/src/model/task.h b/src/model/task.h index 9f04a3d..eb65b21 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -48,6 +48,8 @@ class Task : public QObject, public common::Aggregable void movePath(int index, MoveDirection direction); void movePathToTip(int index, MoveTip tip); + void sortPathsByLength(); + template void forEachPathInStack(Functor &&functor) const { From 742e496cbb13278e5f36efc768dad23a1e3ebbf5 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 20:27:31 +0200 Subject: [PATCH 08/30] chore: reatiain UI space for path parameters when no paths are selected --- src/model/application.cpp | 1 - src/view/task/path.cpp | 8 +- template/uic/path.ui | 181 ++++++++++++++++++++++---------------- 3 files changed, 109 insertions(+), 81 deletions(-) diff --git a/src/model/application.cpp b/src/model/application.cpp index 37bd8e6..dde6207 100644 --- a/src/model/application.cpp +++ b/src/model/application.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include diff --git a/src/view/task/path.cpp b/src/view/task/path.cpp index 396759c..14d5acc 100644 --- a/src/view/task/path.cpp +++ b/src/view/task/path.cpp @@ -7,7 +7,7 @@ void Path::setupModel() { m_groupSettings.reset(new model::PathGroupSettings(task())); - hide(); + stackedWidget->setCurrentWidget(pageNoSelection); connect(&task(), &model::Task::selectionChanged, this, &Path::selectionChanged); @@ -32,15 +32,15 @@ Path::Path(model::Application &app) 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); } } diff --git a/template/uic/path.ui b/template/uic/path.ui index 83882f4..a796c43 100644 --- a/template/uic/path.ui +++ b/template/uic/path.ui @@ -15,88 +15,117 @@ - - - QLayout::SetDefaultConstraint + + + 0 - - QFormLayout::AllNonFixedFieldsGrow - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - Plane Feed Rate - - - - - - - false - - - 9999.989999999999782 - - - - - - - Intensity - - - - - - - false - - - 9999.989999999999782 - - - - - - - Depth - - - - - - - false - - - 9999.989999999999782 + + + + 0 - - 0.100000000000000 + + 0 - - - - - - Depth Feed Rate + + 0 - - - - - - false + + 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 + + + + + + + + + + + From 1c7942bd97255124342191880488ba47dc0dc43e Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Aug 2023 20:35:39 +0200 Subject: [PATCH 09/30] chore: increase simulation time slider step --- template/uic/simulation/simulation.ui | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 194795e54a677dddcc1a46eb0837748e91fe325b Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 25 Aug 2023 23:21:46 +0200 Subject: [PATCH 10/30] fix: setting snot saved when setting windows closed --- src/config/config.cpp | 1 - src/view/mainwindow.cpp | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) 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/view/mainwindow.cpp b/src/view/mainwindow.cpp index 69d0fe9..ae18d36 100644 --- a/src/view/mainwindow.cpp +++ b/src/view/mainwindow.cpp @@ -212,6 +212,7 @@ void MainWindow::openSettings() }(); if (accepted) { + newConfig.save(); m_app.setConfig(std::move(newConfig)); } } From 4a43c7d7759aa2ce9cd100ccee9c0ae871133071 Mon Sep 17 00:00:00 2001 From: tristan Date: Sun, 27 Aug 2023 11:25:39 +0200 Subject: [PATCH 11/30] feat: Introduce an optimizer for path cut order respecting group order. This optimizer take as input a list of node sorted by group and compute an optimum order with minimal distance when passing through all nodes. By definition all nodes of the same groups are next to each other and the nodes of the group i are selected before nodes of group i + 1. In a concrete case, these groups will mean paths of the relativly same length. --- .gitmodules | 3 + CMakeLists.txt | 7 ++ resource/icons/optimize-order.svg | 8 ++ resource/resource.qrc | 1 + src/geometry/CMakeLists.txt | 3 + src/geometry/orderoptimizer.cpp | 131 ++++++++++++++++++++++++++++++ src/geometry/orderoptimizer.h | 72 ++++++++++++++++ template/uic/mainwindow.ui | 17 ++++ test/CMakeLists.txt | 1 + test/orderoptimizer.cpp | 40 +++++++++ thirdparty/CMakeLists.txt | 1 + thirdparty/or-tools | 1 + 12 files changed, 285 insertions(+) create mode 100644 resource/icons/optimize-order.svg create mode 100644 src/geometry/orderoptimizer.cpp create mode 100644 src/geometry/orderoptimizer.h create mode 100644 test/orderoptimizer.cpp create mode 160000 thirdparty/or-tools 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..e5171da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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>") @@ -58,6 +64,7 @@ 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 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 1c0939e..1fa56d7 100644 --- a/resource/resource.qrc +++ b/resource/resource.qrc @@ -21,6 +21,7 @@ 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/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/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/template/uic/mainwindow.ui b/template/uic/mainwindow.ui index dfe9ad4..3b98e2d 100644 --- a/template/uic/mainwindow.ui +++ b/template/uic/mainwindow.ui @@ -92,6 +92,8 @@ + + @@ -329,6 +331,21 @@ Ctrl+Z + + + + :/icons/optimize-order.svg:/icons/optimize-order.svg + + + Optimize Order + + + Optimize Order + + + Ctrl+Shift+O + + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 01c4373..87b6aa4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,6 +21,7 @@ set(SRC dxfplotimporter.cpp exporterfixture.cpp gcodeexporter.cpp + orderoptimizer.cpp path.cpp pathsettings.cpp pocketer.cpp diff --git a/test/orderoptimizer.cpp b/test/orderoptimizer.cpp new file mode 100644 index 0000000..2ed7f60 --- /dev/null +++ b/test/orderoptimizer.cpp @@ -0,0 +1,40 @@ +#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 i = 0; i < 6; ++i) { + const int group1 = groupById[order[i]]; + const int group2 = groupById[order[i + 1]]; + EXPECT_LE(group1, group2); + } +} + diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index c4dea1d..32f1f0d 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -4,3 +4,4 @@ add_subdirectory(fmt) add_subdirectory(libdxfrw) add_subdirectory(nanoflann) add_subdirectory(yaml-cpp) +add_subdirectory(or-tools) diff --git a/thirdparty/or-tools b/thirdparty/or-tools new file mode 160000 index 0000000..44e95f5 --- /dev/null +++ b/thirdparty/or-tools @@ -0,0 +1 @@ +Subproject commit 44e95f51b0c84b6c25414f81c0175703053ddfff From 655947aafb07b20d69ed417c8c834b81d2ba108c Mon Sep 17 00:00:00 2001 From: tristan Date: Sun, 27 Aug 2023 16:53:44 +0200 Subject: [PATCH 12/30] feat: Restrict path ordering to group of similar length --- CMakeLists.txt | 2 +- src/model/CMakeLists.txt | 1 + src/model/application.cpp | 8 ++++ src/model/application.h | 2 + src/model/task.cpp | 78 +++++++++++++++++++++++++++++++++ src/model/task.h | 2 + src/view/mainwindow.cpp | 18 ++++---- src/view/mainwindow.h | 3 +- src/view/task/pathlistmodel.cpp | 3 ++ template/config.xml | 5 +++ test/orderoptimizer.cpp | 22 +++++++++- thirdparty/or-tools | 2 +- 12 files changed, 131 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5171da..cb42eee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,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) diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt index 4fd7a2e..e653052 100644 --- a/src/model/CMakeLists.txt +++ b/src/model/CMakeLists.txt @@ -27,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 dde6207..9b630ca 100644 --- a/src/model/application.cpp +++ b/src/model/application.cpp @@ -390,6 +390,14 @@ void Application::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() { const float fastMoveFeedRate = m_config.root().simulation().fastMoveFeedRate(); diff --git a/src/model/application.h b/src/model/application.h index d3ad8db..2fe25ee 100644 --- a/src/model/application.h +++ b/src/model/application.h @@ -110,6 +110,8 @@ class Application : public QObject void hideSelection(); void showHidden(); + void optimizeOrder(); + Simulation createSimulation(); void takeDocumentSnapshot(); diff --git a/src/model/task.cpp b/src/model/task.cpp index f163b7e..94f5997 100644 --- a/src/model/task.cpp +++ b/src/model/task.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace model { @@ -197,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 eb65b21..da654cb 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -91,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; @@ -104,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 ae18d36..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,8 +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, this, &MainWindow::undo); - connect(actionRedo, &QAction::triggered, this, &MainWindow::redo); + 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() @@ -121,6 +124,7 @@ void MainWindow::setupOpenedDocumentActions() m_openedDocumentActions.addAction(actionSimulate); m_openedDocumentActions.addAction(actionUndo); m_openedDocumentActions.addAction(actionRedo); + m_openedDocumentActions.addAction(actionOptimizeOrder); m_openedDocumentActions.setExclusive(true); } @@ -266,15 +270,9 @@ void MainWindow::simulate() m_simulation->show(); } -void MainWindow::undo() +void MainWindow::optimizeOrder() { - m_app.undoDocumentChanges(); + m_app.optimizeOrder(); } -void MainWindow::redo() -{ - m_app.redoDocumentChanges(); -} - - } diff --git a/src/view/mainwindow.h b/src/view/mainwindow.h index e0df23d..51259cc 100644 --- a/src/view/mainwindow.h +++ b/src/view/mainwindow.h @@ -56,8 +56,7 @@ protected Q_SLOTS: void newDocumentOpened(model::Document *newDocument); void displayError(const QString &message); void simulate(); - void undo(); - void redo(); + void optimizeOrder(); }; } diff --git a/src/view/task/pathlistmodel.cpp b/src/view/task/pathlistmodel.cpp index 18e0e37..b6aab28 100644 --- a/src/view/task/pathlistmodel.cpp +++ b/src/view/task/pathlistmodel.cpp @@ -11,6 +11,9 @@ PathListModel::PathListModel(model::Task &task, QObject *parent) m_task(task), m_ignoreSelectionChanged(false) { + connect(&m_task, &model::Task::pathOrderChanged, [this]{ + emit layoutChanged(); + }); } QVariant PathListModel::data(const QModelIndex &index, int role) const diff --git a/template/config.xml b/template/config.xml index 6173e73..f031c10 100644 --- a/template/config.xml +++ b/template/config.xml @@ -9,6 +9,11 @@ + + + + + diff --git a/test/orderoptimizer.cpp b/test/orderoptimizer.cpp index 2ed7f60..df906e9 100644 --- a/test/orderoptimizer.cpp +++ b/test/orderoptimizer.cpp @@ -31,10 +31,30 @@ TEST(OrderOptimizer, shouldRespectGroups) geometry::OrderOptimizer optimizer(groups, 6); const std::vector order = optimizer.order(); - for (int i = 0; i < 6; ++i) { + 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/thirdparty/or-tools b/thirdparty/or-tools index 44e95f5..ed8db90 160000 --- a/thirdparty/or-tools +++ b/thirdparty/or-tools @@ -1 +1 @@ -Subproject commit 44e95f51b0c84b6c25414f81c0175703053ddfff +Subproject commit ed8db9097cdbaa06b40384d3a86164744a606043 From 8f401b79d6ac039e44028d7f71691a75dd65d9c3 Mon Sep 17 00:00:00 2001 From: tristan Date: Sat, 2 Sep 2023 11:24:15 +0200 Subject: [PATCH 13/30] fix: visibility not handled for path loaded from dxfplot --- src/model/path.cpp | 5 +++++ src/model/path.h | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/model/path.cpp b/src/model/path.cpp index 526688d..2201997 100644 --- a/src/model/path.cpp +++ b/src/model/path.cpp @@ -40,6 +40,11 @@ Path::Path(const Path& other) } } +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 41e0dce..9729b8f 100644 --- a/src/model/path.h +++ b/src/model/path.h @@ -35,7 +35,7 @@ class Path : public Renderable, public common::Aggregable public: explicit Path(geometry::Polyline &&basePolyline, const std::string &name, const PathSettings& settings); explicit Path(const Path& other); - explicit Path() = default; + explicit Path(); static ListUPtr FromPolylines(geometry::Polyline::List &&polylines, const PathSettings &settings, const std::string &layerName); From 863f83b4eb74682410bcf9da77a47f1367929a15 Mon Sep 17 00:00:00 2001 From: tristan Date: Sat, 2 Sep 2023 11:25:16 +0200 Subject: [PATCH 14/30] feat: Make first pass at depth 0 optional from configuration --- src/exporter/renderer/renderer.h | 5 +- template/config.xml | 1 + test/CMakeLists.txt | 1 + test/exporterrenderer.cpp | 104 +++++++++++++++++++++++++++++++ test/gcodeexporter.cpp | 15 +---- test/simulation.cpp | 2 +- 6 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 test/exporterrenderer.cpp diff --git a/src/exporter/renderer/renderer.h b/src/exporter/renderer/renderer.h index 228906b..4a40b04 100644 --- a/src/exporter/renderer/renderer.h +++ b/src/exporter/renderer/renderer.h @@ -13,6 +13,7 @@ class Renderer const config::Tools::Tool &m_tool; const config::Profiles::Profile &m_profile; const float m_depthPerCut; + const float m_maximumStartDepth; const float m_depthToRetract; Visitor &m_visitor; @@ -58,7 +59,8 @@ class Renderer m_visitor.startOperation((*iterator).start(), intensity); - for (float depth = 0.0f; depth < maxDepth + m_depthPerCut; depth += m_depthPerCut, ++iterator) { + 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); @@ -72,6 +74,7 @@ class Renderer :m_tool(tool), m_profile(profile), m_depthPerCut(m_tool.general().depthPerCut()), + m_maximumStartDepth(m_profile.cut().passAtZeroDepth() ? 0.0f : m_depthPerCut), m_depthToRetract(m_tool.general().retractDepth()), m_visitor(visitor) { diff --git a/template/config.xml b/template/config.xml index 6173e73..509df90 100644 --- a/template/config.xml +++ b/template/config.xml @@ -23,6 +23,7 @@ + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 01c4373..f4d271c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,6 +20,7 @@ set(SRC dxfplotexporter.cpp dxfplotimporter.cpp exporterfixture.cpp + exporterrenderer.cpp gcodeexporter.cpp path.cpp pathsettings.cpp 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/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); From c8815cb424d870fa93bffb22b81fbf7703aa12d2 Mon Sep 17 00:00:00 2001 From: tristan Date: Sat, 2 Sep 2023 11:29:02 +0200 Subject: [PATCH 15/30] trigger ci From 867e498fa620b41ab184fde78223c612cfbe7c3b Mon Sep 17 00:00:00 2001 From: tristan Date: Sat, 2 Sep 2023 11:35:20 +0200 Subject: [PATCH 16/30] fix: missing dependencies --- .github/workflows/deploy-linux-appimage.yml | 2 +- .github/workflows/sonarcloud.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-linux-appimage.yml b/.github/workflows/deploy-linux-appimage.yml index 13324ef..2e581e9 100644 --- a/.github/workflows/deploy-linux-appimage.yml +++ b/.github/workflows/deploy-linux-appimage.yml @@ -16,7 +16,7 @@ jobs: - name: Install package run: | sudo apt-get update - sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev + sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev libabsl-dev - name: Build and test run: ci/buildappimage.sh - name: Create Release diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e0a0bff..d10c935 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -15,7 +15,7 @@ jobs: - name: Install package run: | sudo apt-get update - sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev lcov + sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev libabsl-dev lcov - name: Install build wrapper run: | wget http://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip From 284d6bde8bffccb86985be29d88e63edc81ac1ac Mon Sep 17 00:00:00 2001 From: Tristan Porteries Date: Sat, 2 Sep 2023 11:38:56 +0200 Subject: [PATCH 17/30] Update sonarcloud.yml --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index d10c935..ac667b2 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -15,7 +15,7 @@ jobs: - name: Install package run: | sudo apt-get update - sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev libabsl-dev lcov + sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev libabsl-dev libprotobuf-dev lcov - name: Install build wrapper run: | wget http://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip From 4b5990381f2e52ada9b5a24ae3a640c89a7e4347 Mon Sep 17 00:00:00 2001 From: Tristan Porteries Date: Sat, 2 Sep 2023 11:48:48 +0200 Subject: [PATCH 18/30] Update sonarcloud.yml --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index ac667b2..d6729fe 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -15,7 +15,7 @@ jobs: - name: Install package run: | sudo apt-get update - sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev libabsl-dev libprotobuf-dev lcov + sudo apt-get -y install qtbase5-dev qt3d5-dev libqt5svg5-dev freeglut3-dev libabsl-dev libprotobuf-dev libre2-dev lcov - name: Install build wrapper run: | wget http://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip From 3ed575192113179da37746cc4ad0e9dc92db7c58 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 27 Nov 2023 20:45:57 +0100 Subject: [PATCH 19/30] fix: ensure config.h is genertaed before dxfplot importer is compiled. Solve issue #7 --- src/importer/dxfplot/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) 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) From f7ff0f1bec5ec25b764c1ba0f91de3e742c19a4a Mon Sep 17 00:00:00 2001 From: tristan Date: Sun, 11 Aug 2024 00:01:24 +0200 Subject: [PATCH 20/30] feat: add laser option skipping depth management --- src/exporter/renderer/renderer.h | 19 +++++++++++++------ template/config.xml | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/exporter/renderer/renderer.h b/src/exporter/renderer/renderer.h index 4a40b04..8948354 100644 --- a/src/exporter/renderer/renderer.h +++ b/src/exporter/renderer/renderer.h @@ -12,6 +12,7 @@ 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; @@ -50,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(); @@ -59,11 +59,17 @@ class Renderer m_visitor.startOperation((*iterator).start(), intensity); - 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); + 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); @@ -73,9 +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_maximumStartDepth(m_profile.cut().passAtZeroDepth() ? 0.0f : m_depthPerCut), - m_depthToRetract(m_tool.general().retractDepth()), + m_depthToRetract(m_laser ? 0.0f : m_tool.general().retractDepth()), m_visitor(visitor) { } diff --git a/template/config.xml b/template/config.xml index 4abd942..3c2e68c 100644 --- a/template/config.xml +++ b/template/config.xml @@ -41,6 +41,7 @@ + From 0e7b7e2f7820260fc1a158a04a4fe592bc5c3a02 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 23 Dec 2024 17:21:23 +0100 Subject: [PATCH 21/30] feat: hide depth field when a laser tool is selected --- src/model/application.cpp | 1 + src/model/application.h | 1 + src/view/task/path.cpp | 20 ++++++++++++++++++++ src/view/task/path.h | 4 ++++ template/uic/path.ui | 2 +- 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/model/application.cpp b/src/model/application.cpp index 9b630ca..1a1db4a 100644 --- a/src/model/application.cpp +++ b/src/model/application.cpp @@ -168,6 +168,7 @@ bool Application::selectTool(const QString &toolName) if (tool) { if (m_openedDocument) { m_openedDocument->setToolConfig(*tool); + emit toolChanged(); } m_defaultToolConfig = tool; diff --git a/src/model/application.h b/src/model/application.h index 2fe25ee..d883616 100644 --- a/src/model/application.h +++ b/src/model/application.h @@ -123,6 +123,7 @@ class Application : public QObject 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/view/task/path.cpp b/src/view/task/path.cpp index 14d5acc..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 @@ -27,6 +28,8 @@ Path::Path(model::Application &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) @@ -44,4 +47,21 @@ void Path::selectionChanged(bool empty) } } +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 bfb35f3..779ac18 100644 --- a/src/view/task/path.h +++ b/src/view/task/path.h @@ -19,6 +19,10 @@ class Path : public model::DocumentModelObserver, private Ui::Path 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) diff --git a/template/uic/path.ui b/template/uic/path.ui index a796c43..27222b0 100644 --- a/template/uic/path.ui +++ b/template/uic/path.ui @@ -82,7 +82,7 @@ - + Depth From 9d9d5e0c56f186aa668ad9eca34d29722395fb01 Mon Sep 17 00:00:00 2001 From: tristan Date: Sun, 11 Aug 2024 07:23:33 +0200 Subject: [PATCH 22/30] feat: use docker for ubuntu build --- .github/workflows/docker-ubuntu.yml | 42 +++++++++++++++++++++++++++++ .github/workflows/sonarcloud.yml | 18 +++++-------- CMakeLists.txt | 1 + ci/buildsonarcloud.sh | 4 +-- ci/docker/ubuntu/Dockerfile | 18 +++++++++++++ thirdparty/CMakeLists.txt | 1 - thirdparty/or-tools | 1 - 7 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/docker-ubuntu.yml create mode 100644 ci/docker/ubuntu/Dockerfile delete mode 160000 thirdparty/or-tools 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 d6729fe..ae96a73 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -4,26 +4,20 @@ on: branches: - "**" +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/ubuntu:fix-ci + jobs: static_analysis: runs-on: ubuntu-latest + container: + image: ghcr.io/${{ github.repository }}/ubuntu:fix-ci 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 libabsl-dev libprotobuf-dev libre2-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/CMakeLists.txt b/CMakeLists.txt index cb42eee..3f1dcab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ set(TEMPLATE_DIR ${PROJECT_SOURCE_DIR}/template) find_package(codecov) find_package(PythonInterp REQUIRED) +find_package(ortools CONFIG REQUIRED) find_package(Qt5 COMPONENTS REQUIRED Core diff --git a/ci/buildsonarcloud.sh b/ci/buildsonarcloud.sh index 3a2b175..32c287d 100755 --- a/ci/buildsonarcloud.sh +++ b/ci/buildsonarcloud.sh @@ -34,7 +34,7 @@ cmake "$REPO_ROOT" -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON # 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-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" diff --git a/ci/docker/ubuntu/Dockerfile b/ci/docker/ubuntu/Dockerfile new file mode 100644 index 0000000..e00cce0 --- /dev/null +++ b/ci/docker/ubuntu/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:noble + +RUN apt update && apt install -y qtbase5-dev qt3d5-dev libqt5svg5-dev \ + freeglut3-dev lcov \ + build-essential cmake + +RUN apt update && apt install -y wget unzip git python3-jinja2 + +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-4.6.2.2472-linux.zip +RUN unzip sonar-scanner-cli-4.6.2.2472-linux.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 '*' \ No newline at end of file diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 32f1f0d..c4dea1d 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -4,4 +4,3 @@ add_subdirectory(fmt) add_subdirectory(libdxfrw) add_subdirectory(nanoflann) add_subdirectory(yaml-cpp) -add_subdirectory(or-tools) diff --git a/thirdparty/or-tools b/thirdparty/or-tools deleted file mode 160000 index ed8db90..0000000 --- a/thirdparty/or-tools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ed8db9097cdbaa06b40384d3a86164744a606043 From ca0a2f2ef98e75c55837e33d8fe5ec859041da4b Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 26 Dec 2024 21:03:16 +0100 Subject: [PATCH 23/30] fix: geninfo ignore line mismatch --- cmake/FindLcov.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From bba1046aa39e3e137493652e4cfb2c01db4664e0 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 27 Dec 2024 09:02:08 +0100 Subject: [PATCH 24/30] feat: update sonar scanner --- ci/buildsonarcloud.sh | 2 +- ci/docker/ubuntu/Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/buildsonarcloud.sh b/ci/buildsonarcloud.sh index 32c287d..eac6b26 100755 --- a/ci/buildsonarcloud.sh +++ b/ci/buildsonarcloud.sh @@ -44,6 +44,6 @@ ctest -VV make gcov # Scan project -/opt/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 index e00cce0..5444281 100644 --- a/ci/docker/ubuntu/Dockerfile +++ b/ci/docker/ubuntu/Dockerfile @@ -9,8 +9,8 @@ RUN apt update && apt install -y wget unzip git python3-jinja2 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-4.6.2.2472-linux.zip -RUN unzip sonar-scanner-cli-4.6.2.2472-linux.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 From 279cba87bfb46a02e0991d02a01d53d115eae944 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Jul 2025 10:04:50 +0200 Subject: [PATCH 25/30] fix: use generic docker image name --- .github/workflows/sonarcloud.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index ae96a73..3d9a5f1 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -4,15 +4,11 @@ on: branches: - "**" -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }}/ubuntu:fix-ci - jobs: static_analysis: runs-on: ubuntu-latest container: - image: ghcr.io/${{ github.repository }}/ubuntu:fix-ci + image: ghcr.io/${{ github.repository }}/ubuntu:develop steps: - name: Checkout uses: actions/checkout@v2 From d4ff254e01d2f24609117183206974dcc20079ef Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 25 Jul 2025 16:24:37 +0200 Subject: [PATCH 26/30] feat: update third parties --- CMakeLists.txt | 2 +- src/geometry/filter/assembler.h | 2 +- thirdparty/cavaliercontours | 2 +- thirdparty/cereal | 2 +- thirdparty/nanoflann | 2 +- thirdparty/yaml-cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f1dcab..eaa63fc 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) 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/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 From 4a5deb3e3df4a03775bfac90b5f06250548de12c Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 25 Jul 2025 12:27:18 +0200 Subject: [PATCH 27/30] feat: update to qt6 --- CMakeLists.txt | 20 +++++++------- ci/docker/ubuntu/Dockerfile | 7 ++--- resource/CMakeLists.txt | 2 +- src/exporter/gcode/metadata.h | 2 ++ src/view/simulation/internal/CMakeLists.txt | 1 + src/view/simulation/internal/tool.h | 2 +- src/view/simulation/internal/toolpath.cpp | 28 ++++++++++---------- template/uic/CMakeLists.txt | 2 +- template/uic/dialogs/CMakeLists.txt | 2 +- template/uic/dialogs/settings/CMakeLists.txt | 2 +- template/uic/simulation/CMakeLists.txt | 2 +- test/CMakeLists.txt | 4 +-- 12 files changed, 39 insertions(+), 35 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eaa63fc..9b8a036 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ 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 @@ -54,7 +54,7 @@ find_package(Qt5 COMPONENTS REQUIRED 3DExtras ) -#qt_standard_project_setup() +qt_standard_project_setup() set(INCLUDE_DIRS src @@ -69,10 +69,10 @@ set(INCLUDE_DIRS 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 @@ -95,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/docker/ubuntu/Dockerfile b/ci/docker/ubuntu/Dockerfile index 5444281..900bb5d 100644 --- a/ci/docker/ubuntu/Dockerfile +++ b/ci/docker/ubuntu/Dockerfile @@ -1,8 +1,9 @@ FROM ubuntu:noble -RUN apt update && apt install -y qtbase5-dev qt3d5-dev libqt5svg5-dev \ +RUN apt update && apt install -y \ freeglut3-dev lcov \ - build-essential cmake + build-essential cmake \ + fuse RUN apt update && apt install -y wget unzip git python3-jinja2 @@ -15,4 +16,4 @@ 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 '*' \ No newline at end of file +RUN git config --global --add safe.directory '*' 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/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/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/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/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/test/CMakeLists.txt b/test/CMakeLists.txt index c8eb934..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 ) @@ -40,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) From 94a2766bbfa6daa6e879d76bc75b02c98b46a686 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 25 Jul 2025 17:04:54 +0200 Subject: [PATCH 28/30] feat: update docker for qt6 --- ci/buildsonarcloud.sh | 2 +- ci/docker/ubuntu/Dockerfile | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ci/buildsonarcloud.sh b/ci/buildsonarcloud.sh index eac6b26..a8f9a71 100755 --- a/ci/buildsonarcloud.sh +++ b/ci/buildsonarcloud.sh @@ -30,7 +30,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_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON +cmake "$REPO_ROOT" -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON -DQt6_DIR=/opt/qt/ # Wraps the compilation with the Build Wrapper to generate configuration (used # later by the SonarQube Scanner) into the "bw-output" folder diff --git a/ci/docker/ubuntu/Dockerfile b/ci/docker/ubuntu/Dockerfile index 900bb5d..f03db45 100644 --- a/ci/docker/ubuntu/Dockerfile +++ b/ci/docker/ubuntu/Dockerfile @@ -5,7 +5,10 @@ RUN apt update && apt install -y \ build-essential cmake \ fuse -RUN apt update && apt install -y wget unzip git python3-jinja2 +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 RUN wget http://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip RUN unzip build-wrapper-linux-x86.zip -d /opt From cffe835dd449557e18058c4722cd9426be0da761 Mon Sep 17 00:00:00 2001 From: tristan Date: Fri, 25 Jul 2025 17:26:08 +0200 Subject: [PATCH 29/30] feat: use Qt6_DIR for cmake build --- ci/buildappimage.sh | 2 +- ci/buildsonarcloud.sh | 2 +- ci/docker/ubuntu/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/buildappimage.sh b/ci/buildappimage.sh index 2a7e075..b307a40 100755 --- a/ci/buildappimage.sh +++ b/ci/buildappimage.sh @@ -35,7 +35,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 a8f9a71..f73d439 100755 --- a/ci/buildsonarcloud.sh +++ b/ci/buildsonarcloud.sh @@ -30,7 +30,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_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON -DQt6_DIR=/opt/qt/ +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 diff --git a/ci/docker/ubuntu/Dockerfile b/ci/docker/ubuntu/Dockerfile index f03db45..b1aa707 100644 --- a/ci/docker/ubuntu/Dockerfile +++ b/ci/docker/ubuntu/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:noble -RUN apt update && apt install -y \ +RUN apt update && apt install -y libglib2.0-bin libxkbcommon0 libdbus-1-3 \ freeglut3-dev lcov \ build-essential cmake \ fuse From 0e8cba3446e2c7c006e7b2cc47e6fa3cc853045b Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 24 Jul 2025 16:07:56 +0200 Subject: [PATCH 30/30] fix: appimage build and deploy --- .github/workflows/deploy-linux-appimage.yml | 10 ++++++---- ci/buildappimage.sh | 2 ++ ci/docker/ubuntu/Dockerfile | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-linux-appimage.yml b/.github/workflows/deploy-linux-appimage.yml index 2e581e9..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 libabsl-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/ci/buildappimage.sh b/ci/buildappimage.sh index b307a40..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) diff --git a/ci/docker/ubuntu/Dockerfile b/ci/docker/ubuntu/Dockerfile index b1aa707..41e0224 100644 --- a/ci/docker/ubuntu/Dockerfile +++ b/ci/docker/ubuntu/Dockerfile @@ -1,14 +1,16 @@ FROM ubuntu:noble -RUN apt update && apt install -y libglib2.0-bin libxkbcommon0 libdbus-1-3 \ +RUN apt update && apt install -y libglib2.0-bin libdbus-1-3 libgtk-3-0 \ + libxcb* libxkbcommon* \ freeglut3-dev lcov \ build-essential cmake \ - fuse + 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 +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