diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml new file mode 100644 index 0000000..0050891 --- /dev/null +++ b/.github/workflows/build-wasm.yml @@ -0,0 +1,38 @@ +on: + workflow_dispatch: + push: + branches: + - "**" +env: + QT_VERSION: '6.3.0' + +jobs: + build-wasm: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + submodules: true + - name: Install package + run: | + sudo apt-get update + sudo apt-get -y install freeglut3-dev cmake python3-jinja2 + - name: Install Qt desktop + uses: jurplel/install-qt-action@v3 + with: + aqtversion: '==3.1.*' + version: ${{ env.QT_VERSION }} + host: 'linux' + target: 'desktop' + arch: 'gcc_64' + - name: Install Qt wasm + uses: jurplel/install-qt-action@v3 + with: + aqtversion: '==3.1.*' + version: ${{ env.QT_VERSION }} + host: 'linux' + target: 'desktop' + arch: 'wasm_32' + - name: Build and scan + run: ci/buildwasm.sh diff --git a/.github/workflows/deploy-linux-appimage.yml b/.github/workflows/deploy-linux-appimage.yml index 13324ef..8d4e549 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 qt6-base-dev qt6-3d-dev libqt6svg6-dev freeglut3-dev cmake python3-jinja2 - 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..0eff272 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 qt6-base-dev qt6-3d-dev libqt6svg6-dev freeglut3-dev lcov cmake python3-jinja2 - name: Install build wrapper run: | wget http://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip diff --git a/CMakeLists.txt b/CMakeLists.txt index 62fd227..1ba69f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,21 +34,34 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(TEMPLATE_DIR ${PROJECT_SOURCE_DIR}/template) +option(BUILD_WASM "Build to WebAssembly" OFF) + find_package(codecov) find_package(PythonInterp REQUIRED) -find_package(Qt5 COMPONENTS REQUIRED +if (BUILD_WASM) + message(STATUS "Building to WebAssembly (Qt6_DIR ${Qt6_DIR} QT_HOST_PATH ${QT_HOST_PATH})") + + add_compile_definitions(BUILD_WASM) + + set(WITH_3D OFF) + + # set(QT_HOST_PATH "/home/tristan/Compilation/wasm-test/6.5.0/gcc_64/") + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) + list(APPEND CMAKE_PREFIX_PATH ${Qt6_DIR}) + include(${Qt6_DIR}/lib/cmake/Qt6/QtPublicWasmToolchainHelpers.cmake) +else() + set(WITH_3D ON) +endif() + +find_package(Qt6 COMPONENTS REQUIRED Core Widgets Gui Svg - 3DCore - 3DExtras ) -#qt_standard_project_setup() - set(INCLUDE_DIRS src thirdparty @@ -61,10 +74,8 @@ 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} ) set(LINK_LIBRARIES @@ -74,8 +85,6 @@ set(LINK_LIBRARIES view-dialogs-settings view-task view-view2d - view-simulation - view-simulation-internal model config importer-dxf @@ -87,13 +96,34 @@ set(LINK_LIBRARIES geometry-filter libdxfrw fmt::fmt - Qt5::Widgets - Qt5::Svg - Qt5::3DCore - Qt5::3DExtras + Qt::Core + Qt::Gui + Qt::Svg + Qt::Widgets yaml-cpp ) +if (WITH_3D) + add_compile_definitions(WITH_3D) + + find_package(Qt6 COMPONENTS REQUIRED + 3DCore + 3DExtras + ) + + list(APPEND INCLUDE_DIRS + ${Qt63DCore_INCLUDE_DIRS} + ${Qt63DExtras_INCLUDE_DIRS} + ) + + list(APPEND LINK_LIBRARIES + view-simulation + view-simulation-internal + Qt::3DCore + Qt::3DExtras + ) +endif() + include_directories(${INCLUDE_DIRS}) add_subdirectory(template) @@ -105,8 +135,13 @@ if (BUILD_TESTING) add_subdirectory(test) endif() -add_executable(dxfplotter src/main.cpp) -target_link_libraries(dxfplotter ${LINK_LIBRARIES}) +qt_add_executable(dxfplotter src/main.cpp) +target_link_libraries(dxfplotter PRIVATE ${LINK_LIBRARIES}) + +if (BUILD_WASM) + target_link_options(dxfplotter PRIVATE -lidbfs.js) +endif() + add_coverage(dxfplotter) install(TARGETS dxfplotter DESTINATION bin) diff --git a/ci/buildappimage.sh b/ci/buildappimage.sh index 2a7e075..1f9b6a1 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_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_TESTING=OFF # build project and install files into AppDir make -j$(nproc) diff --git a/ci/buildwasm.sh b/ci/buildwasm.sh new file mode 100755 index 0000000..ad7e5b4 --- /dev/null +++ b/ci/buildwasm.sh @@ -0,0 +1,39 @@ +#! /bin/bash + +set -x +set -e + +# building in temporary directory to keep system clean +# use RAM disk if possible (as in: not building on CI system like Travis, and RAM disk is available) +if [ "$CI" == "" ] && [ -d /dev/shm ]; then + TEMP_BASE=/dev/shm +else + TEMP_BASE=/tmp +fi + +BUILD_DIR=$(mktemp -d -p "$TEMP_BASE" analyse-sonarcloud-XXXXXX) + +# make sure to clean up build dir, even if errors occur +cleanup () { + if [ -d "$BUILD_DIR" ]; then + rm -rf "$BUILD_DIR" + fi +} +trap cleanup EXIT + +# store repo root as variable +REPO_ROOT=$(readlink -f $(dirname $(dirname $0))) +OLD_CWD=$(readlink -f .) + +# switch to build dir +pushd "$BUILD_DIR" + +QT6_DIR="${REPO_ROOT}/${QT_VERSION}/" +QT6_DIR_DESKTOP="${QT6_DIR}/gcc_64/" +QT6_DIR_WASM="${QT6_DIR}/wasm_32/" + +# configure build files with CMake +cmake "$REPO_ROOT" -DCMAKE_BUILD_TYPE=Release -DBUILD_WASM=ON -DQt6_DIR=${QT6_DIR_WASM} -DQT_HOST_PATH=${QT6_DIR_DESKTOP} +cmake --build . + + diff --git a/ci/deploywindows.sh b/ci/deploywindows.sh old mode 100644 new mode 100755 diff --git a/resource/CMakeLists.txt b/resource/CMakeLists.txt index 70b1618..c4eb371 100644 --- a/resource/CMakeLists.txt +++ b/resource/CMakeLists.txt @@ -1,4 +1,4 @@ -qt5_add_resources(SOURCES resource.qrc) +qt_add_resources(SOURCES resource.qrc) add_library(resource ${SOURCES}) diff --git a/resource/icons/layer-bottom.svg b/resource/icons/layer-bottom.svg new file mode 100644 index 0000000..709d1cc --- /dev/null +++ b/resource/icons/layer-bottom.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/resource/icons/layer-top.svg b/resource/icons/layer-top.svg new file mode 100644 index 0000000..bb9952d --- /dev/null +++ b/resource/icons/layer-top.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/resource/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/common/copy.h b/src/common/copy.h new file mode 100644 index 0000000..3c8dd67 --- /dev/null +++ b/src/common/copy.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +namespace common +{ + +template > +std::vector deepcopy(const std::vector &other) +{ + std::vector duplicated(other.size()); + std::transform(other.begin(), other.end(), duplicated.begin(), [](const ItemUPtr &item){ + return std::make_unique(*item); + }); + + return duplicated; +} + +} diff --git a/src/config/config.cpp b/src/config/config.cpp index 35ca952..22b6b63 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1,21 +1,27 @@ +#include "yaml-cpp/exceptions.h" #include #include #include +#ifdef BUILD_WASM +# include +#endif + namespace config { Config::Config(const std::string &filePath) :m_filePath(filePath) { - try { - m_yamlRoot = YAML::LoadFile(filePath); + /*try { + //m_yamlRoot = YAML::LoadFile(filePath); + throw YAML::BadFile(filePath); } catch (const YAML::BadFile&) { qInfo() << "Initializing configuration from defaults"; - } + }*/ // Instantiation of root group m_root = Root(m_yamlRoot); diff --git a/src/exporter/gcode/metadata.h b/src/exporter/gcode/metadata.h index 10ff84a..6bc90a4 100644 --- a/src/exporter/gcode/metadata.h +++ b/src/exporter/gcode/metadata.h @@ -12,7 +12,8 @@ class Document; } -class QJsonObject; +class QJsonArray; +class QJsonDocument; namespace exporter::gcode { 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/importer/dxf/importer.cpp b/src/importer/dxf/importer.cpp index c2ee24d..838493b 100644 --- a/src/importer/dxf/importer.cpp +++ b/src/importer/dxf/importer.cpp @@ -16,16 +16,18 @@ void Importer::addLayer(const DRW_Layer &layer) } } -Importer::Importer(const std::string& filename, float splineToArcPrecision, float minimumSplineLength, float minimumArcLength) +Importer::Importer(float splineToArcPrecision, float minimumSplineLength, float minimumArcLength) :m_entityImporterSettings({splineToArcPrecision, minimumSplineLength, minimumArcLength}), m_ignoreEntities(false) +{ +} + +bool Importer::import(std::istream& stream) { Interface interface(*this); - dxfRW rw(filename.c_str()); - if (!rw.read(&interface, false)) { - throw common::FileCouldNotOpenException(); - } + dxfRW rw(""); + return rw.read(stream, &interface, false); } Layer::List Importer::layers() const diff --git a/src/importer/dxf/importer.h b/src/importer/dxf/importer.h index e73f466..daa5622 100644 --- a/src/importer/dxf/importer.h +++ b/src/importer/dxf/importer.h @@ -25,7 +25,9 @@ class Importer void addLayer(const DRW_Layer &layer); public: - explicit Importer(const std::string &filename, float splineToArcPrecision, float minimumSplineLength, float minimumArcLength); + explicit Importer(float splineToArcPrecision, float minimumSplineLength, float minimumArcLength); + + bool import(std::istream &stream); Layer::List layers() const; diff --git a/src/main.cpp b/src/main.cpp index b72ce46..e241947 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,21 @@ #include #include +#include + +#include + +#ifdef BUILD_WASM + +Q_IMPORT_PLUGIN(QWasmIntegrationPlugin) +Q_IMPORT_PLUGIN(QSvgIconPlugin) +Q_IMPORT_PLUGIN(QGifPlugin) +Q_IMPORT_PLUGIN(QICOPlugin) +Q_IMPORT_PLUGIN(QJpegPlugin) +Q_IMPORT_PLUGIN(QSvgPlugin) + +#endif + void setDarkPalette(QApplication &qapp) { QPalette palette; @@ -32,6 +47,8 @@ void setDarkPalette(QApplication &qapp) int main(int argc, char *argv[]) { + qputenv("QT3D_RENDERER", "opengl"); + Q_INIT_RESOURCE(resource); QApplication qapp(argc, argv); @@ -71,5 +88,7 @@ int main(int argc, char *argv[]) const QString fileName = parser.positionalArguments().value(0, ""); app.loadFileFromCmd(fileName); - qapp.exec(); + window.show(); + + return qapp.exec(); } 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..e29ba1a 100644 --- a/src/model/application.cpp +++ b/src/model/application.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -20,26 +19,78 @@ #include #include +#ifdef BUILD_WASM +# include +#endif + namespace model { static const QString configFileName = "config.yml"; +static QDir configFileDir() +{ + return QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); +} + +static void ensureDirExists(const QDir &directory) +{ + directory.mkpath("."); + +#ifdef BUILD_WASM + const char *dirPath = directory.absolutePath().toUtf8().constData(); + + EM_ASM({ + let directory = UTF8ToString($0); + console.log("Setup config directory to: " + directory); + + //let analyse = FS.analyzePath(directory); + //if (!analyse.exists) { + // Make a directory other than '/' + // FS.mkdir(directory); + //} + + // Then mount with IDBFS type + FS.mount(IDBFS, {}, directory); + + // Then sync + FS.syncfs(true, function (err) { + // Error + }); + }, dirPath); +#endif +} + /** Retrieves application config file path * @return config file path */ static std::string configFilePath() { - const QDir dir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + const QDir directory = configFileDir(); // Ensure the path exists - dir.mkpath("."); + ensureDirExists(directory); - const QString path = dir.filePath(configFileName); + const QString path = directory.filePath(configFileName); return path.toStdString(); } +void Application::setOpenedDocument(Document::UPtr &&document) +{ + m_openedDocument = std::move(document); + m_documentHistory = std::make_unique(*m_openedDocument); + + emit newDocumentOpened(m_openedDocument.get()); +} + +void Application::setRestoredDocument(const Document &documentVersion) +{ + m_openedDocument = std::make_unique(documentVersion); + emit documentRestoredFromHistory(m_openedDocument.get()); +} + + QString Application::baseName(const QString& fileName) { const QFileInfo fileInfo(fileName); @@ -100,10 +151,6 @@ geometry::Polyline::List Application::postProcessImportedPolylines(geometry::Pol // Remove small bulges geometry::filter::Cleaner cleaner(assembler.polylines(), dxf.minimumPolylineLength(), dxf.minimumArcLength()); - if (dxf.sortPathByLength()) { - geometry::filter::Sorter sorter(cleaner.polylines()); - return sorter.polylines(); - } return cleaner.polylines(); } @@ -120,7 +167,30 @@ 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; +} + +std::optional Application::findFileType(const QString &fileName) const +{ + const QMimeDatabase db; + const QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); + const QString mineName = mime.name(); + + if (mineName == "image/vnd.dxf") { + return SupportFileType::Dxf; + } + else if (mineName == "application/octet-stream") { + return SupportFileType::Dxfplot; + } + + return std::nullopt; } Application::Application() @@ -215,25 +285,61 @@ void Application::loadFileFromCmd(const QString &fileName) bool Application::loadFile(const QString &fileName) { - qInfo() << "Opening " << fileName; + std::optional fileTypeOpt = findFileType(fileName); + if (!fileTypeOpt) { + qCritical() << "Unsupported file type: " << fileName; + return false; + } - const QMimeDatabase db; - const QMimeType mime = db.mimeTypeForFile(fileName); - const QString mineName = mime.name(); + QFile file(fileName); + file.open(QIODeviceBase::ReadOnly); + const QByteArray fileContent(file.readAll()); + file.close(); - if (mineName == "image/vnd.dxf") { - if (!loadFromDxf(fileName)) { - return false; - } - } - else if (mineName == "text/plain") { - loadFromDxfplot(fileName); + if (fileContent.isEmpty()) { + qCritical() << "Failed opening " << fileName; + return false; } - else { - qCritical() << "Invalid file type: " << fileName; + + return loadFile(fileName, *fileTypeOpt, fileContent); +} + +bool Application::loadFile(const QString &fileName, const QByteArray &fileContent) +{ + std::optional fileTypeOpt = findFileType(fileName); + if (!fileTypeOpt) { + qCritical() << "Unsupported file type: " << fileName; return false; } + return loadFile(fileName, *fileTypeOpt, fileContent); +} + +bool Application::loadFile(const QString &fileName, SupportFileType fileType, const QByteArray &fileContent) +{ + qInfo() << "Opening " << fileName; + + std::istringstream streamFileContent(fileContent.toStdString()); + + switch (fileType) { + case SupportFileType::Dxf: + { + if (!loadFromDxf(streamFileContent)) { + return false; + } + break; + } + case SupportFileType::Dxfplot: + { + if (!loadFromDxfplot(fileName)) { + return false; + } + break; + } + default: + break; + } + m_lastHandledFileBaseName = baseName(fileName); resetLastSavedFileNames(); @@ -245,21 +351,17 @@ bool Application::loadFile(const QString &fileName) return true; } -bool Application::loadFromDxf(const QString &fileName) +bool Application::loadFromDxf(std::istream &fileContent) { const config::Import::Dxf &dxf = m_config.root().import().dxf(); - try { - importer::dxf::Importer importer(fileName.toStdString(), dxf.splineToArcPrecision(), dxf.minimumSplineLength(), dxf.minimumArcLength()); - - m_openedDocument = std::make_unique(createTaskFromDxfImporter(importer), *m_defaultToolConfig, *m_defaultProfileConfig); - } - catch (const common::FileCouldNotOpenException&) { - qCritical() << "File not found:" << fileName; + importer::dxf::Importer importer(dxf.splineToArcPrecision(), dxf.minimumSplineLength(), dxf.minimumArcLength()); + if (!importer.import(fileContent)) { + qCritical() << "Failed to import dxf file"; return false; } - emit documentChanged(m_openedDocument.get()); + setOpenedDocument(std::make_unique(createTaskFromDxfImporter(importer), *m_defaultToolConfig, *m_defaultProfileConfig)); return true; } @@ -269,26 +371,24 @@ 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; } +constexpr exporter::gcode::Exporter::Options gcodeExportOptions = static_cast( + exporter::gcode::Exporter::ExportConfig | + exporter::gcode::Exporter::ExportMetadata +); + bool Application::saveToGcode(const QString &fileName) { try { - const exporter::gcode::Exporter::Options options = static_cast( - exporter::gcode::Exporter::ExportConfig | - exporter::gcode::Exporter::ExportMetadata - ); - - exporter::gcode::Exporter exporter(m_openedDocument->toolConfig(), m_openedDocument->profileConfig(),options); + exporter::gcode::Exporter exporter(m_openedDocument->toolConfig(), m_openedDocument->profileConfig(), gcodeExportOptions); const bool saved = saveToFile(exporter, fileName); if (saved) { m_lastSavedGcodeFileName = fileName; @@ -302,6 +402,13 @@ bool Application::saveToGcode(const QString &fileName) return false; } +QByteArray Application::saveToGcode() +{ + exporter::gcode::Exporter exporter(m_openedDocument->toolConfig(), m_openedDocument->profileConfig(), gcodeExportOptions); + + return saveToBuffer(exporter); +} + bool Application::saveToDxfplot(const QString &fileName) { exporter::dxfplot::Exporter exporter; @@ -314,20 +421,33 @@ bool Application::saveToDxfplot(const QString &fileName) return saved; } +QByteArray Application::saveToDxfplot() +{ + exporter::dxfplot::Exporter exporter; + + return saveToBuffer(exporter); +} + 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 +457,8 @@ void Application::pocketSelection() Task &task = m_openedDocument->task(); task.pocketSelection(radius, dxf.minimumPolylineLength(), dxf.minimumArcLength()); + + takeDocumentSnapshot(); } geometry::Rect Application::selectionBoundingRect() const @@ -349,18 +471,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 +497,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.cpp.orig b/src/model/application.cpp.orig new file mode 100644 index 0000000..906b613 --- /dev/null +++ b/src/model/application.cpp.orig @@ -0,0 +1,516 @@ +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#ifdef BUILD_WASM +# include +#endif + +namespace model +{ + +static const QString configFileName = "config.yml"; + +static QDir configFileDir() +{ + return QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); +} + +static void ensureDirExists(const QDir &directory) +{ + directory.mkpath("."); + +#ifdef BUILD_WASM + const char *dirPath = directory.absolutePath().toUtf8().constData(); + + EM_ASM({ + let directory = UTF8ToString($0); + console.log("Setup config directory to: " + directory); + + //let analyse = FS.analyzePath(directory); + //if (!analyse.exists) { + // Make a directory other than '/' + // FS.mkdir(directory); + //} + + // Then mount with IDBFS type + FS.mount(IDBFS, {}, directory); + + // Then sync + FS.syncfs(true, function (err) { + // Error + }); + }, dirPath); +#endif +} + +/** Retrieves application config file path + * @return config file path + */ +static std::string configFilePath() +{ + const QDir directory = configFileDir(); + + // Ensure the path exists + ensureDirExists(directory); + + const QString path = directory.filePath(configFileName); + + 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); + return fileInfo.absoluteDir().filePath(fileInfo.baseName()); +} + +void Application::resetLastSavedFileNames() +{ + m_lastSavedDxfplotFileName.clear(); + m_lastSavedGcodeFileName.clear(); +} + +PathSettings Application::defaultPathSettings() const +{ + const config::Profiles::Profile::DefaultPath &defaultPath = m_defaultProfileConfig->defaultPath(); + return PathSettings(defaultPath.planeFeedRate(), defaultPath.depthFeedRate(), defaultPath.intensity(), defaultPath.depth()); +} + +const config::Tools::Tool *Application::findTool(const std::string &name) const +{ + const config::Tools &tools = m_config.root().tools(); + if (tools.has(name)) { + return &tools[name]; + } + + return nullptr; +} + +const config::Profiles::Profile *Application::findProfile(const std::string &name) const +{ + const config::Profiles &profiles = m_config.root().profiles(); + if (profiles.has(name)) { + return &profiles[name]; + } + + return nullptr; +} + +void Application::cutterCompensation(float scale) +{ + const config::Import::Dxf &dxf = m_config.root().import().dxf(); + + const float radius = m_openedDocument->toolConfig().general().radius(); + const float scaledRadius = radius * scale; + + Task &task = m_openedDocument->task(); + task.cutterCompensationSelection(scaledRadius, dxf.minimumPolylineLength(), dxf.minimumArcLength()); +} + +geometry::Polyline::List Application::postProcessImportedPolylines(geometry::Polyline::List &&rawPolylines) const +{ + const config::Import::Dxf &dxf = m_config.root().import().dxf(); + + geometry::filter::RemoveExactDuplicate removeExactDuplicate(std::move(rawPolylines)); + + // Merge polylines to create longest contours + geometry::filter::Assembler assembler(removeExactDuplicate.polylines(), dxf.assembleTolerance()); + // Remove small bulges + geometry::filter::Cleaner cleaner(assembler.polylines(), dxf.minimumPolylineLength(), dxf.minimumArcLength()); + + return cleaner.polylines(); +} + +Task::UPtr Application::createTaskFromDxfImporter(const importer::dxf::Importer& importer) +{ + Layer::ListUPtr layers; + for (importer::dxf::Layer &importerLayer : importer.layers()) { + const std::string &layerName = importerLayer.name(); + geometry::Polyline::List polylines = postProcessImportedPolylines(importerLayer.polylines()); + + // Create paths from merged and cleaned polylines of one layer + Path::ListUPtr children = Path::FromPolylines(std::move(polylines), defaultPathSettings(), layerName); + + layers.emplace_back(std::make_unique(layerName, std::move(children))); + } + + 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; +} + +std::optional Application::findFileType(const QString &fileName) const +{ + const QMimeDatabase db; + const QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); + const QString mineName = mime.name(); + + if (mineName == "image/vnd.dxf") { + return SupportFileType::Dxf; + } + else if (mineName == "application/octet-stream") { + return SupportFileType::Dxfplot; + } + + return std::nullopt; +} + +Application::Application() + :m_config(configFilePath()), + // Default select first tool + m_defaultToolConfig(&m_config.root().tools().first()), + // Default select first profile + m_defaultProfileConfig(&m_config.root().profiles().first()) +{ +} + +config::Config &Application::config() +{ + return m_config; +} + +void Application::setConfig(config::Config &&config) +{ + m_config = std::move(config); + emit configChanged(m_config); +} + +bool Application::selectTool(const QString &toolName) +{ + const std::string name = toolName.toStdString(); + const config::Tools::Tool *tool = findTool(name); + + if (tool) { + if (m_openedDocument) { + m_openedDocument->setToolConfig(*tool); + } + m_defaultToolConfig = tool; + + return true; + } + + return false; +} + +void Application::defaultToolFromCmd(const QString &toolName) +{ + if (!selectTool(toolName)) { + qCritical() << "Invalid tool name " << toolName; + } +} + +bool Application::selectProfile(const QString &profileName) +{ + const std::string name = profileName.toStdString(); + const config::Profiles::Profile *profile = findProfile(name); + + if (profile) { + if (m_openedDocument) { + m_openedDocument->setProfileConfig(*profile); + } + m_defaultProfileConfig = profile; + + return true; + } + + return false; +} + +void Application::defaultProfileFromCmd(const QString &profileName) +{ + if (!selectProfile(profileName)) { + qCritical() << "Invalid profile name " << profileName; + } +} + +const QString &Application::lastHandledFileBaseName() const +{ + return m_lastHandledFileBaseName; +} + +const QString &Application::lastSavedDxfplotFileName() const +{ + return m_lastSavedDxfplotFileName; +} + +const QString &Application::lastSavedGcodeFileName() const +{ + return m_lastSavedGcodeFileName; +} + +void Application::loadFileFromCmd(const QString &fileName) +{ + if (!fileName.isEmpty()) { + loadFile(fileName); + } +} + +bool Application::loadFile(const QString &fileName) +{ + std::optional fileTypeOpt = findFileType(fileName); + if (!fileTypeOpt) { + qCritical() << "Unsupported file type: " << fileName; + return false; + } + + QFile file(fileName); + file.open(QIODeviceBase::ReadOnly); + const QByteArray fileContent(file.readAll()); + file.close(); + + if (fileContent.isEmpty()) { + qCritical() << "Failed opening " << fileName; + return false; + } + + return loadFile(fileName, *fileTypeOpt, fileContent); +} + +bool Application::loadFile(const QString &fileName, const QByteArray &fileContent) +{ + std::optional fileTypeOpt = findFileType(fileName); + if (!fileTypeOpt) { + qCritical() << "Unsupported file type: " << fileName; + return false; + } + + return loadFile(fileName, *fileTypeOpt, fileContent); +} + +bool Application::loadFile(const QString &fileName, SupportFileType fileType, const QByteArray &fileContent) +{ + qInfo() << "Opening " << fileName; + + std::istringstream streamFileContent(fileContent.toStdString()); + + switch (fileType) { + case SupportFileType::Dxf: + { + if (!loadFromDxf(streamFileContent)) { + return false; + } + break; + } + case SupportFileType::Dxfplot: + { + if (!loadFromDxfplot(fileName)) { + return false; + } + break; + } + default: + break; + } + + m_lastHandledFileBaseName = baseName(fileName); + resetLastSavedFileNames(); + + // Update window title based on file name. + const QFileInfo fileInfo(fileName); + const QString title = fileInfo.fileName(); + emit titleChanged(title); + + return true; +} + +bool Application::loadFromDxf(std::istream &fileContent) +{ + const config::Import::Dxf &dxf = m_config.root().import().dxf(); + +<<<<<<< HEAD + try { + importer::dxf::Importer importer(fileName.toStdString(), dxf.splineToArcPrecision(), dxf.minimumSplineLength(), dxf.minimumArcLength()); + + setOpenedDocument(std::make_unique(createTaskFromDxfImporter(importer), *m_defaultToolConfig, *m_defaultProfileConfig)); + } + catch (const common::FileCouldNotOpenException&) { + qCritical() << "File not found:" << fileName; + return false; + } + +======= + importer::dxf::Importer importer(dxf.splineToArcPrecision(), dxf.minimumSplineLength(), dxf.minimumArcLength()); + if (!importer.import(fileContent)) { + qCritical() << "Failed to import dxf file"; + return false; + } + + m_openedDocument = std::make_unique(createTaskFromDxfImporter(importer), *m_defaultToolConfig, *m_defaultProfileConfig); + + emit documentChanged(m_openedDocument.get()); + +>>>>>>> d6f2ed7 (chore: Use qt async open file function to open dxf & dxfplot) + return true; +} + +bool Application::loadFromDxfplot(const QString &fileName) +{ + try { + importer::dxfplot::Importer importer(m_config.root().tools(), m_config.root().profiles()); + + setOpenedDocument(importer(fileName.toStdString())); + } + catch (const common::FileCouldNotOpenException&) { + return false; + } + + return true; +} + +bool Application::saveToGcode(const QString &fileName) +{ + try { + const exporter::gcode::Exporter::Options options = static_cast( + exporter::gcode::Exporter::ExportConfig | + exporter::gcode::Exporter::ExportMetadata + ); + + exporter::gcode::Exporter exporter(m_openedDocument->toolConfig(), m_openedDocument->profileConfig(),options); + const bool saved = saveToFile(exporter, fileName); + if (saved) { + m_lastSavedGcodeFileName = fileName; + } + return saved; + } + catch (const std::exception &exception) { + emit errorRaised(exception.what()); + } + + return false; +} + +bool Application::saveToDxfplot(const QString &fileName) +{ + exporter::dxfplot::Exporter exporter; + + const bool saved = saveToFile(exporter, fileName); + if (saved) { + m_lastSavedDxfplotFileName = fileName; + } + + return saved; +} + +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() +{ + const config::Import::Dxf &dxf = m_config.root().import().dxf(); + const float radius = m_openedDocument->toolConfig().general().radius(); + + Task &task = m_openedDocument->task(); + task.pocketSelection(radius, dxf.minimumPolylineLength(), dxf.minimumArcLength()); + + takeDocumentSnapshot(); +} + +geometry::Rect Application::selectionBoundingRect() const +{ + Task &task = m_openedDocument->task(); + return task.selectionBoundingRect(); +} + +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() +{ + const float fastMoveFeedRate = m_config.root().simulation().fastMoveFeedRate(); + 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..22757da 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(); @@ -64,6 +69,23 @@ class Application : public QObject return false; } + template + QByteArray saveToBuffer(Exporter &&exporter) + { + std::stringstream outputStream; + exporter(*m_openedDocument, outputStream); + const std::string output = outputStream.str(); + + return QByteArray(output.data(), output.size()); + } + + enum class SupportFileType { + Dxf, + Dxfplot + }; + + std::optional findFileType(const QString &fileName) const; + public: struct FileExtension { inline static const QString Gcode = ".ngc"; @@ -88,11 +110,15 @@ class Application : public QObject const QString &lastSavedGcodeFileName() const; void loadFileFromCmd(const QString &fileName); bool loadFile(const QString &fileName); - bool loadFromDxf(const QString &fileName); + bool loadFile(const QString &fileName, const QByteArray &fileContent); + bool loadFile(const QString &fileName, SupportFileType fileType, const QByteArray &fileContent); + bool loadFromDxf(std::istream &fileContent); bool loadFromDxfplot(const QString &fileName); bool saveToGcode(const QString &fileName); + QByteArray saveToGcode(); bool saveToDxfplot(const QString &fileName); + QByteArray saveToDxfplot(); void leftCutterCompensation(); void rightCutterCompensation(); @@ -107,8 +133,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..92ded7e 100644 --- a/src/model/document.cpp +++ b/src/model/document.cpp @@ -8,7 +8,20 @@ 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()) +{ +} + +Document &Document::operator=(const Document &other) +{ + m_task = std::make_unique(other.task()); + m_toolConfig = &other.toolConfig(); + m_profileConfig = &other.profileConfig(); } Task &Document::task() @@ -34,13 +47,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..0cb182f 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,10 @@ 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; + Document(const Document &other); + + Document &operator=(Document &&other) = default; + Document &operator=(const Document &other); Task& task(); const Task& task() const; @@ -26,10 +28,6 @@ class Document : public QObject, public common::Aggregable const config::Profiles::Profile &profileConfig() const; void setToolConfig(const config::Tools::Tool &tool); void setProfileConfig(const config::Profiles::Profile &profile); - -Q_SIGNALS: - void toolConfigChanged(const config::Tools::Tool &tool); - void profileConfigChanged(const config::Profiles::Profile &profile); }; } diff --git a/src/model/documenthistory.cpp b/src/model/documenthistory.cpp new file mode 100644 index 0000000..9954724 --- /dev/null +++ b/src/model/documenthistory.cpp @@ -0,0 +1,55 @@ +#include + +#include + +namespace model +{ + +bool DocumentHistory::isCurrentDocumentLastOfHistory() const +{ + return m_currentDocumentIt == (m_documentHistory.end() - 1); +} + +bool DocumentHistory::isCurrentDocumentFirstOfHistory() const +{ + return m_currentDocumentIt == m_documentHistory.begin(); +} + +DocumentHistory::DocumentHistory(const Document& initialDocument) + :m_currentDocumentIt(m_documentHistory.insert(m_documentHistory.end(), initialDocument)) +{ +} + +void DocumentHistory::takeSnapshot(const Document& currentDocument) +{ + if (!isCurrentDocumentLastOfHistory()) { + m_documentHistory.erase(m_currentDocumentIt + 1, m_documentHistory.end()); + } + + constexpr int MaximumSnapshots = 100; + if (m_documentHistory.size() == MaximumSnapshots) { + m_documentHistory.erase(m_documentHistory.begin()); + } + + m_currentDocumentIt = m_documentHistory.insert(m_documentHistory.end(), currentDocument); +} + +const Document &DocumentHistory::undo() +{ + if (!isCurrentDocumentFirstOfHistory()) { + --m_currentDocumentIt; + } + + return *m_currentDocumentIt; +} + +const Document &DocumentHistory::redo() +{ + if (!isCurrentDocumentLastOfHistory()) { + ++m_currentDocumentIt; + } + + return *m_currentDocumentIt; +} + +} diff --git a/src/model/documenthistory.h b/src/model/documenthistory.h new file mode 100644 index 0000000..7931029 --- /dev/null +++ b/src/model/documenthistory.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace model +{ + +class Document; + +class DocumentHistory : public common::Aggregable +{ +private: + Document::List m_documentHistory; + Document::List::iterator m_currentDocumentIt; + + bool isCurrentDocumentLastOfHistory() const; + bool isCurrentDocumentFirstOfHistory() const; + +public: + explicit DocumentHistory(const Document& initialDocument); + + void takeSnapshot(const Document& currentDocument); + const Document &undo(); + const Document &redo(); +}; + +} diff --git a/src/model/documentmodelobserver.h b/src/model/documentmodelobserver.h index 4287b82..74e48a7 100644 --- a/src/model/documentmodelobserver.h +++ b/src/model/documentmodelobserver.h @@ -26,6 +26,16 @@ class DocumentModelObserver : public QtBaseObject */ virtual void documentChanged() = 0; + /// Notify the document was changed after an undo or redo operation + virtual void documentRestoredFromHistory() + { + } + + /// Notify the document was changed after an opening operation + virtual void newDocumentOpened() + { + } + protected: Document *document() const { @@ -46,11 +56,24 @@ private Q_SLOTS: documentChanged(); } + void internalDocumentRestoredFromHistory(Document *newDocument) + { + internalDocumentChanged(newDocument); + documentRestoredFromHistory(); + } + + void internalNewDocumentOpened(Document *newDocument) + { + internalDocumentChanged(newDocument); + newDocumentOpened(); + } + public: explicit DocumentModelObserver(Application &app) :m_document(nullptr) { - QObject::connect(&app, &Application::documentChanged, this, &DocumentModelObserver::internalDocumentChanged); + QObject::connect(&app, &Application::documentRestoredFromHistory, this, &DocumentModelObserver::internalDocumentRestoredFromHistory); + QObject::connect(&app, &Application::newDocumentOpened, this, &DocumentModelObserver::internalNewDocumentOpened); } }; diff --git a/src/model/layer.cpp b/src/model/layer.cpp index 88082a0..0a8531c 100644 --- a/src/model/layer.cpp +++ b/src/model/layer.cpp @@ -1,5 +1,7 @@ #include +#include + namespace model { @@ -17,6 +19,15 @@ Layer::Layer(const std::string &name, Path::ListUPtr &&children) assignSelfToChildren(); } +Layer::Layer(const Layer& other) + :Renderable(other), + m_children(common::deepcopy(other.m_children)) +{ + for (Path::UPtr &child : m_children) { + child->setLayer(*this); + } +} + int Layer::childrenCount() const { return m_children.size(); diff --git a/src/model/layer.h b/src/model/layer.h index 1e7c5e5..1cd2ef7 100644 --- a/src/model/layer.h +++ b/src/model/layer.h @@ -21,6 +21,7 @@ class Layer : public Renderable, public common::Aggregable public: explicit Layer(const std::string &name, Path::ListUPtr &&children); explicit Layer() = default; + explicit Layer(const Layer& other);; int childrenCount() const; Path& childrenAt(int index); diff --git a/src/model/offsettedpath.cpp b/src/model/offsettedpath.cpp index 25294e7..d2465f3 100644 --- a/src/model/offsettedpath.cpp +++ b/src/model/offsettedpath.cpp @@ -9,6 +9,13 @@ OffsettedPath::OffsettedPath(geometry::Polyline::List &&offsettedPolylines, Dire { } +OffsettedPath::OffsettedPath(const OffsettedPath& other) + :QObject(), + m_polylines(other.m_polylines), + m_direction(other.m_direction) +{ +} + const geometry::Polyline::List &OffsettedPath::polylines() const { return m_polylines; diff --git a/src/model/offsettedpath.h b/src/model/offsettedpath.h index 8c623e6..55e015d 100644 --- a/src/model/offsettedpath.h +++ b/src/model/offsettedpath.h @@ -33,6 +33,7 @@ class OffsettedPath : public QObject public: explicit OffsettedPath(geometry::Polyline::List &&offsettedPolylines, Direction direction); + explicit OffsettedPath(const OffsettedPath& other); explicit OffsettedPath() = default; const geometry::Polyline::List &polylines() const; diff --git a/src/model/path.cpp b/src/model/path.cpp index 89b5d53..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 b2ea3d1..f163b7e 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(); @@ -72,6 +91,58 @@ void Task::movePath(int index, MoveDirection direction) } } +void Task::movePathToTip(int index, MoveTip tip) +{ + assert(0 <= index && index < pathCount()); + + Path *path = m_stack[index]; + m_stack.erase(m_stack.begin() + index); + + switch (tip) { + case MoveTip::Bottom: + { + m_stack.push_back(path); + break; + } + case MoveTip::Top: + { + m_stack.insert(m_stack.begin(), path); + break; + } + } +} + +void Task::sortPathsByLength() +{ + struct PathLength + { + Path *path; + float length; + + PathLength() = default; + + explicit PathLength(Path *path) + :path(path), + length(path->basePolyline().length()) + { + } + + bool operator<(const PathLength& other) const + { + return length < other.length; + } + }; + + std::vector pathsLength(m_paths.size()); + std::transform(m_paths.begin(), m_paths.end(), pathsLength.begin(), + [](Path *path){ return PathLength(path); }); + + std::sort(pathsLength.begin(), pathsLength.end()); + + std::transform(pathsLength.begin(), pathsLength.end(), m_stack.begin(), + [](PathLength& pathLength){ return pathLength.path; }); +} + void Task::resetCutterCompensationSelection() { forEachSelectedPath([](model::Path &path){ path.resetOffset(); }); diff --git a/src/model/task.h b/src/model/task.h index 770ef3d..eb65b21 100644 --- a/src/model/task.h +++ b/src/model/task.h @@ -30,8 +30,15 @@ class Task : public QObject, public common::Aggregable DOWN = 1 }; + enum class MoveTip + { + Top, + Bottom + }; + explicit Task() = default; explicit Task(Layer::ListUPtr &&layers); + explicit Task(const Task &other); int pathCount() const; const Path &pathAt(int index) const; @@ -39,6 +46,9 @@ class Task : public QObject, public common::Aggregable int pathIndexFor(const Path &path) const; void movePath(int index, MoveDirection direction); + void movePathToTip(int index, MoveTip tip); + + void sortPathsByLength(); template void forEachPathInStack(Functor &&functor) const diff --git a/src/view/CMakeLists.txt b/src/view/CMakeLists.txt index 33184ab..387b579 100644 --- a/src/view/CMakeLists.txt +++ b/src/view/CMakeLists.txt @@ -1,7 +1,10 @@ add_subdirectory(dialogs) add_subdirectory(task) add_subdirectory(view2d) -add_subdirectory(simulation) + +if (WITH_3D) + add_subdirectory(simulation) +endif() set(SRC info.cpp diff --git a/src/view/dialogs/settings/settings.cpp b/src/view/dialogs/settings/settings.cpp index a6da204..6dd22a3 100644 --- a/src/view/dialogs/settings/settings.cpp +++ b/src/view/dialogs/settings/settings.cpp @@ -62,13 +62,11 @@ void Settings::currentChanged(const QModelIndex ¤t, const QModelIndex &) NodeVisitor visitor; m_model->visit(current, visitor); - // Replace old center widget + // Replace old properties widget QWidget *newWidget = visitor.newWidget(); - QLayoutItem *item = gridLayout->replaceWidget(center, newWidget); - center = newWidget; - - // Delete old center widget - delete item->widget(); + QWidget *oldWidget = center->findChild(QString(), Qt::FindDirectChildrenOnly); + center->layout()->replaceWidget(oldWidget, newWidget); + delete oldWidget; const bool isList = m_model->isList(current); addButton->disconnect(); diff --git a/src/view/mainwindow.cpp b/src/view/mainwindow.cpp index 80846d0..85535eb 100644 --- a/src/view/mainwindow.cpp +++ b/src/view/mainwindow.cpp @@ -4,7 +4,9 @@ #include #include #include -#include +#ifdef WITH_3D +# include +#endif #include #include #include @@ -37,12 +39,16 @@ QWidget *MainWindow::setupLeftPanel() QWidget *MainWindow::setupCenterPanel() { view2d::Viewport *viewport2d = new view2d::Viewport(m_app); - m_simulation = new simulation::Simulation(); Info *info = new Info(*viewport2d, m_app); QSplitter *splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(viewport2d); + +#ifdef WITH_3D + m_simulation = new simulation::Simulation(); splitter->addWidget(m_simulation); +#endif + splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 0); @@ -82,10 +88,15 @@ void MainWindow::setupMenuActions() { // File actions connect(actionOpenFile, &QAction::triggered, this, &MainWindow::openFile); +#ifdef BUILD_WASM + connect(actionSaveFile, &QAction::triggered, this, &MainWindow::downloadFile); + connect(actionExportFile, &QAction::triggered, this, &MainWindow::downloadExportFile); +#else connect(actionSaveFile, &QAction::triggered, this, &MainWindow::saveFile); connect(actionSaveAsFile, &QAction::triggered, this, &MainWindow::saveAsFile); connect(actionExportFile, &QAction::triggered, this, &MainWindow::exportFile); connect(actionExportAsFile, &QAction::triggered, this, &MainWindow::exportAsFile); +#endif connect(actionOpenSettings, &QAction::triggered, this, &MainWindow::openSettings); // Edit actions @@ -99,13 +110,20 @@ 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() { +#ifndef BUILD_WASM + m_openedDocumentActions.addAction(actionSaveFile); m_openedDocumentActions.addAction(actionExportFile); +#else + actionSaveAsFile->setVisible(false); + actionExportAsFile->setVisible(false); +#endif m_openedDocumentActions.addAction(actionExportAsFile); - m_openedDocumentActions.addAction(actionSaveFile); m_openedDocumentActions.addAction(actionSaveAsFile); m_openedDocumentActions.addAction(actionLeftCutterCompensation); m_openedDocumentActions.addAction(actionRightCutterCompensation); @@ -117,6 +135,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,16 +164,31 @@ 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); } void MainWindow::openFile() { - const QString fileName = QFileDialog::getOpenFileName(this); - if (!fileName.isEmpty() && !m_app.loadFile(fileName)) { - QMessageBox::critical(this, "Error", "Invalid file type " + fileName); - } + auto onFileContentReady = [this](const QString &fileName, const QByteArray &fileContent){ + if (!fileName.isEmpty() && !m_app.loadFile(fileName, fileContent)) { + QMessageBox::critical(this, "Error", "Invalid file type " + fileName); + } + }; + + QFileDialog::getOpenFileContent("Text files (*.dxf *.dxfplot)", onFileContentReady); +} + +void MainWindow::downloadFile() +{ + const QString fileName = defaultFileName(model::Application::FileExtension::Dxfplot); + QFileDialog::saveFileContent(m_app.saveToDxfplot(), fileName); +} + +void MainWindow::downloadExportFile() +{ + const QString fileName = defaultFileName(model::Application::FileExtension::Gcode); + QFileDialog::saveFileContent(m_app.saveToGcode(), fileName); } void MainWindow::saveFile() @@ -216,6 +251,7 @@ void MainWindow::transformSelection() { dialogs::Transform transform; if (transform.exec() == QDialog::Accepted) { + m_app.takeDocumentSnapshot(); m_app.transformSelection(transform.matrix()); } } @@ -224,6 +260,7 @@ void MainWindow::mirrorSelection() { dialogs::Mirror mirror; if (mirror.exec() == QDialog::Accepted) { + m_app.takeDocumentSnapshot(); m_app.transformSelection(mirror.matrix()); } } @@ -234,15 +271,18 @@ 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)); +#ifdef WITH_3D m_simulation->hide(); +#endif } void MainWindow::displayError(const QString &message) @@ -253,9 +293,22 @@ void MainWindow::displayError(const QString &message) void MainWindow::simulate() { +#ifdef WITH_3D model::Simulation simulation = m_app.createSimulation(); m_simulation->setSimulation(std::move(simulation)); m_simulation->show(); +#endif +} + +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..3a9eec6 100644 --- a/src/view/mainwindow.h +++ b/src/view/mainwindow.h @@ -25,7 +25,9 @@ class MainWindow : public QMainWindow, private Ui::MainWindow private: model::Application &m_app; +#ifdef WITH_3D simulation::Simulation *m_simulation; +#endif QActionGroup m_openedDocumentActions; @@ -49,15 +51,17 @@ protected Q_SLOTS: void saveAsFile(); void exportFile(); void exportAsFile(); + void downloadFile(); + void downloadExportFile(); void openSettings(); 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/simulation/internal/toolpath.cpp b/src/view/simulation/internal/toolpath.cpp index 3e9a4e7..4c99938 100644 --- a/src/view/simulation/internal/toolpath.cpp +++ b/src/view/simulation/internal/toolpath.cpp @@ -1,9 +1,9 @@ #include -#include -#include -#include -#include +#include +#include +#include +#include #include @@ -51,28 +51,28 @@ void ToolPath::createPolylineFromPoints(const model::Simulation::ToolPathPoint3D m_indices[i] = i; } - Qt3DRender::QGeometry *geometry = new Qt3DRender::QGeometry(this); + Qt3DCore::QGeometry *geometry = new Qt3DCore::QGeometry(this); const QByteArray vertexData = QByteArray::fromRawData((const char *)m_packedPoints.get(), sizeof(PackedVector3D) * nbPoints); - Qt3DRender::QBuffer *vertexBuffer = new Qt3DRender::QBuffer(geometry); + Qt3DCore::QBuffer *vertexBuffer = new Qt3DCore::QBuffer(geometry); vertexBuffer->setData(vertexData); const QByteArray colorData = QByteArray::fromRawData((const char *)m_colors.get(), sizeof(uint32_t) * nbPoints); - Qt3DRender::QBuffer *colorBuffer = new Qt3DRender::QBuffer(geometry); + Qt3DCore::QBuffer *colorBuffer = new Qt3DCore::QBuffer(geometry); colorBuffer->setData(colorData); const QByteArray indexData = QByteArray::fromRawData((const char *)m_indices.get(), sizeof(uint32_t) * nbPoints); - Qt3DRender::QBuffer *indexBuffer = new Qt3DRender::QBuffer(geometry); + Qt3DCore::QBuffer *indexBuffer = new Qt3DCore::QBuffer(geometry); indexBuffer->setData(indexData); - Qt3DRender::QAttribute *vertexAttribute = new Qt3DRender::QAttribute(vertexBuffer, Qt3DRender::QAttribute::defaultPositionAttributeName(), Qt3DRender::QAttribute::Float, 3, nbPoints); - vertexAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute); + Qt3DCore::QAttribute *vertexAttribute = new Qt3DCore::QAttribute(vertexBuffer, Qt3DCore::QAttribute::defaultPositionAttributeName(), Qt3DCore::QAttribute::Float, 3, nbPoints); + vertexAttribute->setAttributeType(Qt3DCore::QAttribute::VertexAttribute); - Qt3DRender::QAttribute *colorAttribute = new Qt3DRender::QAttribute(colorBuffer, Qt3DRender::QAttribute::defaultColorAttributeName(), Qt3DRender::QAttribute::UnsignedByte, 4, nbPoints); - colorAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute); + Qt3DCore::QAttribute *colorAttribute = new Qt3DCore::QAttribute(colorBuffer, Qt3DCore::QAttribute::defaultColorAttributeName(), Qt3DCore::QAttribute::UnsignedByte, 4, nbPoints); + colorAttribute->setAttributeType(Qt3DCore::QAttribute::VertexAttribute); - Qt3DRender::QAttribute *indexAttribute = new Qt3DRender::QAttribute(indexBuffer, Qt3DRender::QAttribute::UnsignedInt, 3, nbPoints); - indexAttribute->setAttributeType(Qt3DRender::QAttribute::IndexAttribute); + Qt3DCore::QAttribute *indexAttribute = new Qt3DCore::QAttribute(indexBuffer, Qt3DCore::QAttribute::UnsignedInt, 3, nbPoints); + indexAttribute->setAttributeType(Qt3DCore::QAttribute::IndexAttribute); geometry->addAttribute(vertexAttribute); geometry->addAttribute(colorAttribute); diff --git a/src/view/task/layertreemodel.cpp b/src/view/task/layertreemodel.cpp index 61eef27..4badfe4 100644 --- a/src/view/task/layertreemodel.cpp +++ b/src/view/task/layertreemodel.cpp @@ -7,7 +7,8 @@ namespace view::task LayerTreeModel::LayerTreeModel(model::Task &task, QObject *parent) :QAbstractItemModel(parent), - m_task(task) + m_task(task), + m_ignoreSelectionChanged(false) { } @@ -117,21 +118,40 @@ void LayerTreeModel::itemClicked(const QModelIndex& index) model::Renderable *item = static_cast(index.internalPointer()); item->toggleVisible(); + emit documentVisibilityChanged(); + emit dataChanged(index, index); } } } +void LayerTreeModel::clearSelection(QItemSelectionModel *selectionModel) +{ + m_ignoreSelectionChanged = true; + + selectionModel->clear(); + + m_ignoreSelectionChanged = false; +} + void LayerTreeModel::updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel) { + m_ignoreSelectionChanged = true; + const std::pair indices = m_task.layerAndPathIndexFor(path); const QModelIndex parentIndex = index(indices.first, 0); const QModelIndex childIndex = index(indices.second, 0, parentIndex); selectionModel->select(childIndex, flag); + + m_ignoreSelectionChanged = false; } void LayerTreeModel::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { + if (m_ignoreSelectionChanged) { + return; + } + for (const QModelIndex &index : selected.indexes()) { model::Renderable &renderable = *static_cast(index.internalPointer()); renderable.setSelected(true); diff --git a/src/view/task/layertreemodel.h b/src/view/task/layertreemodel.h index d8863c7..d215c81 100644 --- a/src/view/task/layertreemodel.h +++ b/src/view/task/layertreemodel.h @@ -16,6 +16,7 @@ class LayerTreeModel: public QAbstractItemModel private: model::Task &m_task; + bool m_ignoreSelectionChanged; public: explicit LayerTreeModel(model::Task &task, QObject *parent); @@ -28,8 +29,12 @@ class LayerTreeModel: public QAbstractItemModel Qt::ItemFlags flags(const QModelIndex &index) const override; void itemClicked(const QModelIndex &index); + void clearSelection(QItemSelectionModel *selectionModel); void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel); void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +signals: + void documentVisibilityChanged(); }; } diff --git a/src/view/task/path.cpp b/src/view/task/path.cpp index 440a030..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); @@ -23,7 +23,8 @@ void Path::documentChanged() } Path::Path(model::Application &app) - :DocumentModelObserver(app) + :DocumentModelObserver(app), + m_app(app) { setupUi(this); } @@ -31,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/src/view/task/path.h b/src/view/task/path.h index ddc7d27..bfb35f3 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, func](ValueType value){ + func(value); + + m_app.takeDocumentSnapshot(); + }); } template diff --git a/src/view/task/pathlistmodel.cpp b/src/view/task/pathlistmodel.cpp index f9ca39e..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; @@ -96,6 +97,23 @@ QModelIndex PathListModel::movePath(const QModelIndex &index, model::Task::MoveD return index; } +QModelIndex PathListModel::movePathToTip(const QModelIndex &index, model::Task::MoveTip tip) +{ + const int row = index.row(); + const int newRow = (tip == model::Task::MoveTip::Top) ? 0 : (rowCount(index) - 1); + + if (index.isValid()) { + m_task.movePathToTip(row, tip); + + const QModelIndex newIndex = this->index(newRow); + emit dataChanged(index, newIndex); + + return newIndex; + } + + return index; +} + void PathListModel::itemClicked(const QModelIndex& index) { if ((index.flags() & Qt::ItemIsEnabled) == 0) { @@ -113,14 +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 8652e4a..88f155c 100644 --- a/src/view/task/pathlistmodel.h +++ b/src/view/task/pathlistmodel.h @@ -16,6 +16,7 @@ class PathListModel : public QAbstractListModel private: model::Task &m_task; + bool m_ignoreSelectionChanged; public: explicit PathListModel(model::Task &task, QObject *parent); @@ -25,11 +26,16 @@ class PathListModel : public QAbstractListModel int columnCount(const QModelIndex &parent = QModelIndex()) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; - QModelIndex movePath(const QModelIndex &index, model::Task::MoveDirection direction); + QModelIndex movePathToDirection(const QModelIndex &index, model::Task::MoveDirection direction); + QModelIndex movePathToTip(const QModelIndex &index, model::Task::MoveTip tip); void itemClicked(const QModelIndex &index); + void clearSelection(QItemSelectionModel *selectionModel); void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag, QItemSelectionModel *selectionModel); void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +signals: + void documentVisibilityChanged(); }; } diff --git a/src/view/task/task.cpp b/src/view/task/task.cpp index 6ad375b..3067af9 100644 --- a/src/view/task/task.cpp +++ b/src/view/task/task.cpp @@ -10,7 +10,8 @@ namespace view::task { Task::Task(model::Application &app) - :DocumentModelObserver(app) + :DocumentModelObserver(app), + m_app(app) { setupUi(this); } @@ -31,8 +32,10 @@ void Task::setupController() setupTreeViewController(m_pathListModel, pathsTreeView); setupTreeViewController(m_layerTreeModel, layersTreeView); - connect(moveUp, &QPushButton::pressed, [this](){ moveCurrentPath(model::Task::MoveDirection::UP); }); - connect(moveDown, &QPushButton::pressed, [this](){ moveCurrentPath(model::Task::MoveDirection::DOWN); }); + connect(moveUp, &QPushButton::pressed, [this](){ moveCurrentPathToDirection(model::Task::MoveDirection::UP); }); + connect(moveDown, &QPushButton::pressed, [this](){ moveCurrentPathToDirection(model::Task::MoveDirection::DOWN); }); + connect(moveTop, &QPushButton::pressed, [this](){ moveCurrentPathToTip(model::Task::MoveTip::Top); }); + connect(moveBottom, &QPushButton::pressed, [this](){ moveCurrentPathToTip(model::Task::MoveTip::Bottom); }); } void Task::updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag) @@ -53,12 +56,39 @@ void Task::pathSelectedChanged(model::Path &path, bool selected) selected ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); } -void Task::moveCurrentPath(model::Task::MoveDirection direction) +void Task::moveCurrentPathToDirection(model::Task::MoveDirection direction) { - QItemSelectionModel *selectionModel = pathsTreeView->selectionModel(); - const QModelIndex currentSelectedIndex = selectionModel->currentIndex(); - const QModelIndex newSelectedIndex = m_pathListModel->movePath(currentSelectedIndex, direction); - selectionModel->setCurrentIndex(newSelectedIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current); + moveCurrentPath([this, direction](const QModelIndex& index){ + m_pathListModel->movePathToDirection(index, direction); + }); +} + +void Task::documentVisibilityChanged() +{ + m_app.takeDocumentSnapshot(); +} + +void Task::moveCurrentPathToTip(model::Task::MoveTip tip) +{ + moveCurrentPath([this, tip](const QModelIndex& index){ + m_pathListModel->movePathToTip(index, tip); + }); +} + +void Task::rebuildSelectionFromTask() +{ + QItemSelectionModel *pathsTreeSelectionModel = pathsTreeView->selectionModel(); + QItemSelectionModel *layersTreeSelectionModel = layersTreeView->selectionModel(); + + m_pathListModel->clearSelection(pathsTreeSelectionModel); + m_layerTreeModel->clearSelection(layersTreeSelectionModel); + + task().forEachSelectedPath([this, pathsTreeSelectionModel, layersTreeSelectionModel](const model::Path& path){ + constexpr QItemSelectionModel::SelectionFlag flag = QItemSelectionModel::Select; + + m_pathListModel->updateItemSelection(path, flag, pathsTreeSelectionModel); + m_layerTreeModel->updateItemSelection(path, flag, layersTreeSelectionModel); + }); } } diff --git a/src/view/task/task.h b/src/view/task/task.h index ba35ea2..a532270 100644 --- a/src/view/task/task.h +++ b/src/view/task/task.h @@ -17,6 +17,8 @@ class LayerTreeModel; class Task : public model::DocumentModelObserver, private Ui::Task { private: + model::Application &m_app; + std::unique_ptr m_pathListModel; std::unique_ptr m_layerTreeModel; @@ -42,6 +44,8 @@ class Task : public model::DocumentModelObserver, private Ui::Task connect(selectionModel, &QItemSelectionModel::selectionChanged, model.get(), &Model::selectionChanged); connect(treeView, &QTreeView::clicked, model.get(), &Model::itemClicked); + + connect(model.get(), &Model::documentVisibilityChanged, this, &Task::documentVisibilityChanged); } void setupModel(); @@ -49,6 +53,26 @@ class Task : public model::DocumentModelObserver, private Ui::Task void updateItemSelection(const model::Path &path, QItemSelectionModel::SelectionFlag flag); + void moveCurrentPathToDirection(model::Task::MoveDirection direction); + void moveCurrentPathToTip(model::Task::MoveTip tip); + + template + void moveCurrentPath(Func &&movement) + { + QItemSelectionModel *selectionModel = pathsTreeView->selectionModel(); + + const QModelIndexList selectedItems = selectionModel->selectedIndexes(); + for (const QModelIndex& selectedIndex : selectedItems) { + movement(selectedIndex); + } + + rebuildSelectionFromTask(); + + m_app.takeDocumentSnapshot(); + } + + void rebuildSelectionFromTask(); + public: explicit Task(model::Application &app); @@ -56,9 +80,8 @@ class Task : public model::DocumentModelObserver, private Ui::Task void documentChanged(); protected Q_SLOTS: - void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void pathSelectedChanged(model::Path &path, bool selected); - void moveCurrentPath(model::Task::MoveDirection direction); + void documentVisibilityChanged(); }; } diff --git a/src/view/view2d/viewport.cpp b/src/view/view2d/viewport.cpp index 2c41469..cb63a11 100644 --- a/src/view/view2d/viewport.cpp +++ b/src/view/view2d/viewport.cpp @@ -250,7 +250,11 @@ class BackgroundPainter void Viewport::documentChanged() { setupModel(); - fitItemsInView(); +} + +void Viewport::newDocumentOpened() +{ + fitItemsInView(); // TODO delay after UI update } void Viewport::wheelEvent(QWheelEvent *event) diff --git a/src/view/view2d/viewport.h b/src/view/view2d/viewport.h index e67e807..51c6f18 100644 --- a/src/view/view2d/viewport.h +++ b/src/view/view2d/viewport.h @@ -42,6 +42,7 @@ class Viewport : public model::DocumentModelObserver protected: void documentChanged() override; + void newDocumentOpened() override; void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; diff --git a/template/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/dialogs/settings/list.ui b/template/uic/dialogs/settings/list.ui index ba306c3..f8dd218 100644 --- a/template/uic/dialogs/settings/list.ui +++ b/template/uic/dialogs/settings/list.ui @@ -14,7 +14,7 @@ GroupBox - Qt::RightToLeft + Qt::LeftToRight diff --git a/template/uic/dialogs/settings/settings.ui b/template/uic/dialogs/settings/settings.ui index 5879977..c49ffda 100644 --- a/template/uic/dialogs/settings/settings.ui +++ b/template/uic/dialogs/settings/settings.ui @@ -7,21 +7,15 @@ 0 0 1000 - 298 + 498 - + 0 0 - - - 1000 - 0 - - Settings @@ -31,62 +25,110 @@ 0 - + QLayout::SetDefaultConstraint - - - - false - - - + + + + 0 - - - :/icons/list-remove.svg:/icons/list-remove.svg - - + + + + true + + + + + + + 0 + + + + + false + + + + + + + :/icons/list-add.svg:/icons/list-add.svg + + + + + + + false + + + + + + + :/icons/list-remove.svg:/icons/list-remove.svg + + + + + + + false + + + + + + + :/icons/edit-copy.svg:/icons/edit-copy.svg + + + + + + - - - + + + true - - - - - - false - - - - - - - :/icons/list-add.svg:/icons/list-add.svg - - - - - - - - - - - - false - - - - - - - :/icons/edit-copy.svg:/icons/edit-copy.svg - + + + + 0 + 0 + 723 + 426 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 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..a796c43 100644 --- a/template/uic/path.ui +++ b/template/uic/path.ui @@ -7,7 +7,7 @@ 0 0 230 - 168 + 176 @@ -15,76 +15,117 @@ - - - QLayout::SetDefaultConstraint + + + 0 - - QFormLayout::AllNonFixedFieldsGrow - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - Plane Feed Rate - - - - - - - 9999.989999999999782 - - - - - - - Intensity - - - - - - - 9999.989999999999782 - - - - - - - Depth + + + + 0 - - - - - - 9999.989999999999782 + + 0 - - 0.100000000000000 + + 0 - - - - - - Depth Feed Rate + + 0 - - - - - - 9999.989999999999782 + + 0 - - - + + + + QLayout::SetDefaultConstraint + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + Plane Feed Rate + + + + + + + false + + + 9999.989999999999782 + + + + + + + Intensity + + + + + + + false + + + 9999.989999999999782 + + + + + + + Depth + + + + + + + false + + + 9999.989999999999782 + + + 0.100000000000000 + + + + + + + Depth Feed Rate + + + + + + + false + + + 9999.989999999999782 + + + + + + + + + + + diff --git a/template/uic/simulation/CMakeLists.txt b/template/uic/simulation/CMakeLists.txt index 51d2061..61ba327 100644 --- a/template/uic/simulation/CMakeLists.txt +++ b/template/uic/simulation/CMakeLists.txt @@ -1,4 +1,4 @@ -qt5_wrap_ui(UIC_HEADERS +qt_wrap_ui(UIC_HEADERS simulation.ui ) diff --git a/template/uic/simulation/simulation.ui b/template/uic/simulation/simulation.ui index 0007ab7..f0a7810 100644 --- a/template/uic/simulation/simulation.ui +++ b/template/uic/simulation/simulation.ui @@ -67,6 +67,12 @@ 0 + + 100 + + + 1000 + Qt::Horizontal diff --git a/template/uic/task.ui b/template/uic/task.ui index 55e9a31..b92ab9a 100644 --- a/template/uic/task.ui +++ b/template/uic/task.ui @@ -92,6 +92,28 @@ + + + + + + + + :/icons/layer-top.svg:/icons/layer-top.svg + + + + + + + + + + + :/icons/layer-bottom.svg:/icons/layer-bottom.svg + + + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 01c4373..25a1139 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 ) @@ -37,8 +37,8 @@ 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_include_directories(dxfplotter-test PRIVATE ${QtTest_INCLUDE_DIRS}) +target_link_libraries(dxfplotter-test ${LINK_LIBRARIES} Qt::Test gtest_main) add_coverage(dxfplotter-test) diff --git a/thirdparty/libdxfrw/intern/dxfreader.h b/thirdparty/libdxfrw/intern/dxfreader.h index e4bba4b..b0a8652 100644 --- a/thirdparty/libdxfrw/intern/dxfreader.h +++ b/thirdparty/libdxfrw/intern/dxfreader.h @@ -27,7 +27,7 @@ class dxfReader { }; enum TYPE type; public: - dxfReader(std::ifstream *stream){ + dxfReader(std::istream *stream){ filestr = stream; type = INVALID; } @@ -60,7 +60,7 @@ class dxfReader { virtual bool readBool() = 0; protected: - std::ifstream *filestr; + std::istream *filestr; std::string strData; double doubleData; signed int intData; //32 bits integer @@ -73,7 +73,7 @@ class dxfReader { class dxfReaderBinary : public dxfReader { public: - dxfReaderBinary(std::ifstream *stream):dxfReader(stream){skip = false; } + dxfReaderBinary(std::istream *stream):dxfReader(stream){skip = false; } virtual ~dxfReaderBinary() {} virtual bool readCode(int *code); virtual bool readString(std::string *text); @@ -88,7 +88,7 @@ class dxfReaderBinary : public dxfReader { class dxfReaderAscii : public dxfReader { public: - dxfReaderAscii(std::ifstream *stream):dxfReader(stream){skip = true; } + dxfReaderAscii(std::istream *stream):dxfReader(stream){skip = true; } virtual ~dxfReaderAscii(){} virtual bool readCode(int *code); virtual bool readString(std::string *text); diff --git a/thirdparty/libdxfrw/libdxfrw.cpp b/thirdparty/libdxfrw/libdxfrw.cpp index b40a17f..01c8f3d 100644 --- a/thirdparty/libdxfrw/libdxfrw.cpp +++ b/thirdparty/libdxfrw/libdxfrw.cpp @@ -62,49 +62,53 @@ void dxfRW::setDebug(DRW::DebugLevel lvl){ } } -bool dxfRW::read(DRW_Interface *interface_, bool ext){ - drw_assert(fileName.empty() == false); +bool dxfRW::read(std::istream& stream, DRW_Interface *interface_, bool ext){ applyExt = ext; - std::ifstream filestr; - if (nullptr == interface_) { - return setError(DRW::BAD_UNKNOWN); - } - DRW_DBG("dxfRW::read 1def\n"); - filestr.open (fileName.c_str(), std::ios_base::in | std::ios::binary); - if (!filestr.is_open() - || !filestr.good()) { - return setError(DRW::BAD_OPEN); - } char line[22]; char line2[22] = "AutoCAD Binary DXF\r\n"; line2[20] = (char)26; line2[21] = '\0'; - filestr.read (line, 22); - filestr.close(); + stream.read (line, 22); + stream.seekg(0); iface = interface_; DRW_DBG("dxfRW::read 2\n"); if (strcmp(line, line2) == 0) { - filestr.open (fileName.c_str(), std::ios_base::in | std::ios::binary); binFile = true; - //skip sentinel - filestr.seekg (22, std::ios::beg); - reader = new dxfReaderBinary(&filestr); + reader = new dxfReaderBinary(&stream); DRW_DBG("dxfRW::read binary file\n"); } else { binFile = false; - filestr.open (fileName.c_str(), std::ios_base::in); - reader = new dxfReaderAscii(&filestr); + reader = new dxfReaderAscii(&stream); } bool isOk {processDxf()}; - filestr.close(); version = (DRW::Version) reader->getVersion(); delete reader; reader = nullptr; return isOk; } + +bool dxfRW::read(DRW_Interface *interface_, bool ext){ + drw_assert(fileName.empty() == false); + std::ifstream filestr; + if (nullptr == interface_) { + return setError(DRW::BAD_UNKNOWN); + } + DRW_DBG("dxfRW::read 1def\n"); + filestr.open (fileName.c_str(), std::ios_base::in | std::ios::binary); + if (!filestr.is_open() + || !filestr.good()) { + return setError(DRW::BAD_OPEN); + } + + const bool isOk = read(filestr, interface_, ext); + filestr.close(); + + return isOk; +} + bool dxfRW::write(DRW_Interface *interface_, DRW::Version ver, bool bin){ bool isOk = false; std::ofstream filestr; diff --git a/thirdparty/libdxfrw/libdxfrw.h b/thirdparty/libdxfrw/libdxfrw.h index 7deee2f..c52e0b5 100644 --- a/thirdparty/libdxfrw/libdxfrw.h +++ b/thirdparty/libdxfrw/libdxfrw.h @@ -39,6 +39,7 @@ class dxfRW { * @return true for success */ bool read(DRW_Interface *interface_, bool ext); + bool read(std::istream &stream, DRW_Interface *interface_, bool ext); void setBinary(bool b) {binFile = b;} bool write(DRW_Interface *interface_, DRW::Version ver, bool bin); diff --git a/wasm/Dockerfile b/wasm/Dockerfile new file mode 100644 index 0000000..b6636e3 --- /dev/null +++ b/wasm/Dockerfile @@ -0,0 +1,4 @@ +FROM stateoftheartio/qt6:6.3-wasm-aqt + +RUN sudo apt-get update && sudo apt-get install -y python3-pip +RUN python3 -m pip install jinja2