diff --git a/src/app/GUI/canvaswindow.cpp b/src/app/GUI/canvaswindow.cpp index 6ddb2920f..f1d782b9a 100644 --- a/src/app/GUI/canvaswindow.cpp +++ b/src/app/GUI/canvaswindow.cpp @@ -28,6 +28,7 @@ #include #include +#include #include "mainwindow.h" #include "GUI/BoxesList/boxscroller.h" @@ -173,6 +174,10 @@ void CanvasWindow::renderSk(SkCanvas * const canvas) { qreal pixelRatio = this->devicePixelRatioF(); if (mCurrentCanvas) { + const QTransform worldToScreen(mViewTransform.m11(), mViewTransform.m12(), 0.0, + mViewTransform.m21(), mViewTransform.m22(), 0.0, + mViewTransform.dx(), mViewTransform.dy(), 1.0); + mCurrentCanvas->setWorldToScreen(worldToScreen, pixelRatio); canvas->save(); mCurrentCanvas->renderSk(canvas, rect(), diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index ef419bac7..bcb50ec78 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include "GUI/edialogs.h" #include "dialogs/applyexpressiondialog.h" @@ -76,6 +77,7 @@ #include "widgets/assetswidget.h" #include "dialogs/adjustscenedialog.h" #include "dialogs/commandpalette.h" +#include "dialogs/gridsettingsdialog.h" using namespace Friction; @@ -117,6 +119,18 @@ MainWindow::MainWindow(Document& document, , mInvertSelAct(nullptr) , mClearSelAct(nullptr) , mAddKeyAct(nullptr) + , mShowGridAct(nullptr) + , mSnappingAct(nullptr) + , mSnapToGridAct(nullptr) + , mSnapToCanvasAct(nullptr) + , mSnapToBoxesAct(nullptr) + , mSnapToNodesAct(nullptr) + , mSnapToPivotsAct(nullptr) + , mSnapAnchorPivotAct(nullptr) + , mSnapAnchorBoundsAct(nullptr) + , mSnapAnchorNodesAct(nullptr) + , mGridSettingsAct(nullptr) + , mGridDrawOnTopAct(nullptr) , mAddToQueAct(nullptr) , mViewFullScreenAct(nullptr) , mFontWidget(nullptr) @@ -160,6 +174,13 @@ MainWindow::MainWindow(Document& document, Q_ASSERT(!sInstance); sInstance = this; + connect(&mDocument, &Document::gridSettingsChanged, + this, &MainWindow::onGridSettingsChanged); + connect(&mDocument, &Document::gridSnapEnabledChanged, + this, &MainWindow::onGridSnapEnabledChanged); + connect(&mDocument, &Document::snappingActiveChanged, + this, &MainWindow::onSnappingActiveChanged); + setWindowIcon(QIcon::fromTheme(AppSupport::getAppID())); setContextMenuPolicy(Qt::NoContextMenu); @@ -208,6 +229,109 @@ BoundingBox *MainWindow::getCurrentBox() return box; } +void MainWindow::openGridSettingsDialog() +{ + auto dialog = new GridSettingsDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setWindowTitle(tr("Grid Settings")); + dialog->setSettings(mDocument.gridController().settings); + connect(dialog, &GridSettingsDialog::applyRequested, + this, [this](Friction::Core::GridSettings settings, bool saveDefaults) { + settings.enabled = mDocument.gridController().settings.enabled; + mDocument.setGridSettings(settings); + if (saveDefaults) { + mDocument.saveGridSettingsAsDefault(mDocument.gridController().settings); + } + }); + connect(dialog, &QDialog::accepted, + this, [this, dialog]() { + auto settings = dialog->settings(); + settings.enabled = mDocument.gridController().settings.enabled; + mDocument.setGridSettings(settings); + if (dialog->saveAsDefault()) { + mDocument.saveGridSettingsAsDefault(mDocument.gridController().settings); + } + }); + dialog->show(); +} + +void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& settings) +{ + onGridSnapEnabledChanged(settings.enabled); + if (mShowGridAct) { + QSignalBlocker blocker(mShowGridAct); + if (mShowGridAct->isChecked() != settings.show) { + mShowGridAct->setChecked(settings.show); + } + } + if (mSnapToCanvasAct) { + QSignalBlocker blocker(mSnapToCanvasAct); + if (mSnapToCanvasAct->isChecked() != settings.snapToCanvas) { + mSnapToCanvasAct->setChecked(settings.snapToCanvas); + } + } + if (mSnapToBoxesAct) { + QSignalBlocker blocker(mSnapToBoxesAct); + if (mSnapToBoxesAct->isChecked() != settings.snapToBoxes) { + mSnapToBoxesAct->setChecked(settings.snapToBoxes); + } + } + if (mSnapToNodesAct) { + QSignalBlocker blocker(mSnapToNodesAct); + if (mSnapToNodesAct->isChecked() != settings.snapToNodes) { + mSnapToNodesAct->setChecked(settings.snapToNodes); + } + } + if (mSnapToPivotsAct) { + QSignalBlocker blocker(mSnapToPivotsAct); + if (mSnapToPivotsAct->isChecked() != settings.snapToPivots) { + mSnapToPivotsAct->setChecked(settings.snapToPivots); + } + } + if (mSnapAnchorPivotAct) { + QSignalBlocker blocker(mSnapAnchorPivotAct); + if (mSnapAnchorPivotAct->isChecked() != settings.snapAnchorPivot) { + mSnapAnchorPivotAct->setChecked(settings.snapAnchorPivot); + } + } + if (mSnapAnchorBoundsAct) { + QSignalBlocker blocker(mSnapAnchorBoundsAct); + if (mSnapAnchorBoundsAct->isChecked() != settings.snapAnchorBounds) { + mSnapAnchorBoundsAct->setChecked(settings.snapAnchorBounds); + } + } + if (mSnapAnchorNodesAct) { + QSignalBlocker blocker(mSnapAnchorNodesAct); + if (mSnapAnchorNodesAct->isChecked() != settings.snapAnchorNodes) { + mSnapAnchorNodesAct->setChecked(settings.snapAnchorNodes); + } + } + if (mGridDrawOnTopAct) { + QSignalBlocker blocker(mGridDrawOnTopAct); + if (mGridDrawOnTopAct->isChecked() != settings.drawOnTop) { + mGridDrawOnTopAct->setChecked(settings.drawOnTop); + } + } +} + +void MainWindow::onGridSnapEnabledChanged(bool enabled) +{ + if (!mSnapToGridAct) { return; } + QSignalBlocker blocker(mSnapToGridAct); + if (mSnapToGridAct->isChecked() != enabled) { + mSnapToGridAct->setChecked(enabled); + } +} + +void MainWindow::onSnappingActiveChanged(bool active) +{ + if (!mSnappingAct) { return; } + QSignalBlocker blocker(mSnappingAct); + if (mSnappingAct->isChecked() != active) { + mSnappingAct->setChecked(active); + } +} + void MainWindow::checkAutoSaveTimer() { if (mShutdown) { return; } diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 37ba7eaf7..c0c2b77f3 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -51,6 +51,7 @@ #include "window.h" #include "GUI/RenderWidgets/renderwidget.h" #include "gizmos.h" +#include "gridcontroller.h" #include "widgets/fontswidget.h" #include "widgets/toolbar.h" @@ -198,6 +199,10 @@ class MainWindow : public QMainWindow void openWelcomeDialog(); void closeWelcomeDialog(); + void openGridSettingsDialog(); + void onGridSettingsChanged(const Friction::Core::GridSettings& settings); + void onGridSnapEnabledChanged(bool enabled); + void onSnappingActiveChanged(bool active); eKeyFilter* mNumericFilter = eKeyFilter::sCreateNumberFilter(this); eKeyFilter* mLineFilter = eKeyFilter::sCreateLineFilter(this); @@ -255,6 +260,18 @@ class MainWindow : public QMainWindow QAction *mZoomInAction; QAction *mZoomOutAction; QAction *mFitViewAction; + QAction *mShowGridAct; + QAction *mSnappingAct; + QAction *mSnapToGridAct; + QAction *mSnapToCanvasAct; + QAction *mSnapToBoxesAct; + QAction *mSnapToNodesAct; + QAction *mSnapToPivotsAct; + QAction *mSnapAnchorPivotAct; + QAction *mSnapAnchorBoundsAct; + QAction *mSnapAnchorNodesAct; + QAction *mGridSettingsAct; + QAction *mGridDrawOnTopAct; QAction *mNoneQuality; QAction *mLowQuality; @@ -282,6 +299,7 @@ class MainWindow : public QMainWindow QMenu *mEffectsMenu; QMenu *mSceneMenu; QMenu *mViewMenu; + QMenu *mGridMenu; QMenu *mPanelsMenu; QMenu *mRenderMenu; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index 8792d10f9..0b47d8570 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -566,6 +566,134 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mResetZoomAction); + mGridMenu = mViewMenu->addMenu(QIcon::fromTheme("grid"), + tr("Grid && Snapping", "MenuBar_View")); + + mSnappingAct = mGridMenu->addAction(tr("Snapping")); + mSnappingAct->setCheckable(true); + mSnappingAct->setChecked(mDocument.isSnappingActive()); + connect(mSnappingAct, &QAction::toggled, this, [this](bool checked) { + mDocument.setSnappingActive(checked); + }); + cmdAddAction(mSnappingAct); + + mGridMenu->addSeparator(); + + mSnapToCanvasAct = mGridMenu->addAction(tr("Snap to Canvas")); + mSnapToCanvasAct->setCheckable(true); + mSnapToCanvasAct->setChecked(mDocument.gridController().settings.snapToCanvas); + connect(mSnapToCanvasAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.snapToCanvas == checked) { return; } + settings.snapToCanvas = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mSnapToCanvasAct); + + mSnapToGridAct = mGridMenu->addAction(tr("Snap to Grid")); + mSnapToGridAct->setCheckable(true); + mSnapToGridAct->setChecked(mDocument.gridController().settings.enabled); + connect(mSnapToGridAct, &QAction::toggled, this, [this](bool checked) { + mDocument.setGridSnapEnabled(checked); + }); + cmdAddAction(mSnapToGridAct); + + mSnapToPivotsAct = mGridMenu->addAction(tr("Snap to Pivots")); + mSnapToPivotsAct->setCheckable(true); + mSnapToPivotsAct->setChecked(mDocument.gridController().settings.snapToPivots); + connect(mSnapToPivotsAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.snapToPivots == checked) { return; } + settings.snapToPivots = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mSnapToPivotsAct); + + mSnapToBoxesAct = mGridMenu->addAction(tr("Snap to Boxes")); + mSnapToBoxesAct->setCheckable(true); + mSnapToBoxesAct->setChecked(mDocument.gridController().settings.snapToBoxes); + connect(mSnapToBoxesAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.snapToBoxes == checked) { return; } + settings.snapToBoxes = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mSnapToBoxesAct); + + mSnapToNodesAct = mGridMenu->addAction(tr("Snap to Nodes")); + mSnapToNodesAct->setCheckable(true); + mSnapToNodesAct->setChecked(mDocument.gridController().settings.snapToNodes); + connect(mSnapToNodesAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.snapToNodes == checked) { return; } + settings.snapToNodes = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mSnapToNodesAct); + + mGridMenu->addSeparator(); + + mSnapAnchorPivotAct = mGridMenu->addAction(tr("Pivot anchor")); + mSnapAnchorPivotAct->setCheckable(true); + mSnapAnchorPivotAct->setChecked(mDocument.gridController().settings.snapAnchorPivot); + connect(mSnapAnchorPivotAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.snapAnchorPivot == checked) { return; } + settings.snapAnchorPivot = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mSnapAnchorPivotAct); + + mSnapAnchorBoundsAct = mGridMenu->addAction(tr("Boxes anchors")); + mSnapAnchorBoundsAct->setCheckable(true); + mSnapAnchorBoundsAct->setChecked(mDocument.gridController().settings.snapAnchorBounds); + connect(mSnapAnchorBoundsAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.snapAnchorBounds == checked) { return; } + settings.snapAnchorBounds = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mSnapAnchorBoundsAct); + + mSnapAnchorNodesAct = mGridMenu->addAction(tr("Nodes anchors")); + mSnapAnchorNodesAct->setCheckable(true); + mSnapAnchorNodesAct->setChecked(mDocument.gridController().settings.snapAnchorNodes); + connect(mSnapAnchorNodesAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.snapAnchorNodes == checked) { return; } + settings.snapAnchorNodes = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mSnapAnchorNodesAct); + + mGridMenu->addSeparator(); + + mShowGridAct = mGridMenu->addAction(tr("Show Grid")); + mShowGridAct->setCheckable(true); + mShowGridAct->setChecked(mDocument.gridController().settings.show); + connect(mShowGridAct, &QAction::toggled, this, [this](bool checked) { + mDocument.setGridVisible(checked); + }); + cmdAddAction(mShowGridAct); + + mGridDrawOnTopAct = mGridMenu->addAction(tr("Grid on top")); + mGridDrawOnTopAct->setCheckable(true); + connect(mGridDrawOnTopAct, &QAction::toggled, this, [this](bool checked) { + auto settings = mDocument.gridController().settings; + if (settings.drawOnTop == checked) { return; } + settings.drawOnTop = checked; + mDocument.setGridSettings(settings); + }); + cmdAddAction(mGridDrawOnTopAct); + + mGridMenu->addSeparator(); + + mGridSettingsAct = mGridMenu->addAction(tr("Grid Settings...")); + connect(mGridSettingsAct, &QAction::triggered, this, &MainWindow::openGridSettingsDialog); + cmdAddAction(mGridSettingsAct); + + onGridSettingsChanged(mDocument.gridController().settings); + const auto filteringMenu = mViewMenu->addMenu(QIcon::fromTheme("user-desktop"), tr("Filtering", "MenuBar_View")); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fb668b8b0..40ff516e2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -54,6 +54,7 @@ include_directories( set( SOURCES appsupport.cpp + gridcontroller.cpp Boxes/nullobject.cpp Expressions/expression.cpp Expressions/expressionpresets.cpp @@ -359,6 +360,7 @@ set( set( HEADERS appsupport.h + gridcontroller.h Boxes/nullobject.h Expressions/expression.h Expressions/expressionpresets.h diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 28085eaf8..51b284a48 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -28,6 +28,11 @@ #include "canvas.h" #include "simpletask.h" +#include +#include + +#include + Document* Document::sInstance = nullptr; using namespace Friction::Core; @@ -36,6 +41,7 @@ Document::Document(TaskScheduler& taskScheduler) { Q_ASSERT(!sInstance); sInstance = this; + loadGridSettingsFromSettings(); connect(&taskScheduler, &TaskScheduler::finishedAllQuedTasks, this, &Document::updateScenes); } @@ -64,6 +70,287 @@ void Document::replaceClipboard(const stdsptr &container) { fClipboardContainer = container; } +GridController& Document::gridController() +{ + return mGridController; +} + +const GridController& Document::gridController() const +{ + return mGridController; +} + +template +static bool gridNearlyEqual(const T lhs, const T rhs) +{ + constexpr double eps = 1e-6; + return std::abs(static_cast(lhs) - static_cast(rhs)) <= eps; +} + +static GridSettings sanitizedGridSettings(GridSettings settings) +{ + if (settings.sizeX <= 0.0) { settings.sizeX = 1.0; } + if (settings.sizeY <= 0.0) { settings.sizeY = 1.0; } + if (settings.majorEveryX < 1) { settings.majorEveryX = 1; } + if (settings.majorEveryY < 1) { settings.majorEveryY = 1; } + if (settings.snapThresholdPx < 0) { settings.snapThresholdPx = 0; } + auto ensureAnimatorColor = [](qsptr& animator, + const QColor& fallback) + { + if (!animator) { animator = enve::make_shared(); } + QColor color = animator->getColor(); + if (!color.isValid()) { color = fallback; } + int alpha = color.alpha(); + if (alpha < 0) { alpha = 0; } + if (alpha > 255) { alpha = 255; } + color.setAlpha(alpha); + animator->setColor(color); + return color; + }; + const QColor minorFallback = GridSettings::defaults().colorAnimator->getColor(); + const QColor majorFallback = GridSettings::defaults().majorColorAnimator->getColor(); + ensureAnimatorColor(settings.colorAnimator, minorFallback); + ensureAnimatorColor(settings.majorColorAnimator, majorFallback); + return settings; +} + +void Document::setGridSnapEnabled(const bool enabled) +{ + auto updated = mGridController.settings; + if (updated.enabled == enabled) { return; } + updated.enabled = enabled; + applyGridSettings(updated, false, false); +} + +bool Document::isSnappingActive() const +{ + return mSnappingActive; +} + +void Document::setSnappingActive(const bool active) +{ + if (mSnappingActive == active) { return; } + mSnappingActive = active; + AppSupport::setSettings("grid", "snappingActive", mSnappingActive); + if (auto* settingsMgr = eSettings::sInstance) { + settingsMgr->fGridSnappingActive = mSnappingActive; + settingsMgr->saveKeyToFile("gridSnappingActive"); + } + emit snappingActiveChanged(mSnappingActive); +} + +void Document::setGridVisible(const bool visible) +{ + auto updated = mGridController.settings; + if (updated.show == visible) { return; } + updated.show = visible; + applyGridSettings(updated, false, false); +} + +void Document::setGridSettings(const GridSettings& settings) +{ + auto updated = settings; + updated.enabled = mGridController.settings.enabled; + applyGridSettings(updated, false, false); +} + +void Document::loadGridSettingsFromSettings() +{ + GridSettings defaults; + if (eSettings::sInstance) { + const auto& settingsMgr = eSettings::instance(); + defaults.drawOnTop = settingsMgr.fGridDrawOnTop; + defaults.snapToCanvas = settingsMgr.fGridSnapToCanvas; + defaults.snapToBoxes = settingsMgr.fGridSnapToBoxes; + defaults.snapToNodes = settingsMgr.fGridSnapToNodes; + defaults.snapToPivots = settingsMgr.fGridSnapToPivots; + defaults.snapAnchorPivot = settingsMgr.fGridSnapAnchorPivot; + defaults.snapAnchorBounds = settingsMgr.fGridSnapAnchorBounds; + defaults.snapAnchorNodes = settingsMgr.fGridSnapAnchorNodes; + } + GridSettings loaded = defaults; + loaded.sizeX = AppSupport::getSettings("grid", "sizeX", defaults.sizeX).toDouble(); + loaded.sizeY = AppSupport::getSettings("grid", "sizeY", defaults.sizeY).toDouble(); + loaded.originX = AppSupport::getSettings("grid", "originX", defaults.originX).toDouble(); + loaded.originY = AppSupport::getSettings("grid", "originY", defaults.originY).toDouble(); + loaded.snapThresholdPx = AppSupport::getSettings("grid", "snapThresholdPx", defaults.snapThresholdPx).toInt(); + loaded.enabled = AppSupport::getSettings("grid", "enabled", defaults.enabled).toBool(); + loaded.show = AppSupport::getSettings("grid", "show", defaults.show).toBool(); + loaded.drawOnTop = AppSupport::getSettings("grid", "drawOnTop", defaults.drawOnTop).toBool(); + loaded.snapToCanvas = AppSupport::getSettings("grid", "snapToCanvas", defaults.snapToCanvas).toBool(); + loaded.snapToBoxes = AppSupport::getSettings("grid", "snapToBoxes", defaults.snapToBoxes).toBool(); + loaded.snapToNodes = AppSupport::getSettings("grid", "snapToNodes", defaults.snapToNodes).toBool(); + loaded.snapToPivots = AppSupport::getSettings("grid", "snapToPivots", defaults.snapToPivots).toBool(); + loaded.snapAnchorPivot = AppSupport::getSettings("grid", "snapAnchorPivot", defaults.snapAnchorPivot).toBool(); + loaded.snapAnchorBounds = AppSupport::getSettings("grid", "snapAnchorBounds", defaults.snapAnchorBounds).toBool(); + loaded.snapAnchorNodes = AppSupport::getSettings("grid", "snapAnchorNodes", defaults.snapAnchorNodes).toBool(); + auto readMajorEvery = [](const QString& key, + int fallback, + bool& found) + { + QVariant variant = AppSupport::getSettings("grid", key, QVariant()); + if (variant.isValid()) { + found = true; + bool ok = false; + const int value = variant.toInt(&ok); + if (ok && value > 0) { + return value; + } + } + found = false; + return fallback; + }; + bool hasMajorX = false; + bool hasMajorY = false; + loaded.majorEveryX = readMajorEvery("majorEveryX", defaults.majorEveryX, hasMajorX); + loaded.majorEveryY = readMajorEvery("majorEveryY", defaults.majorEveryY, hasMajorY); + const QVariant legacyMajorVariant = AppSupport::getSettings("grid", "majorEvery", QVariant()); + if (legacyMajorVariant.isValid()) { + bool ok = false; + const int legacyMajor = legacyMajorVariant.toInt(&ok); + if (ok && legacyMajor > 0) { + if (!hasMajorX) { loaded.majorEveryX = legacyMajor; } + if (!hasMajorY) { loaded.majorEveryY = legacyMajor; } + } + } + auto readColor = [](const QVariant& variant, + const QColor& fallback) + { + QColor value; + if (variant.canConvert()) { + value = variant.value(); + } else { + value = QColor(variant.toString()); + } + if (!value.isValid()) { value = fallback; } + return value; + }; + QColor storedMinor = readColor( + AppSupport::getSettings("grid", "color", defaults.colorAnimator->getColor()), + GridSettings::defaults().colorAnimator->getColor()); + QColor storedMajor = readColor( + AppSupport::getSettings("grid", "majorColor", defaults.majorColorAnimator->getColor()), + GridSettings::defaults().majorColorAnimator->getColor()); + if (auto* settingsMgr = eSettings::sInstance) { + storedMinor = settingsMgr->fGridColor; + storedMajor = settingsMgr->fGridMajorColor; + } + if (!loaded.colorAnimator) { loaded.colorAnimator = enve::make_shared(); } + loaded.colorAnimator->setColor(storedMinor); + if (!loaded.majorColorAnimator) { loaded.majorColorAnimator = enve::make_shared(); } + loaded.majorColorAnimator->setColor(storedMajor); + applyGridSettings(loaded, true, true); + bool defaultSnappingActive = false; + if (auto* settingsMgr = eSettings::sInstance) { + defaultSnappingActive = settingsMgr->fGridSnappingActive; + } + mSnappingActive = AppSupport::getSettings("grid", "snappingActive", defaultSnappingActive).toBool(); + if (auto* settingsMgr = eSettings::sInstance) { + settingsMgr->fGridSnappingActive = mSnappingActive; + } +} + +void Document::saveGridSettingsToSettings(const GridSettings& settings) const +{ + AppSupport::setSettings("grid", "sizeX", settings.sizeX); + AppSupport::setSettings("grid", "sizeY", settings.sizeY); + AppSupport::setSettings("grid", "originX", settings.originX); + AppSupport::setSettings("grid", "originY", settings.originY); + AppSupport::setSettings("grid", "snapThresholdPx", settings.snapThresholdPx); + AppSupport::setSettings("grid", "enabled", settings.enabled); + AppSupport::setSettings("grid", "show", settings.show); + AppSupport::setSettings("grid", "drawOnTop", settings.drawOnTop); + AppSupport::setSettings("grid", "snapToCanvas", settings.snapToCanvas); + AppSupport::setSettings("grid", "snapToBoxes", settings.snapToBoxes); + AppSupport::setSettings("grid", "snapToNodes", settings.snapToNodes); + AppSupport::setSettings("grid", "snapToPivots", settings.snapToPivots); + AppSupport::setSettings("grid", "snapAnchorPivot", settings.snapAnchorPivot); + AppSupport::setSettings("grid", "snapAnchorBounds", settings.snapAnchorBounds); + AppSupport::setSettings("grid", "snapAnchorNodes", settings.snapAnchorNodes); + AppSupport::setSettings("grid", "majorEveryX", settings.majorEveryX); + AppSupport::setSettings("grid", "majorEveryY", settings.majorEveryY); + AppSupport::setSettings("grid", "majorEvery", settings.majorEveryX); + AppSupport::setSettings("grid", "snappingActive", mSnappingActive); + const QColor color = settings.colorAnimator ? settings.colorAnimator->getColor() : GridSettings::defaults().colorAnimator->getColor(); + const QColor majorColor = settings.majorColorAnimator ? settings.majorColorAnimator->getColor() : GridSettings::defaults().majorColorAnimator->getColor(); + AppSupport::setSettings("grid", "color", color); + AppSupport::setSettings("grid", "majorColor", majorColor); +} + +void Document::saveGridSettingsAsDefault(const GridSettings& settings) +{ + const GridSettings sanitized = sanitizedGridSettings(settings); + if (auto* settingsMgr = eSettings::sInstance) { + settingsMgr->fGridColor = sanitized.colorAnimator ? sanitized.colorAnimator->getColor() : GridSettings::defaults().colorAnimator->getColor(); + settingsMgr->fGridMajorColor = sanitized.majorColorAnimator ? sanitized.majorColorAnimator->getColor() : GridSettings::defaults().majorColorAnimator->getColor(); + settingsMgr->fGridDrawOnTop = sanitized.drawOnTop; + settingsMgr->fGridSnapToCanvas = sanitized.snapToCanvas; + settingsMgr->fGridSnapToBoxes = sanitized.snapToBoxes; + settingsMgr->fGridSnapToNodes = sanitized.snapToNodes; + settingsMgr->fGridSnapToPivots = sanitized.snapToPivots; + settingsMgr->fGridSnapAnchorPivot = sanitized.snapAnchorPivot; + settingsMgr->fGridSnapAnchorBounds = sanitized.snapAnchorBounds; + settingsMgr->fGridSnapAnchorNodes = sanitized.snapAnchorNodes; + settingsMgr->fGridSnappingActive = mSnappingActive; + settingsMgr->saveKeyToFile("gridColor"); + settingsMgr->saveKeyToFile("gridMajorColor"); + settingsMgr->saveKeyToFile("gridDrawOnTop"); + settingsMgr->saveKeyToFile("gridSnapToCanvas"); + settingsMgr->saveKeyToFile("gridSnapToBoxes"); + settingsMgr->saveKeyToFile("gridSnapToNodes"); + settingsMgr->saveKeyToFile("gridSnapToPivots"); + settingsMgr->saveKeyToFile("gridSnapAnchorPivot"); + settingsMgr->saveKeyToFile("gridSnapAnchorBounds"); + settingsMgr->saveKeyToFile("gridSnapAnchorNodes"); + settingsMgr->saveKeyToFile("gridSnappingActive"); + } + saveGridSettingsToSettings(sanitized); +} + +void Document::applyGridSettings(const GridSettings& settings, + const bool silent, + const bool skipSave) +{ + const GridSettings sanitized = sanitizedGridSettings(settings); + const auto previous = mGridController.settings; + if (previous == sanitized) { return; } + + const bool snapChanged = previous.enabled != sanitized.enabled; + const bool showChanged = previous.show != sanitized.show; + const bool metricsChanged = + gridNearlyEqual(previous.sizeX, sanitized.sizeX) == false || + gridNearlyEqual(previous.sizeY, sanitized.sizeY) == false || + gridNearlyEqual(previous.originX, sanitized.originX) == false || + gridNearlyEqual(previous.originY, sanitized.originY) == false || + previous.majorEveryX != sanitized.majorEveryX || + previous.majorEveryY != sanitized.majorEveryY; + const QColor previousColor = previous.colorAnimator ? previous.colorAnimator->getColor() : QColor(); + const QColor sanitizedColor = sanitized.colorAnimator ? sanitized.colorAnimator->getColor() : QColor(); + const QColor previousMajorColor = previous.majorColorAnimator ? previous.majorColorAnimator->getColor() : QColor(); + const QColor sanitizedMajorColor = sanitized.majorColorAnimator ? sanitized.majorColorAnimator->getColor() : QColor(); + const bool colorChanged = previousColor != sanitizedColor || + previousMajorColor != sanitizedMajorColor; + const bool orderChanged = previous.drawOnTop != sanitized.drawOnTop; + + mGridController.settings = sanitized; + + if (!skipSave) { + saveGridSettingsToSettings(mGridController.settings); + } + + if (silent) { return; } + + emit gridSettingsChanged(mGridController.settings); + if (snapChanged) { + emit gridSnapEnabledChanged(mGridController.settings.enabled); + } + + if (showChanged || (mGridController.settings.show && (metricsChanged || colorChanged || orderChanged))) { + updateScenes(); + } +} + + Clipboard *Document::getClipboard(const ClipboardType type) const { if(!fClipboardContainer) return nullptr; if(type == fClipboardContainer->getType()) @@ -377,6 +664,8 @@ void Document::clear() { removeBookmarkColor(color); } fColors.clear(); + + loadGridSettingsFromSettings(); } void Document::SWT_setupAbstraction(SWT_Abstraction * const abstraction, diff --git a/src/core/Private/document.h b/src/core/Private/document.h index 729f88fe2..24761fce7 100644 --- a/src/core/Private/document.h +++ b/src/core/Private/document.h @@ -46,6 +46,7 @@ #include "ReadWrite/ewritestream.h" #include "gizmos.h" #include "appsupport.h" +#include "gridcontroller.h" class SceneBoundGradient; class FileDataCacheHandler; @@ -70,6 +71,16 @@ class CORE_EXPORT Document : public SingleWidgetTarget { static Document* sInstance; + Friction::Core::GridController& gridController(); + const Friction::Core::GridController& gridController() const; + + void setGridSnapEnabled(bool enabled); + void setGridVisible(bool visible); + void setSnappingActive(bool active); + bool isSnappingActive() const; + void setGridSettings(const Friction::Core::GridSettings& settings); + void saveGridSettingsAsDefault(const Friction::Core::GridSettings& settings); + stdsptr fClipboardContainer; QString fEvFile; @@ -109,6 +120,8 @@ class CORE_EXPORT Document : public SingleWidgetTarget { bool fOnionVisible = false; PaintMode fPaintMode = PaintMode::normal; + Friction::Core::GridController mGridController; + QList> fScenes; std::map fVisibleScenes; ConnContextPtr fActiveScene; @@ -198,9 +211,20 @@ class CORE_EXPORT Document : public SingleWidgetTarget { void writeBookmarked(eWriteStream &dst) const; void readBookmarked(eReadStream &src); + void writeGridSettings(eWriteStream &dst) const; + void readGridSettings(eReadStream &src); + void readGridSettings(const QDomElement& element); + void loadGridSettingsFromSettings(); + void saveGridSettingsToSettings(const Friction::Core::GridSettings& settings) const; + void applyGridSettings(const Friction::Core::GridSettings& settings, + bool silent, + bool skipSave); void readGradients(eReadStream& src); signals: + void gridSettingsChanged(const Friction::Core::GridSettings& settings); + void gridSnapEnabledChanged(bool enabled); + void snappingActiveChanged(bool active); void canvasModeSet(CanvasMode); void gizmoVisibilityChanged(const Friction::Core::Gizmos::Interact &ti, @@ -237,6 +261,8 @@ class CORE_EXPORT Document : public SingleWidgetTarget { void openApplyExpressionDialog(QrealAnimator* const target); void newVideo(const VideoBox::VideoSpecs specs); void currentPixelColor(const QColor &color); +private: + bool mSnappingActive = false; }; #endif // DOCUMENT_H diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 9fa04b075..2773f2f52 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -32,7 +32,13 @@ #include "Paint/brushescontext.h" #include "simpletask.h" #include "canvas.h" +#include "gridcontroller.h" +#include "smartPointers/ememory.h" #include "appsupport.h" +#include +#include + +using Friction::Core::GridSettings; void Document::writeBookmarked(eWriteStream &dst) const { dst << fColors.count(); @@ -46,7 +52,29 @@ void Document::writeBookmarked(eWriteStream &dst) const { } } +void Document::writeGridSettings(eWriteStream &dst) const +{ + const auto& s = mGridController.settings; + dst << s.sizeX; + dst << s.sizeY; + dst << s.originX; + dst << s.originY; + dst << s.snapThresholdPx; + dst << s.enabled; + dst << s.show; + dst << s.majorEveryX; + dst << s.majorEveryY; + const QColor color = s.colorAnimator ? s.colorAnimator->getColor() : Friction::Core::GridSettings::defaults().colorAnimator->getColor(); + const QColor majorColor = s.majorColorAnimator ? s.majorColorAnimator->getColor() : Friction::Core::GridSettings::defaults().majorColorAnimator->getColor(); + dst << color; + dst << majorColor; +} + + void Document::writeScenes(eWriteStream& dst) const { + writeGridSettings(dst); + dst.writeCheckpoint(); + writeBookmarked(dst); dst.writeCheckpoint(); @@ -72,6 +100,38 @@ void Document::readBookmarked(eReadStream &src) { } } +void Document::readGridSettings(eReadStream &src) +{ + GridSettings settings = mGridController.settings; + src >> settings.sizeX; + src >> settings.sizeY; + src >> settings.originX; + src >> settings.originY; + src >> settings.snapThresholdPx; + bool enabled = mGridController.settings.enabled; + bool show = mGridController.settings.show; + src >> enabled; + src >> show; + src >> settings.majorEveryX; + src >> settings.majorEveryY; + QColor color; + src >> color; + QColor majorColor = color; + src >> majorColor; + settings.enabled = enabled; + settings.show = show; + if (!settings.colorAnimator) { + settings.colorAnimator = enve::make_shared(); + } + settings.colorAnimator->setColor(color); + if (!settings.majorColorAnimator) { + settings.majorColorAnimator = enve::make_shared(); + } + settings.majorColorAnimator->setColor(majorColor); + applyGridSettings(settings, false, true); +} + + void Document::readGradients(eReadStream& src) { int nGrads; src >> nGrads; for(int i = 0; i < nGrads; i++) { @@ -80,6 +140,10 @@ void Document::readGradients(eReadStream& src) { } void Document::readScenes(eReadStream& src) { + if (src.evFileVersion() >= EvFormat::grids) { + readGridSettings(src); + src.readCheckpoint("Error reading grid settings"); + } if(src.evFileVersion() > 1) { readBookmarked(src); src.readCheckpoint("Error reading bookmarks"); @@ -106,6 +170,65 @@ void Document::readScenes(eReadStream& src) { SimpleTask::sProcessAll(); } +void Document::readGridSettings(const QDomElement& element) +{ + if (element.isNull()) { return; } + GridSettings settings = mGridController.settings; + bool ok = false; + settings.sizeX = element.attribute("sizeX", QString::number(settings.sizeX)).toDouble(&ok); + settings.sizeY = element.attribute("sizeY", QString::number(settings.sizeY)).toDouble(&ok); + settings.originX = element.attribute("originX", QString::number(settings.originX)).toDouble(&ok); + settings.originY = element.attribute("originY", QString::number(settings.originY)).toDouble(&ok); + settings.snapThresholdPx = element.attribute("snapThresholdPx", QString::number(settings.snapThresholdPx)).toInt(); + settings.enabled = element.attribute("enabled", settings.enabled ? "true" : "false") == "true"; + settings.show = element.attribute("show", settings.show ? "true" : "false") == "true"; + auto parseMajorAttr = [](const QDomElement& elem, + const QString& attribute, + int currentValue, + bool& applied) + { + applied = false; + if (!elem.hasAttribute(attribute)) { return currentValue; } + bool okValue = false; + const int parsed = elem.attribute(attribute).toInt(&okValue); + if (okValue && parsed > 0) { + applied = true; + return parsed; + } + return currentValue; + }; + bool appliedMajorX = false; + bool appliedMajorY = false; + settings.majorEveryX = parseMajorAttr(element, "majorEveryX", settings.majorEveryX, appliedMajorX); + settings.majorEveryY = parseMajorAttr(element, "majorEveryY", settings.majorEveryY, appliedMajorY); + if ((!appliedMajorX || !appliedMajorY) && element.hasAttribute("majorEvery")) { + bool okLegacy = false; + const int legacy = element.attribute("majorEvery").toInt(&okLegacy); + if (okLegacy && legacy > 0) { + if (!appliedMajorX) { settings.majorEveryX = legacy; } + if (!appliedMajorY) { settings.majorEveryY = legacy; } + } + } + const QString colorStr = element.attribute("color"); + if (!colorStr.isEmpty()) { + const QColor parsed(colorStr); + if (parsed.isValid()) { + if (!settings.colorAnimator) { settings.colorAnimator = enve::make_shared(); } + settings.colorAnimator->setColor(parsed); + } + } + const QString majorColorStr = element.attribute("majorColor"); + if (!majorColorStr.isEmpty()) { + const QColor parsed(majorColorStr); + if (parsed.isValid()) { + if (!settings.majorColorAnimator) { settings.majorColorAnimator = enve::make_shared(); } + settings.majorColorAnimator->setColor(parsed); + } + } + applyGridSettings(settings, false, true); +} + + void Document::writeDoxumentXEV(QDomDocument& doc) const { auto document = doc.createElement("Document"); document.setAttribute("format-version", XevFormat::version); @@ -125,6 +248,23 @@ void Document::writeDoxumentXEV(QDomDocument& doc) const { } document.appendChild(bBrushes); + auto gridSettings = doc.createElement("GridSettings"); + const auto& grid = mGridController.settings; + gridSettings.setAttribute("sizeX", QString::number(grid.sizeX)); + gridSettings.setAttribute("sizeY", QString::number(grid.sizeY)); + gridSettings.setAttribute("originX", QString::number(grid.originX)); + gridSettings.setAttribute("originY", QString::number(grid.originY)); + gridSettings.setAttribute("snapThresholdPx", QString::number(grid.snapThresholdPx)); + gridSettings.setAttribute("enabled", grid.enabled ? "true" : "false"); + gridSettings.setAttribute("show", grid.show ? "true" : "false"); + gridSettings.setAttribute("majorEveryX", QString::number(grid.majorEveryX)); + gridSettings.setAttribute("majorEveryY", QString::number(grid.majorEveryY)); + const QColor gridColor = grid.colorAnimator ? grid.colorAnimator->getColor() : Friction::Core::GridSettings::defaults().colorAnimator->getColor(); + const QColor gridMajorColor = grid.majorColorAnimator ? grid.majorColorAnimator->getColor() : Friction::Core::GridSettings::defaults().majorColorAnimator->getColor(); + gridSettings.setAttribute("color", gridColor.name(QColor::HexArgb)); + gridSettings.setAttribute("majorColor", gridMajorColor.name(QColor::HexArgb)); + document.appendChild(gridSettings); + auto scenes = doc.createElement("Scenes"); for(const auto &s : fScenes) { auto scene = doc.createElement("Scene"); @@ -183,6 +323,11 @@ void Document::readDocumentXEV(const QDomDocument& doc, if(versionStr.isEmpty()) RuntimeThrow("No format version specified"); // const int version = XmlExportHelpers::stringToInt(versionStr); + const auto gridElement = document.firstChildElement("GridSettings"); + if (!gridElement.isNull()) { + readGridSettings(gridElement); + } + auto bColors = document.firstChildElement("ColorBookmarks"); const auto colors = bColors.elementsByTagName("Color"); const int nColors = colors.count(); diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index a35dbfb76..0c403c4c2 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -25,6 +25,8 @@ #include "esettings.h" +#include "gridcontroller.h" + #include "GUI/global.h" #include "exceptions.h" @@ -268,6 +270,51 @@ eSettings::eSettings(const int cpuThreads, fTimelineHighlightRowColor, "timelineHighlightRowColor", QColor(255, 0, 0, 15));*/ + // Grid default colors, TODO: move them to ThemeSupport + gSettings << std::make_shared( + fGridColor, + "gridColor", + Friction::Core::GridSettings::defaults().colorAnimator->getColor()); + gSettings << std::make_shared( + fGridMajorColor, + "gridMajorColor", + Friction::Core::GridSettings::defaults().majorColorAnimator->getColor()); + gSettings << std::make_shared( + fGridDrawOnTop, + "gridDrawOnTop", + true); + gSettings << std::make_shared( + fGridSnapToCanvas, + "gridSnapToCanvas", + false); + gSettings << std::make_shared( + fGridSnapToBoxes, + "gridSnapToBoxes", + false); + gSettings << std::make_shared( + fGridSnapToNodes, + "gridSnapToNodes", + false); + gSettings << std::make_shared( + fGridSnapToPivots, + "gridSnapToPivots", + false); + gSettings << std::make_shared( + fGridSnapAnchorPivot, + "gridSnapAnchorPivot", + true); + gSettings << std::make_shared( + fGridSnapAnchorBounds, + "gridSnapAnchorBounds", + true); + gSettings << std::make_shared( + fGridSnapAnchorNodes, + "gridSnapAnchorNodes", + false); + gSettings << std::make_shared( + fGridSnappingActive, + "gridSnappingActive", + false); gSettings << std::make_shared( fObjectKeyframeColor, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 7c4d92118..d8516e28a 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -34,6 +34,7 @@ #include "efiltersettings.h" #include "memorystructs.h" #include "appsupport.h" +#include "gridcontroller.h" #include "themesupport.h" #include "Expressions/expressionpresets.h" @@ -152,6 +153,19 @@ class CORE_EXPORT eSettings : public QObject bool fTimelineHighlightRow = true; QColor fTimelineHighlightRowColor = ThemeSupport::getThemeHighlightColor(15); + // Grid default colors + QColor fGridColor = Friction::Core::GridSettings::defaults().colorAnimator->getColor(); + QColor fGridMajorColor = Friction::Core::GridSettings::defaults().majorColorAnimator->getColor(); + bool fGridDrawOnTop = true; + bool fGridSnapToCanvas = false; + bool fGridSnapToBoxes = false; + bool fGridSnapToNodes = false; + bool fGridSnapToPivots = false; + bool fGridSnapAnchorPivot = true; + bool fGridSnapAnchorBounds = true; + bool fGridSnapAnchorNodes = false; + bool fGridSnappingActive = false; + QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; QColor fPropertyKeyframeColor; diff --git a/src/core/ReadWrite/evformat.h b/src/core/ReadWrite/evformat.h index 5d45c54ff..989dc8277 100644 --- a/src/core/ReadWrite/evformat.h +++ b/src/core/ReadWrite/evformat.h @@ -46,6 +46,7 @@ namespace EvFormat { formatOptions2 = 31, subPathOffset = 32, avStretch = 33, + grids = 34, nextVersion }; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index a2a5ae8cf..abb99daa5 100644 --- a/src/core/canvas.cpp +++ b/src/core/canvas.cpp @@ -101,6 +101,16 @@ Canvas::Canvas(Document &document, //setCanvasMode(MOVE_PATH); } +void Canvas::setWorldToScreen(const QTransform& transform, qreal devicePixelRatio) +{ + mWorldToScreen = transform; + mDevicePixelRatio = devicePixelRatio > 0.0 ? devicePixelRatio : 1.0; + bool invertible = false; + mScreenToWorld = transform.inverted(&invertible); + mHasWorldToScreen = invertible; +} + + Canvas::~Canvas() { clearPointsSelection(); @@ -254,6 +264,27 @@ void Canvas::renderSk(SkCanvas* const canvas, const float intervals[2] = {eSizesUI::widget*0.25f*invZoom, eSizesUI::widget*0.25f*invZoom}; const auto dashPathEffect = SkDashPathEffect::Make(intervals, 2, 0); + QTransform worldToScreenTransform = mWorldToScreen; + QTransform screenToWorldTransform = mScreenToWorld; + bool haveWorldTransform = mHasWorldToScreen; + if (!haveWorldTransform) { + bool invertible = false; + worldToScreenTransform = QTransform(viewTrans.m11(), viewTrans.m12(), 0.0, + viewTrans.m21(), viewTrans.m22(), 0.0, + viewTrans.dx(), viewTrans.dy(), 1.0); + screenToWorldTransform = worldToScreenTransform.inverted(&invertible); + haveWorldTransform = invertible; + } + const QRectF worldViewport = haveWorldTransform + ? screenToWorldTransform.mapRect(QRectF(drawRect)) + : QRectF(QPointF(0.0, 0.0), QSizeF(drawRect.width(), drawRect.height())); + QRectF gridViewport = worldViewport.normalized(); + const qreal gridPixelRatio = haveWorldTransform ? mDevicePixelRatio : pixelRatio; + const auto& gridSettings = mDocument.gridController().settings; + const bool gridVisible = gridSettings.show && + (!haveWorldTransform || !gridViewport.isEmpty()); + const bool gridOnTop = gridSettings.drawOnTop; + const bool drawCanvas = mSceneFrame && mSceneFrame->fBoxState == mStateId; canvas->concat(skViewTrans); if(isPreviewingOrRendering()) { @@ -272,7 +303,6 @@ void Canvas::renderSk(SkCanvas* const canvas, canvas->save(); if(mClipToCanvasSize) { canvas->clear(SK_ColorBLACK); - canvas->clipRect(canvasRect); } else { canvas->clear(ThemeSupport::getThemeBaseSkColor()); paint.setColor(SK_ColorGRAY); @@ -280,20 +310,29 @@ void Canvas::renderSk(SkCanvas* const canvas, paint.setPathEffect(dashPathEffect); canvas->drawRect(toSkRect(getCurrentBounds()), paint); } - const bool drawCanvas = mSceneFrame && mSceneFrame->fBoxState == mStateId; - if(bgColor.alpha() != 255) - drawTransparencyMesh(canvas, canvasRect); - if(!mClipToCanvasSize || !drawCanvas) { - canvas->saveLayer(nullptr, nullptr); if(bgColor.alpha() == 255 && skViewTrans.mapRect(canvasRect).contains(toSkRect(drawRect))) { canvas->clear(toSkColor(bgColor)); } else { - paint.setStyle(SkPaint::kFill_Style); - paint.setColor(toSkColor(bgColor)); - canvas->drawRect(canvasRect, paint); + SkPaint bgPaint; + bgPaint.setStyle(SkPaint::kFill_Style); + bgPaint.setColor(toSkColor(bgColor)); + canvas->drawRect(canvasRect, bgPaint); } + } + if (gridVisible && !gridOnTop) { + mDocument.gridController().drawGrid(canvas, gridViewport, worldToScreenTransform, gridPixelRatio); + } + canvas->save(); + if(mClipToCanvasSize) { + canvas->clipRect(canvasRect); + } + if(bgColor.alpha() != 255) + drawTransparencyMesh(canvas, canvasRect); + + if(!mClipToCanvasSize || !drawCanvas) { + canvas->saveLayer(nullptr, nullptr); drawContained(canvas, filter); canvas->restore(); } else if(drawCanvas) { @@ -303,8 +342,12 @@ void Canvas::renderSk(SkCanvas* const canvas, mSceneFrame->drawImage(canvas, filter); canvas->restore(); } - canvas->restore(); + canvas->restore(); + + if (gridVisible && gridOnTop) { + mDocument.gridController().drawGrid(canvas, gridViewport, worldToScreenTransform, gridPixelRatio); + } if (!enve_cast(mCurrentContainer)) { mCurrentContainer->drawBoundingRect(canvas, invZoom); diff --git a/src/core/canvas.h b/src/core/canvas.h index b1ae35d5c..b01383b97 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -46,6 +46,8 @@ #include #include #include +#include +#include #include "gizmos.h" @@ -187,6 +189,7 @@ class CORE_EXPORT Canvas : public CanvasBase const bool startTrans); qreal getResolution() const; + void setWorldToScreen(const QTransform& transform, qreal devicePixelRatio); void setResolution(const qreal percent); void applyCurrentTransformToSelected(); @@ -808,6 +811,12 @@ class CORE_EXPORT Canvas : public CanvasBase void setAxisGizmoHover(Friction::Core::Gizmos::AxisConstraint axis, bool hovered); + QPointF snapPosToGrid(const QPointF& pos, + Qt::KeyboardModifiers modifiers, + bool forceSnap) const; + QPointF snapEventPos(const eMouseEvent& e, + bool forceSnap) const; + void drawPathClear(); void drawPathFinish(const qreal invScale); @@ -824,6 +833,15 @@ class CORE_EXPORT Canvas : public CanvasBase protected: Document& mDocument; + QTransform mWorldToScreen; + QTransform mScreenToWorld; + bool mHasWorldToScreen = false; + qreal mDevicePixelRatio = 1.0; + QPointF mGridMoveStartPivot; + std::vector mGridSnapAnchorOffsets; + bool mHasCreationPressPos = false; + QPointF mCreationPressPos; + bool mDrawnSinceQue = true; qsptr mUndoRedoStack; @@ -932,6 +950,14 @@ class CORE_EXPORT Canvas : public CanvasBase QPointF getMoveByValueForEvent(const eMouseEvent &e); void cancelCurrentTransform(); void cancelCurrentTransformGimzos(); + + void collectSnapTargets(bool includePivots, + bool includeBounds, + bool includeNodes, + std::vector& pivotTargets, + std::vector& boxTargets, + std::vector& nodeTargets, + bool includeSelectedBounds = false) const; }; #endif // CANVAS_H diff --git a/src/core/canvashandlesmartpath.cpp b/src/core/canvashandlesmartpath.cpp index edc15603f..cee745bb1 100644 --- a/src/core/canvashandlesmartpath.cpp +++ b/src/core/canvashandlesmartpath.cpp @@ -27,6 +27,7 @@ #include "MovablePoints/smartnodepoint.h" #include "Boxes/smartvectorpath.h" #include "eevent.h" +#include "Private/document.h" void Canvas::clearCurrentSmartEndPoint() { setCurrentSmartEndPoint(nullptr); @@ -59,14 +60,16 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { mCurrentContainer->addContained(newPath); clearBoxesSelection(); addBoxToSelection(newPath.get()); - const auto relPos = newPath->mapAbsPosToRel(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + const auto relPos = newPath->mapAbsPosToRel(snappedPos); newPath->getBoxTransformAnimator()->setPosition(relPos.x(), relPos.y()); const auto newHandler = newPath->getPathAnimator(); const auto node = newHandler->createNewSubPathAtRelPos({0, 0}); setCurrentSmartEndPoint(node); } else { if(!nodePointUnderMouse) { - const auto newPoint = mLastEndPoint->actionAddPointAbsPos(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + const auto newPoint = mLastEndPoint->actionAddPointAbsPos(snappedPos); //newPoint->startTransform(); setCurrentSmartEndPoint(newPoint); } else if(!mLastEndPoint) { @@ -90,11 +93,12 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { void Canvas::handleAddSmartPointMouseMove(const eMouseEvent &e) { if(!mLastEndPoint) return; if(mStartTransform) mLastEndPoint->startTransform(); + const QPointF snappedPos = snapEventPos(e, false); if(mLastEndPoint->hasNextNormalPoint() && mLastEndPoint->hasPrevNormalPoint()) { mLastEndPoint->setCtrlsMode(CtrlsMode::corner); mLastEndPoint->setC0Enabled(true); - mLastEndPoint->moveC0ToAbsPos(e.fPos); + mLastEndPoint->moveC0ToAbsPos(snappedPos); } else { if(!mLastEndPoint->hasNextNormalPoint() && !mLastEndPoint->hasPrevNormalPoint()) { @@ -104,9 +108,9 @@ void Canvas::handleAddSmartPointMouseMove(const eMouseEvent &e) { mLastEndPoint->setCtrlsMode(CtrlsMode::symmetric); } if(mLastEndPoint->hasNextNormalPoint()) { - mLastEndPoint->moveC0ToAbsPos(e.fPos); + mLastEndPoint->moveC0ToAbsPos(snappedPos); } else { - mLastEndPoint->moveC2ToAbsPos(e.fPos); + mLastEndPoint->moveC2ToAbsPos(snappedPos); } } } diff --git a/src/core/canvasmouseevents.cpp b/src/core/canvasmouseevents.cpp index c7e3bc9ee..b76a411ce 100644 --- a/src/core/canvasmouseevents.cpp +++ b/src/core/canvasmouseevents.cpp @@ -145,19 +145,28 @@ void Canvas::mouseMoveEvent(const eMouseEvent &e) } else if(mCurrentMode == CanvasMode::pathCreate) { handleAddSmartPointMouseMove(e); } else if(mCurrentMode == CanvasMode::circleCreate) { + const QPointF anchor = mHasCreationPressPos + ? mCreationPressPos + : snapPosToGrid(e.fLastPressPos, e.fModifiers, false); + const QPointF current = snapEventPos(e, false); + const QPointF delta = current - anchor; if(e.shiftMod()) { - const qreal lenR = pointToLen(e.fPos - e.fLastPressPos); + const qreal lenR = pointToLen(delta); mCurrentCircle->moveRadiusesByAbs({lenR, lenR}); } else { - mCurrentCircle->moveRadiusesByAbs(e.fPos - e.fLastPressPos); + mCurrentCircle->moveRadiusesByAbs(delta); } } else if(mCurrentMode == CanvasMode::rectCreate) { + const QPointF anchor = mHasCreationPressPos + ? mCreationPressPos + : snapPosToGrid(e.fLastPressPos, e.fModifiers, false); + const QPointF current = snapEventPos(e, false); + const QPointF trans = current - anchor; if(e.shiftMod()) { - const QPointF trans = e.fPos - e.fLastPressPos; const qreal valF = qMax(trans.x(), trans.y()); mCurrentRectangle->moveSizePointByAbs({valF, valF}); } else { - mCurrentRectangle->moveSizePointByAbs(e.fPos - e.fLastPressPos); + mCurrentRectangle->moveSizePointByAbs(trans); } } } @@ -217,6 +226,9 @@ void Canvas::mouseReleaseEvent(const eMouseEvent &e) mPressedBox = nullptr; mHoveredPoint_d = mPressedPoint; mPressedPoint = nullptr; + if (e.fButton == Qt::LeftButton) { + mHasCreationPressPos = false; + } } #include "MovablePoints/smartnodepoint.h" diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 205219c22..8fc4dea8a 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -30,6 +30,7 @@ #include "Private/document.h" #include "GUI/dialogsinterface.h" +#include "Boxes/boundingbox.h" #include "Boxes/circle.h" #include "Boxes/rectangle.h" #include "Boxes/imagebox.h" @@ -51,6 +52,9 @@ #include "MovablePoints/smartnodepoint.h" #include "MovablePoints/pathpivot.h" +#include +#include + #include #include #include @@ -60,6 +64,159 @@ using namespace Friction::Core; +void Canvas::collectSnapTargets(bool includePivots, + bool includeBounds, + bool includeNodes, + std::vector& pivotTargets, + std::vector& boxTargets, + std::vector& nodeTargets, + bool includeSelectedBounds) const +{ + pivotTargets.clear(); + boxTargets.clear(); + nodeTargets.clear(); + + if ((!includePivots && !includeBounds && !includeNodes) || !mCurrentContainer) { + return; + } + + auto addIfValid = [](std::vector& target, const QPointF& pt) { + if (isPointFinite(pt)) { + target.push_back(pt); + } + }; + + auto appendBoundsTargets = [&](const QRectF& rect) { + const QRectF normalized = rect.normalized(); + if (normalized.isNull() || !normalized.isValid()) { return; } + + const QPointF topLeft = normalized.topLeft(); + const QPointF topRight = normalized.topRight(); + const QPointF bottomLeft = normalized.bottomLeft(); + const QPointF bottomRight = normalized.bottomRight(); + const QPointF topCenter((normalized.left() + normalized.right()) * 0.5, normalized.top()); + const QPointF bottomCenter((normalized.left() + normalized.right()) * 0.5, normalized.bottom()); + const QPointF leftCenter(normalized.left(), (normalized.top() + normalized.bottom()) * 0.5); + const QPointF rightCenter(normalized.right(), (normalized.top() + normalized.bottom()) * 0.5); + + addIfValid(boxTargets, normalized.center()); + addIfValid(boxTargets, topLeft); + addIfValid(boxTargets, topRight); + addIfValid(boxTargets, bottomLeft); + addIfValid(boxTargets, bottomRight); + addIfValid(boxTargets, topCenter); + addIfValid(boxTargets, bottomCenter); + addIfValid(boxTargets, leftCenter); + addIfValid(boxTargets, rightCenter); + }; + + const std::function recurse = + [&](const ContainerBox* container, bool ancestorSelected) { + if (!container) { return; } + const auto& boxes = container->getContainedBoxes(); + for (auto* box : boxes) { + if (!box) { continue; } + const bool selectedBranch = ancestorSelected || box->isSelected(); + const bool visible = box->isVisible(); + + if (!selectedBranch && visible) { + if (includePivots) { + addIfValid(pivotTargets, box->getPivotAbsPos()); + } + if (includeBounds) { + appendBoundsTargets(box->getAbsBoundingRect()); + } + if (includeNodes) { + auto* mutableBox = const_cast(box); + const MovablePoint::PtOp gather = [&](MovablePoint* point) { + if (!point || !point->isSmartNodePoint()) { return; } + const auto* node = static_cast(point); + addIfValid(nodeTargets, node->getAbsolutePos()); + }; + mutableBox->selectAllCanvasPts(gather, CanvasMode::pointTransform); + } + } + + if (const auto* childContainer = enve_cast(box)) { + recurse(childContainer, selectedBranch); + } + } + }; + + recurse(mCurrentContainer, false); + + if (includeBounds && includeSelectedBounds) { + for (const auto& selectedBox : mSelectedBoxes) { + if (!selectedBox || !selectedBox->isVisible()) { continue; } + appendBoundsTargets(selectedBox->getAbsBoundingRect()); + } + } +} + +QPointF Canvas::snapPosToGrid(const QPointF& pos, + Qt::KeyboardModifiers modifiers, + bool forceSnap) const +{ + if (!mHasWorldToScreen) { return pos; } + + if (!mDocument.isSnappingActive()) { + return pos; + } + + const auto& gridController = mDocument.gridController(); + const auto& settings = gridController.settings; + + const bool bypassSnap = modifiers & Qt::AltModifier; + if (bypassSnap) { return pos; } + + const bool gridEnabled = settings.enabled; + const bool canvasSnapEnabled = settings.snapToCanvas; + const bool pivotsSnapEnabled = settings.snapToPivots; + const bool boxesSnapEnabled = settings.snapToBoxes; + const bool nodesSnapEnabled = settings.snapToNodes; + + std::vector pivotTargets; + std::vector boxTargets; + std::vector nodeTargets; + if (pivotsSnapEnabled || boxesSnapEnabled || nodesSnapEnabled) { + collectSnapTargets(pivotsSnapEnabled, boxesSnapEnabled, nodesSnapEnabled, + pivotTargets, boxTargets, nodeTargets); + } + const bool hasPivotTargets = pivotsSnapEnabled && !pivotTargets.empty(); + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + + const bool hasSnapSource = gridEnabled || canvasSnapEnabled || + hasPivotTargets || hasBoxTargets || hasNodeTargets; + const bool shouldForce = (forceSnap && hasSnapSource) || + (modifiers & Qt::ControlModifier); + + if (!hasSnapSource && !shouldForce) { return pos; } + + QRectF canvasRect; + const QRectF* canvasRectPtr = nullptr; + if (canvasSnapEnabled) { + canvasRect = QRectF(QPointF(0.0, 0.0), QSizeF(mWidth, mHeight)); + canvasRectPtr = &canvasRect; + } + + return gridController.maybeSnapPivot(pos, + mWorldToScreen, + shouldForce, + false, + canvasRectPtr, + nullptr, + hasPivotTargets ? &pivotTargets : nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); +} + +QPointF Canvas::snapEventPos(const eMouseEvent& e, + bool forceSnap) const +{ + return snapPosToGrid(e.fPos, e.fModifiers, forceSnap); +} + void Canvas::handleMovePathMousePressEvent(const eMouseEvent& e) { mPressedBox = mCurrentContainer->getBoxAt(e.fPos); if(e.shiftMod()) return; @@ -208,6 +365,7 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { mDoubleClick = false; //mMovesToSkip = 2; mStartTransform = true; + mHasCreationPressPos = false; const qreal invScale = 1/e.fScale; const qreal invScaleUi = (qApp ? qApp->devicePixelRatio() : 1.0) * invScale; @@ -274,11 +432,14 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - newPath->setAbsolutePos(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + newPath->setAbsolutePos(snappedPos); clearBoxesSelection(); addBoxToSelection(newPath.get()); mCurrentCircle = newPath.get(); + mCreationPressPos = snappedPos; + mHasCreationPressPos = true; } else if(mCurrentMode == CanvasMode::nullCreate) { const auto newPath = enve::make_shared(); @@ -291,11 +452,14 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - newPath->setAbsolutePos(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + newPath->setAbsolutePos(snappedPos); clearBoxesSelection(); addBoxToSelection(newPath.get()); mCurrentRectangle = newPath.get(); + mCreationPressPos = snappedPos; + mHasCreationPressPos = true; } else if (mCurrentMode == CanvasMode::textCreate) { if (enve_cast(mHoveredBox)) { setCurrentBox(mHoveredBox); @@ -666,6 +830,28 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { if(mStartTransform) mCurrentNormalSegment.startPassThroughTransform(); mCurrentNormalSegment.makePassThroughAbs(e.fPos, mCurrentNormalSegmentT); } else { + const auto& gridSettings = mDocument.gridController().settings; + const bool snappingActive = mDocument.isSnappingActive(); + const bool pivotsSnapEnabled = snappingActive && gridSettings.snapToPivots; + const bool boxesSnapEnabled = snappingActive && gridSettings.snapToBoxes; + const bool nodesSnapEnabled = snappingActive && gridSettings.snapToNodes; + std::vector pivotTargets; + std::vector boxTargets; + std::vector nodeTargets; + const bool includeSelectedBounds = + boxesSnapEnabled && mPressedPoint && mPressedPoint->isPivotPoint(); + if (snappingActive && (pivotsSnapEnabled || boxesSnapEnabled || nodesSnapEnabled)) { + collectSnapTargets(pivotsSnapEnabled, boxesSnapEnabled, nodesSnapEnabled, + pivotTargets, boxTargets, nodeTargets, + includeSelectedBounds); + } + const bool hasPivotTargets = pivotsSnapEnabled && !pivotTargets.empty(); + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + const bool snapSourcesAvailable = snappingActive && (gridSettings.enabled || + gridSettings.snapToCanvas || + hasPivotTargets || hasBoxTargets || hasNodeTargets); + if(mPressedPoint) { addPointToSelection(mPressedPoint); const auto mods = QGuiApplication::queryKeyboardModifiers(); @@ -726,13 +912,77 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { } if(!mPressedPoint->selectionEnabled()) { - if(mStartTransform) mPressedPoint->startTransform(); - mPressedPoint->moveByAbs(getMoveByValueForEvent(e)); + if(mStartTransform) { + mPressedPoint->startTransform(); + mGridMoveStartPivot = mPressedPoint->getAbsolutePos(); + } + + QPointF moveBy = getMoveByValueForEvent(e); + const bool bypassSnap = e.fModifiers & Qt::AltModifier; + const bool forceSnap = e.fModifiers & Qt::ControlModifier; + + if(snappingActive && mHasWorldToScreen && + (snapSourcesAvailable || forceSnap)) { + const QPointF targetPos = mGridMoveStartPivot + moveBy; + QRectF canvasRect; + const QRectF* canvasPtr = nullptr; + if (gridSettings.snapToCanvas) { + canvasRect = QRectF(QPointF(0.0, 0.0), QSizeF(mWidth, mHeight)); + canvasPtr = &canvasRect; + } + const auto snapped = mDocument.gridController().maybeSnapPivot( + targetPos, + mWorldToScreen, + forceSnap, + bypassSnap, + canvasPtr, + nullptr, + hasPivotTargets ? &pivotTargets : nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); + if(snapped != targetPos) { + moveBy = snapped - mGridMoveStartPivot; + } + } + + mPressedPoint->moveByAbs(moveBy); return; } } - moveSelectedPointsByAbs(getMoveByValueForEvent(e), - mStartTransform); + + if(mStartTransform && !mSelectedPoints_d.isEmpty()) { + mGridMoveStartPivot = getSelectedPointsAbsPivotPos(); + } + + QPointF moveBy = getMoveByValueForEvent(e); + const bool bypassSnap = e.fModifiers & Qt::AltModifier; + const bool forceSnap = e.fModifiers & Qt::ControlModifier; + + if(!mSelectedPoints_d.isEmpty() && snappingActive && mHasWorldToScreen && + (snapSourcesAvailable || forceSnap)) { + const QPointF targetPivot = mGridMoveStartPivot + moveBy; + QRectF canvasRect; + const QRectF* canvasPtr = nullptr; + if (gridSettings.snapToCanvas) { + canvasRect = QRectF(QPointF(0.0, 0.0), QSizeF(mWidth, mHeight)); + canvasPtr = &canvasRect; + } + const auto snapped = mDocument.gridController().maybeSnapPivot( + targetPivot, + mWorldToScreen, + forceSnap, + bypassSnap, + canvasPtr, + nullptr, + hasPivotTargets ? &pivotTargets : nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); + if(snapped != targetPivot) { + moveBy = snapped - mGridMoveStartPivot; + } + } + + moveSelectedPointsByAbs(moveBy, mStartTransform); } } @@ -883,7 +1133,112 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { mPressedBox = nullptr; } - const auto moveBy = getMoveByValueForEvent(e); + const auto& gridSettings = mDocument.gridController().settings; + + if (mStartTransform && !mSelectedBoxes.isEmpty()) { + mGridMoveStartPivot = getSelectedBoxesAbsPivotPos(); + + mGridSnapAnchorOffsets.clear(); + if (gridSettings.snapAnchorPivot) { + mGridSnapAnchorOffsets.emplace_back(QPointF(0.0, 0.0)); + } + + QRectF combinedRect; + bool hasRect = false; + for (const auto& box : mSelectedBoxes) { + const QRectF rect = box->getAbsBoundingRect(); + if (rect.width() < 0.0 || rect.height() < 0.0) { + continue; + } + if (!hasRect) { + combinedRect = rect; + hasRect = true; + } else { + combinedRect = combinedRect.united(rect); + } + } + + if (hasRect && gridSettings.snapAnchorBounds) { + const QPointF topLeft = combinedRect.topLeft(); + const QPointF topRight = combinedRect.topRight(); + const QPointF bottomLeft = combinedRect.bottomLeft(); + const QPointF bottomRight = combinedRect.bottomRight(); + const QPointF topCenter((topLeft.x() + topRight.x()) * 0.5, topLeft.y()); + const QPointF bottomCenter((bottomLeft.x() + bottomRight.x()) * 0.5, bottomLeft.y()); + const QPointF leftCenter(topLeft.x(), (topLeft.y() + bottomLeft.y()) * 0.5); + const QPointF rightCenter(topRight.x(), (topRight.y() + bottomRight.y()) * 0.5); + const QPointF center = combinedRect.center(); + + mGridSnapAnchorOffsets.emplace_back(topLeft - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(topRight - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(bottomLeft - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(bottomRight - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(topCenter - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(bottomCenter - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(leftCenter - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(rightCenter - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(center - mGridMoveStartPivot); + } + + if (gridSettings.snapAnchorNodes) { + const MovablePoint::PtOp gatherOffsets = [&](MovablePoint* point) { + if (!point || !point->isSmartNodePoint()) { return; } + const auto* node = static_cast(point); + mGridSnapAnchorOffsets.emplace_back(node->getAbsolutePos() - mGridMoveStartPivot); + }; + for (const auto& box : mSelectedBoxes) { + if (!box) { continue; } + auto* mutableBox = const_cast(box); + mutableBox->selectAllCanvasPts(gatherOffsets, CanvasMode::pointTransform); + } + } + } + + auto moveBy = getMoveByValueForEvent(e); + const bool bypassSnap = e.fModifiers & Qt::AltModifier; + const bool forceSnap = e.fModifiers & Qt::ControlModifier; + const bool hasAnchorOffsets = !mGridSnapAnchorOffsets.empty(); + const bool snappingActive = mDocument.isSnappingActive(); + const bool pivotsSnapEnabled = snappingActive && gridSettings.snapToPivots; + const bool boxesSnapEnabled = snappingActive && gridSettings.snapToBoxes; + const bool nodesSnapEnabled = snappingActive && gridSettings.snapToNodes; + std::vector pivotTargets; + std::vector boxTargets; + std::vector nodeTargets; + if (snappingActive && (pivotsSnapEnabled || boxesSnapEnabled || nodesSnapEnabled)) { + collectSnapTargets(pivotsSnapEnabled, boxesSnapEnabled, nodesSnapEnabled, + pivotTargets, boxTargets, nodeTargets); + } + const bool hasPivotTargets = pivotsSnapEnabled && !pivotTargets.empty(); + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + const bool snapSourcesAvailable = snappingActive && (gridSettings.enabled || + gridSettings.snapToCanvas || + hasPivotTargets || hasBoxTargets || hasNodeTargets || + hasAnchorOffsets); + if (!mSelectedBoxes.isEmpty() && snappingActive && mHasWorldToScreen && + (snapSourcesAvailable || forceSnap)) { + const QPointF targetPivot = mGridMoveStartPivot + moveBy; + QRectF canvasRect; + const QRectF* canvasPtr = nullptr; + if (gridSettings.snapToCanvas) { + canvasRect = QRectF(QPointF(0.0, 0.0), QSizeF(mWidth, mHeight)); + canvasPtr = &canvasRect; + } + const auto snapped = mDocument.gridController().maybeSnapPivot(targetPivot, + mWorldToScreen, + forceSnap, + bypassSnap, + canvasPtr, + &mGridSnapAnchorOffsets, + hasPivotTargets ? &pivotTargets : nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); + if (snapped != targetPivot) { + moveBy = snapped - mGridMoveStartPivot; + } + } + moveSelectedBoxesByAbs(moveBy, mStartTransform); } } diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp new file mode 100644 index 000000000..a731a9522 --- /dev/null +++ b/src/core/gridcontroller.cpp @@ -0,0 +1,466 @@ +/* +# +# Friction - https://friction.graphics +# +# Copyright (c) Ole-André Rodlie and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# See 'README.md' for more information. +# +*/ + +#include "gridcontroller.h" + +#include "skia/skqtconversions.h" + +#include "include/core/SkCanvas.h" +#include "include/core/SkPaint.h" +#include "include/core/SkPoint.h" + +#include +#include +#include + +#include + +#include +#include +#include + +using namespace Friction::Core; + +namespace { + +double clampToRange(double value, double minValue, double maxValue) +{ + if (value < minValue) { return minValue; } + if (value > maxValue) { return maxValue; } + return value; +} + +bool nearlyEqual(double lhs, double rhs) +{ + constexpr double eps = 1e-6; + return std::abs(lhs - rhs) <= eps; +} + +GridSettings sanitizeSettings(const GridSettings& in) +{ + GridSettings copy = in; + if (copy.sizeX <= 0.0) { copy.sizeX = 1.0; } + if (copy.sizeY <= 0.0) { copy.sizeY = 1.0; } + if (copy.majorEveryX < 1) { copy.majorEveryX = 1; } + if (copy.majorEveryY < 1) { copy.majorEveryY = 1; } + if (copy.snapThresholdPx < 0) { copy.snapThresholdPx = 0; } + auto ensureAnimatorColor = [](qsptr& animator, + const QColor& fallback) + { + if (!animator) { + animator = enve::make_shared(); + } + QColor color = animator->getColor(); + if (!color.isValid()) { + color = fallback; + } + const double alpha = clampToRange(static_cast(color.alpha()), 0.0, 255.0); + color.setAlpha(static_cast(alpha)); + animator->setColor(color); + return color; + }; + const auto& defaults = GridSettings::defaults(); + const QColor minorFallback = defaults.colorAnimator->getColor(); + const QColor majorFallback = defaults.majorColorAnimator->getColor(); + ensureAnimatorColor(copy.colorAnimator, minorFallback); + ensureAnimatorColor(copy.majorColorAnimator, majorFallback); + return copy; +} + +QColor scaledAlpha(const QColor& base, double factor) +{ + QColor c = base; + factor = clampToRange(factor, 0.0, 1.0); + c.setAlphaF(c.alphaF() * factor); + return c; +} + +double lineSpacingPx(const QTransform& worldToScreen, + qreal devicePixelRatio, + const QPointF& delta) +{ + const QPointF origin = worldToScreen.map(QPointF(0.0, 0.0)); + const QPointF mapped = worldToScreen.map(delta); + return QLineF(origin, mapped).length() * devicePixelRatio; +} + +double effectiveScale(const QTransform& worldToScreen) +{ + const double sx = std::hypot(worldToScreen.m11(), worldToScreen.m12()); + const double sy = std::hypot(worldToScreen.m21(), worldToScreen.m22()); + const double avg = (sx + sy) * 0.5; + return avg > 0.0 ? avg : 1.0; +} + +double fadeFactor(double spacingPx) +{ + constexpr double minVisible = 4.0; + constexpr double fullVisible = 16.0; + if (spacingPx <= minVisible) { return 0.0; } + if (spacingPx >= fullVisible) { return 1.0; } + return (spacingPx - minVisible) / (fullVisible - minVisible); +} + +} // namespace + +bool GridSettings::operator==(const GridSettings& other) const +{ + const QColor thisColor = colorAnimator ? colorAnimator->getColor() : QColor(); + const QColor otherColor = other.colorAnimator ? other.colorAnimator->getColor() : QColor(); + const QColor thisMajorColor = majorColorAnimator ? majorColorAnimator->getColor() : QColor(); + const QColor otherMajorColor = other.majorColorAnimator ? other.majorColorAnimator->getColor() : QColor(); + return nearlyEqual(sizeX, other.sizeX) && + nearlyEqual(sizeY, other.sizeY) && + nearlyEqual(originX, other.originX) && + nearlyEqual(originY, other.originY) && + snapThresholdPx == other.snapThresholdPx && + enabled == other.enabled && + show == other.show && + drawOnTop == other.drawOnTop && + snapToCanvas == other.snapToCanvas && + snapToBoxes == other.snapToBoxes && + snapToNodes == other.snapToNodes && + snapToPivots == other.snapToPivots && + snapAnchorPivot == other.snapAnchorPivot && + snapAnchorBounds == other.snapAnchorBounds && + snapAnchorNodes == other.snapAnchorNodes && + majorEveryX == other.majorEveryX && + majorEveryY == other.majorEveryY && + thisColor == otherColor && + thisMajorColor == otherMajorColor; +} + + + + +void GridController::drawGrid(QPainter* painter, + const QRectF& worldViewport, + const QTransform& worldToScreen, + const qreal devicePixelRatio) const +{ + const GridSettings sanitizedSettings = sanitizeSettings(settings); + if (!painter || !sanitizedSettings.show) { return; } + + const auto& defaults = GridSettings::defaults(); + const QColor minorBase = sanitizedSettings.colorAnimator + ? sanitizedSettings.colorAnimator->getColor() + : defaults.colorAnimator->getColor(); + const QColor majorBase = sanitizedSettings.majorColorAnimator + ? sanitizedSettings.majorColorAnimator->getColor() + : defaults.majorColorAnimator->getColor(); + + auto drawLine = [&](const QPointF& a, + const QPointF& b, + const bool major, + const Orientation orientation, + const double alphaFactor) + { + QPen pen(major ? majorBase : minorBase); + pen.setCosmetic(true); + pen.setColor(scaledAlpha(pen.color(), alphaFactor)); + painter->setPen(pen); + painter->drawLine(a, b); + Q_UNUSED(orientation) + }; + + forEachGridLine(worldViewport, worldToScreen, devicePixelRatio, drawLine); +} + +void GridController::drawGrid(SkCanvas* canvas, + const QRectF& worldViewport, + const QTransform& worldToScreen, + const qreal devicePixelRatio) const +{ + const GridSettings sanitizedSettings = sanitizeSettings(settings); + if (!canvas || !sanitizedSettings.show) { return; } + + const auto& defaults = GridSettings::defaults(); + const QColor minorBase = sanitizedSettings.colorAnimator + ? sanitizedSettings.colorAnimator->getColor() + : defaults.colorAnimator->getColor(); + const QColor majorBase = sanitizedSettings.majorColorAnimator + ? sanitizedSettings.majorColorAnimator->getColor() + : defaults.majorColorAnimator->getColor(); + const float strokeWidth = static_cast( + devicePixelRatio / effectiveScale(worldToScreen)); + + auto drawLine = [&](const QPointF& a, + const QPointF& b, + const bool major, + const Orientation orientation, + const double alphaFactor) + { + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setStrokeWidth(strokeWidth); + paint.setAntiAlias(false); + const QColor base = major ? majorBase : minorBase; + paint.setColor(toSkColor(scaledAlpha(base, alphaFactor))); + canvas->drawLine(SkPoint::Make(static_cast(a.x()), + static_cast(a.y())), + SkPoint::Make(static_cast(b.x()), + static_cast(b.y())), + paint); + Q_UNUSED(orientation) + }; + + forEachGridLine(worldViewport, worldToScreen, devicePixelRatio, drawLine); +} + +QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, + const QTransform& worldToScreen, + const bool forceSnap, + const bool bypassSnap, + const QRectF* canvasRectWorld, + const std::vector* anchorOffsets, + const std::vector* pivotTargets, + const std::vector* boxTargets, + const std::vector* nodeTargets) const +{ + const GridSettings sanitizedSettings = sanitizeSettings(settings); + + const bool hasPivotTargets = sanitizedSettings.snapToPivots && + pivotTargets && !pivotTargets->empty(); + const bool hasBoxTargets = sanitizedSettings.snapToBoxes && + boxTargets && !boxTargets->empty(); + const bool hasNodeTargets = sanitizedSettings.snapToNodes && + nodeTargets && !nodeTargets->empty(); + + const bool snapSourcesEnabled = sanitizedSettings.enabled || + (sanitizedSettings.snapToCanvas && canvasRectWorld) || + hasPivotTargets || hasBoxTargets || hasNodeTargets; + if ((!snapSourcesEnabled && !forceSnap) || bypassSnap) { + return pivotWorld; + } + + const double sizeX = sanitizedSettings.sizeX; + const double sizeY = sanitizedSettings.sizeY; + const bool hasGrid = sizeX > 0.0 && sizeY > 0.0; + + const bool canUseCanvas = sanitizedSettings.snapToCanvas && canvasRectWorld; + QRectF normalizedCanvas; + if (canUseCanvas) { + normalizedCanvas = canvasRectWorld->normalized(); + } + const bool hasCanvasTargets = canUseCanvas && !normalizedCanvas.isEmpty(); + + if (!hasGrid && !hasCanvasTargets && !hasPivotTargets && !hasBoxTargets && !hasNodeTargets) { + return pivotWorld; + } + + const std::vector* offsetsPtr = anchorOffsets; + std::vector fallbackOffsets; + if (!offsetsPtr) { + fallbackOffsets.emplace_back(QPointF(0.0, 0.0)); + offsetsPtr = &fallbackOffsets; + } + const auto& offsets = *offsetsPtr; + if (offsets.empty()) { + return pivotWorld; + } + + struct AnchorContext { + QPointF offset; + QPointF world; + QPointF screen; + }; + std::vector anchors; + anchors.reserve(offsets.size()); + for (const auto& offset : offsets) { + const QPointF worldPoint = pivotWorld + offset; + anchors.push_back({offset, worldPoint, worldToScreen.map(worldPoint)}); + } + + if (anchors.empty()) { + return pivotWorld; + } + + QPointF bestPivot = pivotWorld; + double bestDistance = std::numeric_limits::infinity(); + bool foundCandidate = false; + + auto considerCandidate = [&](const AnchorContext& anchor, + const QPointF& candidateAnchorWorld) + { + const QPointF candidatePivot = candidateAnchorWorld - anchor.offset; + const QPointF screenCandidate = worldToScreen.map(candidateAnchorWorld); + const double candidateDistance = QLineF(anchor.screen, screenCandidate).length(); + if (candidateDistance < bestDistance) { + bestDistance = candidateDistance; + bestPivot = candidatePivot; + foundCandidate = true; + } + }; + + if (hasGrid && (sanitizedSettings.enabled || forceSnap)) { + for (const auto& anchor : anchors) { + const double gx = sanitizedSettings.originX + + std::round((anchor.world.x() - sanitizedSettings.originX) / sizeX) * sizeX; + const double gy = sanitizedSettings.originY + + std::round((anchor.world.y() - sanitizedSettings.originY) / sizeY) * sizeY; + considerCandidate(anchor, QPointF(gx, gy)); + } + } + + if (hasCanvasTargets) { + const double left = normalizedCanvas.left(); + const double right = normalizedCanvas.right(); + const double top = normalizedCanvas.top(); + const double bottom = normalizedCanvas.bottom(); + const double midX = (left + right) * 0.5; + const double midY = (top + bottom) * 0.5; + + const QPointF canvasTargets[] = { + QPointF(left, top), + QPointF(right, top), + QPointF(left, bottom), + QPointF(right, bottom), + QPointF(midX, top), + QPointF(midX, bottom), + QPointF(left, midY), + QPointF(right, midY), + QPointF(midX, midY) + }; + + for (const auto& anchor : anchors) { + for (const auto& target : canvasTargets) { + considerCandidate(anchor, target); + } + } + } + + + if (hasPivotTargets) { + for (const auto& anchor : anchors) { + for (const auto& target : *pivotTargets) { + considerCandidate(anchor, target); + } + } + } + + if (hasBoxTargets) { + for (const auto& anchor : anchors) { + for (const auto& target : *boxTargets) { + considerCandidate(anchor, target); + } + } + } + + if (hasNodeTargets) { + for (const auto& anchor : anchors) { + for (const auto& target : *nodeTargets) { + considerCandidate(anchor, target); + } + } + } + + if (!foundCandidate) { + return pivotWorld; + } + + if (forceSnap) { + return bestPivot; + } + + if (bestDistance <= sanitizedSettings.snapThresholdPx) { + return bestPivot; + } + return pivotWorld; +} + +template +void GridController::forEachGridLine(const QRectF& viewport, + const QTransform& worldToScreen, + const qreal devicePixelRatio, + DrawLineFunc&& drawLine) const +{ + const GridSettings sanitizedSettings = sanitizeSettings(settings); + if (!sanitizedSettings.show) { return; } + + const double sizeX = sanitizedSettings.sizeX; + const double sizeY = sanitizedSettings.sizeY; + if (sizeX <= 0.0 || sizeY <= 0.0) { return; } + + QRectF baseView = viewport.normalized(); + if (!baseView.isValid() || baseView.isEmpty()) { return; } + const double expandX = std::max(baseView.width(), sizeX); + const double expandY = std::max(baseView.height(), sizeY); + QRectF view = baseView.adjusted(-expandX, -expandY, expandX, expandY); + + const int majorEveryX = std::max(1, sanitizedSettings.majorEveryX); + const int majorEveryY = std::max(1, sanitizedSettings.majorEveryY); + + const double spacingX = lineSpacingPx(worldToScreen, devicePixelRatio, {sizeX, 0.0}); + const double spacingY = lineSpacingPx(worldToScreen, devicePixelRatio, {0.0, sizeY}); + const double majorSpacingX = spacingX * majorEveryX; + const double majorSpacingY = spacingY * majorEveryY; + + const double majorAlphaX = fadeFactor(majorSpacingX); + const double majorAlphaY = fadeFactor(majorSpacingY); + + if (majorAlphaX <= 0.0 && majorAlphaY <= 0.0) { + return; + } + + const double minorAlphaX = fadeFactor(spacingX); + const double minorAlphaY = fadeFactor(spacingY); + + auto firstAligned = [](double start, + double origin, + double spacing) + { + const double steps = std::floor((start - origin) / spacing); + return origin + steps * spacing; + }; + + const double originX = sanitizedSettings.originX; + const double originY = sanitizedSettings.originY; + + const double xBegin = firstAligned(view.left(), originX, sizeX); + const double xEnd = view.right() + sizeX; + + for (double x = xBegin; x <= xEnd; x += sizeX) { + const long long index = static_cast( + std::llround((x - originX) / sizeX)); + const bool major = (index % majorEveryX) == 0; + const double alpha = major ? majorAlphaX : minorAlphaX; + if (!major && alpha <= 0.0) { continue; } + const QPointF top(x, view.top()); + const QPointF bottom(x, view.bottom()); + drawLine(top, bottom, major, Orientation::Vertical, alpha); + } + + const double yBegin = firstAligned(view.top(), originY, sizeY); + const double yEnd = view.bottom() + sizeY; + + for (double y = yBegin; y <= yEnd; y += sizeY) { + const long long index = static_cast( + std::llround((y - originY) / sizeY)); + const bool major = (index % majorEveryY) == 0; + const double alpha = major ? majorAlphaY : minorAlphaY; + if (!major && alpha <= 0.0) { continue; } + const QPointF left(view.left(), y); + const QPointF right(view.right(), y); + drawLine(left, right, major, Orientation::Horizontal, alpha); + } +} diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h new file mode 100644 index 000000000..704212cd4 --- /dev/null +++ b/src/core/gridcontroller.h @@ -0,0 +1,127 @@ +/* +# +# Friction - https://friction.graphics +# +# Copyright (c) Ole-André Rodlie and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# See 'README.md' for more information. +# +*/ + +#ifndef GRIDCONTROLLER_H +#define GRIDCONTROLLER_H + +#include "core_global.h" +#include "Animators/coloranimator.h" +#include "smartPointers/ememory.h" + + +#include +#include +#include +#include + +#include +#include + +class QPainter; +class SkCanvas; + +namespace Friction { +namespace Core { + +struct CORE_EXPORT GridSettings { + GridSettings() + : colorAnimator(enve::make_shared()) + , majorColorAnimator(enve::make_shared()) + { + colorAnimator->setColor(QColor(128, 127, 255, 75)); + majorColorAnimator->setColor(QColor(255, 127, 234, 125)); + } + + static const GridSettings& defaults(); + + double sizeX = 40.0; + double sizeY = 40.0; + double originX = 960.0; + double originY = 540.0; + int snapThresholdPx = 40; + bool enabled = false; + bool show = false; + bool drawOnTop = false; + bool snapToCanvas = false; + bool snapToBoxes = false; + bool snapToNodes = false; + bool snapToPivots = false; + bool snapAnchorPivot = true; + bool snapAnchorBounds = true; + bool snapAnchorNodes = false; + int majorEveryX = 8; + int majorEveryY = 8; + qsptr colorAnimator; + qsptr majorColorAnimator; + + bool operator==(const GridSettings& other) const; + bool operator!=(const GridSettings& other) const { return !(*this == other); } +}; + +inline const GridSettings& GridSettings::defaults() +{ + static const GridSettings defaultsInstance; + return defaultsInstance; +} + +class CORE_EXPORT GridController { +public: + GridSettings settings; + + void drawGrid(QPainter* painter, + const QRectF& worldViewport, + const QTransform& worldToScreen, + qreal devicePixelRatio = 1.0) const; + void drawGrid(SkCanvas* canvas, + const QRectF& worldViewport, + const QTransform& worldToScreen, + qreal devicePixelRatio = 1.0) const; + + QPointF maybeSnapPivot(const QPointF& pivotWorld, + const QTransform& worldToScreen, + bool forceSnap, + bool bypassSnap, + const QRectF* canvasRectWorld = nullptr, + const std::vector* anchorOffsets = nullptr, + const std::vector* pivotTargets = nullptr, + const std::vector* boxTargets = nullptr, + const std::vector* nodeTargets = nullptr) const; + +private: + enum class Orientation { + Vertical, + Horizontal + }; + + template + void forEachGridLine(const QRectF& worldViewport, + const QTransform& worldToScreen, + qreal devicePixelRatio, + DrawLineFunc&& drawLine) const; + +}; + +} // namespace Core +} // namespace Friction + +#endif // GRIDCONTROLLER_H diff --git a/src/core/simplemath.cpp b/src/core/simplemath.cpp index 6b018115f..ef252d9e0 100644 --- a/src/core/simplemath.cpp +++ b/src/core/simplemath.cpp @@ -25,6 +25,7 @@ #include "simplemath.h" #include "skia/skqtconversions.h" +#include qreal signedSquare(const qreal val) { return val*val*SIGN(val); @@ -131,6 +132,10 @@ bool isPointZero(QPointF pos) { return pointToLen(pos) < 0.0001; } +bool isPointFinite(const QPointF& point) { + return std::isfinite(point.x()) && std::isfinite(point.y()); +} + bool isNonZero(const float val) { return val > 0.0001f || val < - 0.0001f; } diff --git a/src/core/simplemath.h b/src/core/simplemath.h index bb3cb48c3..e16b0bdd0 100644 --- a/src/core/simplemath.h +++ b/src/core/simplemath.h @@ -196,4 +196,7 @@ extern QPointF gQPointFDisplace(const QPointF& pt, const qreal displ); CORE_EXPORT extern bool isPointZero(QPointF pos); +CORE_EXPORT +extern bool isPointFinite(const QPointF& point); + #endif // SIMPLEMATH_H diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index e60e94183..e3b892868 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -48,6 +48,7 @@ set( dialogs/dialog.cpp dialogs/durationrectsettingsdialog.cpp dialogs/exportsvgdialog.cpp + dialogs/gridsettingsdialog.cpp dialogs/markereditordialog.cpp dialogs/qrealpointvaluedialog.cpp dialogs/renderoutputwidget.cpp @@ -123,6 +124,7 @@ set( dialogs/dialog.h dialogs/durationrectsettingsdialog.h dialogs/exportsvgdialog.h + dialogs/gridsettingsdialog.h dialogs/markereditordialog.h dialogs/qrealpointvaluedialog.h dialogs/renderoutputwidget.h diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp new file mode 100644 index 000000000..73d6fa929 --- /dev/null +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -0,0 +1,292 @@ +/* +# +# Friction - https://friction.graphics +# +# Copyright (c) Ole-André Rodlie and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# See 'README.md' for more information. +# +*/ + +#include "gridsettingsdialog.h" + +#include "gridcontroller.h" +#include "GUI/coloranimatorbutton.h" +#include "GUI/global.h" +#include "Private/esettings.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Friction::Core::GridSettings; + +namespace { +constexpr double kMinSpacing = 1.0; +constexpr double kMaxSpacing = 10000.0; +constexpr double kOriginRange = 100000.0; +constexpr int kMaxSnapThreshold = 200; +constexpr int kMaxMajorEvery = 100; +} + +GridSettingsDialog::GridSettingsDialog(QWidget* parent) + : Friction::Ui::Dialog(parent) + , mSizeX(nullptr) + , mSizeY(nullptr) + , mOriginX(nullptr) + , mOriginY(nullptr) + , mSnapThreshold(nullptr) + , mMajorEveryX(nullptr) + , mMajorEveryY(nullptr) + , mSaveAsDefault(nullptr) + , mApplyButton(nullptr) + , mOkButton(nullptr) + , mCancelButton(nullptr) + , mColorButton(nullptr) + , mMajorColorButton(nullptr) + , mColorAnimator(enve::make_shared()) + , mMajorColorAnimator(enve::make_shared()) + , mSnapEnabled(true) + , mStoredSnapToCanvas(false) + , mStoredSnapToBoxes(false) + , mStoredSnapToNodes(false) + , mStoredSnapToPivots(false) + , mStoredSnapAnchorPivot(true) + , mStoredSnapAnchorBounds(true) + , mStoredSnapAnchorNodes(false) +{ + setModal(false); + const QColor defaultMinor = GridSettings::defaults().colorAnimator->getColor(); + const QColor defaultMajor = GridSettings::defaults().majorColorAnimator->getColor(); + if (auto* settings = eSettings::sInstance) { + mColorAnimator->setColor(settings->fGridColor); + mMajorColorAnimator->setColor(settings->fGridMajorColor); + } else { + mColorAnimator->setColor(defaultMinor); + mMajorColorAnimator->setColor(defaultMajor); + } + setupUi(); +} + +void GridSettingsDialog::setupUi() +{ + setWindowTitle(tr("Grid Settings")); + auto* layout = new QVBoxLayout(this); + + auto* form = new QGridLayout(); + form->setColumnStretch(1, 1); + form->setColumnStretch(2, 1); + form->setContentsMargins(0, 0, 0, 0); + form->setHorizontalSpacing(12); + form->setVerticalSpacing(8); + + auto addLabel = [this, form](int row, const QString& text, QWidget* buddy) { + auto* label = new QLabel(text, this); + if (buddy) { + label->setBuddy(buddy); + } + form->addWidget(label, row, 0); + }; + + int formRow = 0; + + mOriginX = new QDoubleSpinBox(this); + mOriginX->setDecimals(0); + mOriginX->setRange(-kOriginRange, kOriginRange); + mOriginX->setSingleStep(1.0); + mOriginX->setToolTip(tr("Horizontal origin offset")); + mOriginY = new QDoubleSpinBox(this); + mOriginY->setDecimals(0); + mOriginY->setRange(-kOriginRange, kOriginRange); + mOriginY->setSingleStep(1.0); + mOriginY->setToolTip(tr("Vertical origin offset")); + addLabel(formRow, tr("Origin"), mOriginX); + form->addWidget(mOriginX, formRow, 1); + form->addWidget(mOriginY, formRow, 2); + ++formRow; + + mSizeX = new QDoubleSpinBox(this); + mSizeX->setDecimals(0); + mSizeX->setRange(kMinSpacing, kMaxSpacing); + mSizeX->setSingleStep(1.0); + mSizeX->setToolTip(tr("Horizontal grid spacing")); + mSizeY = new QDoubleSpinBox(this); + mSizeY->setDecimals(0); + mSizeY->setRange(kMinSpacing, kMaxSpacing); + mSizeY->setSingleStep(1.0); + mSizeY->setToolTip(tr("Vertical grid spacing")); + addLabel(formRow, tr("Spacing"), mSizeX); + form->addWidget(mSizeX, formRow, 1); + form->addWidget(mSizeY, formRow, 2); + ++formRow; + + mSnapThreshold = new QSpinBox(this); + mSnapThreshold->setRange(0, kMaxSnapThreshold); + mSnapThreshold->setSingleStep(1); + addLabel(formRow, tr("Snap radius"), mSnapThreshold); + form->addWidget(mSnapThreshold, formRow, 1, 1, 2); + ++formRow; + + mMajorEveryX = new QSpinBox(this); + mMajorEveryX->setRange(1, kMaxMajorEvery); + mMajorEveryX->setSingleStep(1); + mMajorEveryX->setToolTip(tr("Horizontal major grid line interval")); + mMajorEveryY = new QSpinBox(this); + mMajorEveryY->setRange(1, kMaxMajorEvery); + mMajorEveryY->setSingleStep(1); + mMajorEveryY->setToolTip(tr("Vertical major grid line interval")); + addLabel(formRow, tr("Major line every"), mMajorEveryX); + form->addWidget(mMajorEveryX, formRow, 1); + form->addWidget(mMajorEveryY, formRow, 2); + ++formRow; + + mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); + mMajorColorButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + addLabel(formRow, tr("Major line color"), mMajorColorButton); + form->addWidget(mMajorColorButton, formRow, 1, 1, 2); + ++formRow; + + mColorButton = new ColorAnimatorButton(mColorAnimator.get(), this); + mColorButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + addLabel(formRow, tr("Minor line color"), mColorButton); + form->addWidget(mColorButton, formRow, 1, 1, 2); + ++formRow; + + mApplyButton = new QPushButton(QIcon::fromTheme("dialog-apply"), tr("Apply"), this); + mApplyButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + mApplyButton->setAutoDefault(false); + mApplyButton->setDefault(false); + form->addWidget(mApplyButton, formRow, 0, 1, 3); + + layout->addLayout(form); + eSizesUI::widget.addSpacing(layout); + + mSaveAsDefault = new QCheckBox(tr("Save as default"), this); + layout->addWidget(mSaveAsDefault); + + mOkButton = new QPushButton(QIcon::fromTheme("dialog-ok"), tr("Ok"), this); + mCancelButton = new QPushButton(QIcon::fromTheme("dialog-cancel"), tr("Cancel"), this); + + auto* buttonLayout = new QHBoxLayout(); + layout->addLayout(buttonLayout); + + buttonLayout->addWidget(mOkButton); + buttonLayout->addWidget(mCancelButton); + + connect(mOkButton, &QPushButton::released, + this, &GridSettingsDialog::accept); + connect(mCancelButton, &QPushButton::released, + this, &GridSettingsDialog::reject); + connect(mApplyButton, &QPushButton::released, this, [this]() { + emit applyRequested(settings(), saveAsDefault()); + }); + connect(this, &QDialog::rejected, this, &QDialog::close); +} + +void GridSettingsDialog::setSettings(const GridSettings& settings) +{ + mSnapEnabled = settings.enabled; + mSizeX->setValue(settings.sizeX); + mSizeY->setValue(settings.sizeY); + mOriginX->setValue(settings.originX); + mOriginY->setValue(settings.originY); + mSnapThreshold->setValue(settings.snapThresholdPx); + mStoredSnapToCanvas = settings.snapToCanvas; + mStoredSnapToBoxes = settings.snapToBoxes; + mStoredSnapToNodes = settings.snapToNodes; + mStoredSnapToPivots = settings.snapToPivots; + mStoredSnapAnchorPivot = settings.snapAnchorPivot; + mStoredSnapAnchorBounds = settings.snapAnchorBounds; + mStoredSnapAnchorNodes = settings.snapAnchorNodes; + mMajorEveryX->setValue(settings.majorEveryX); + mMajorEveryY->setValue(settings.majorEveryY); + mStoredShow = settings.show; + mStoredDrawOnTop = settings.drawOnTop; + if (mSaveAsDefault) { + mSaveAsDefault->setChecked(false); + } + + const auto ensureAnimator = [](qsptr& animator, + ColorAnimatorButton* button) + { + if (!animator) { + animator = enve::make_shared(); + if (button) { + button->setColorTarget(animator.get()); + } + } + }; + + ensureAnimator(mColorAnimator, mColorButton); + ensureAnimator(mMajorColorAnimator, mMajorColorButton); + + const QColor appliedColor = settings.colorAnimator + ? settings.colorAnimator->getColor() + : GridSettings::defaults().colorAnimator->getColor(); + mColorAnimator->setColor(appliedColor); + + const QColor appliedMajorColor = settings.majorColorAnimator + ? settings.majorColorAnimator->getColor() + : GridSettings::defaults().majorColorAnimator->getColor(); + mMajorColorAnimator->setColor(appliedMajorColor); +} + +GridSettings GridSettingsDialog::settings() const +{ + GridSettings result; + result.enabled = mSnapEnabled; + result.sizeX = mSizeX->value(); + result.sizeY = mSizeY->value(); + result.originX = mOriginX->value(); + result.originY = mOriginY->value(); + result.snapThresholdPx = mSnapThreshold->value(); + result.snapToCanvas = mStoredSnapToCanvas; + result.snapToBoxes = mStoredSnapToBoxes; + result.snapToNodes = mStoredSnapToNodes; + result.snapToPivots = mStoredSnapToPivots; + result.snapAnchorPivot = mStoredSnapAnchorPivot; + result.snapAnchorBounds = mStoredSnapAnchorBounds; + result.snapAnchorNodes = mStoredSnapAnchorNodes; + result.majorEveryX = mMajorEveryX->value(); + result.majorEveryY = mMajorEveryY->value(); + result.show = mStoredShow; + result.drawOnTop = mStoredDrawOnTop; + + const QColor finalColor = mColorAnimator + ? mColorAnimator->getColor() + : GridSettings::defaults().colorAnimator->getColor(); + result.colorAnimator = enve::make_shared(); + result.colorAnimator->setColor(finalColor); + + const QColor finalMajorColor = mMajorColorAnimator + ? mMajorColorAnimator->getColor() + : GridSettings::defaults().majorColorAnimator->getColor(); + result.majorColorAnimator = enve::make_shared(); + result.majorColorAnimator->setColor(finalMajorColor); + return result; +} + +bool GridSettingsDialog::saveAsDefault() const +{ + return mSaveAsDefault && mSaveAsDefault->isChecked(); +} diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h new file mode 100644 index 000000000..d391c9c48 --- /dev/null +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -0,0 +1,89 @@ +/* +# +# Friction - https://friction.graphics +# +# Copyright (c) Ole-André Rodlie and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# See 'README.md' for more information. +# +*/ + +#ifndef GRIDSETTINGSDIALOG_H +#define GRIDSETTINGSDIALOG_H + +#include "ui_global.h" +#include "dialog.h" +#include "Animators/coloranimator.h" +#include "smartPointers/ememory.h" + + +class QDoubleSpinBox; +class QSpinBox; +class QCheckBox; +class QPushButton; +class ColorAnimatorButton; + +namespace Friction { +namespace Core { +struct GridSettings; +} +} + +class UI_EXPORT GridSettingsDialog : public Friction::Ui::Dialog +{ + Q_OBJECT + +public: + explicit GridSettingsDialog(QWidget* parent = nullptr); + + void setSettings(const Friction::Core::GridSettings& settings); + Friction::Core::GridSettings settings() const; + bool saveAsDefault() const; + +signals: + void applyRequested(Friction::Core::GridSettings settings, bool saveAsDefault); + +private: + void setupUi(); + + QDoubleSpinBox* mSizeX; + QDoubleSpinBox* mSizeY; + QDoubleSpinBox* mOriginX; + QDoubleSpinBox* mOriginY; + QSpinBox* mSnapThreshold; + QSpinBox* mMajorEveryX; + QSpinBox* mMajorEveryY; + QCheckBox* mSaveAsDefault; + QPushButton* mApplyButton; + QPushButton* mOkButton; + QPushButton* mCancelButton; + ColorAnimatorButton* mColorButton; + ColorAnimatorButton* mMajorColorButton; + qsptr mColorAnimator; + qsptr mMajorColorAnimator; + bool mSnapEnabled = true; + bool mStoredShow = true; + bool mStoredDrawOnTop = true; + bool mStoredSnapToCanvas = false; + bool mStoredSnapToBoxes = false; + bool mStoredSnapToNodes = false; + bool mStoredSnapToPivots = false; + bool mStoredSnapAnchorPivot = true; + bool mStoredSnapAnchorBounds = true; + bool mStoredSnapAnchorNodes = false; +}; + +#endif // GRIDSETTINGSDIALOG_H