From 43a01cc6381b9f3bcaa3352c191f6818347c032c Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 16 Oct 2025 09:28:18 +0200 Subject: [PATCH 01/37] Grid: first working version --- src/app/GUI/canvaswindow.cpp | 5 + src/app/GUI/mainwindow.cpp | 35 +++ src/app/GUI/mainwindow.h | 6 + src/app/GUI/menu.cpp | 18 +- src/core/CMakeLists.txt | 2 + src/core/Private/document.cpp | 143 ++++++++++++ src/core/Private/document.h | 20 ++ src/core/Private/documentrw.cpp | 98 +++++++++ src/core/ReadWrite/evformat.h | 1 + src/core/canvas.cpp | 57 ++++- src/core/canvas.h | 8 + src/core/canvasmouseinteractions.cpp | 20 +- src/core/gridcontroller.cpp | 306 ++++++++++++++++++++++++++ src/core/gridcontroller.h | 101 +++++++++ src/ui/CMakeLists.txt | 2 + src/ui/dialogs/gridsettingsdialog.cpp | 221 +++++++++++++++++++ src/ui/dialogs/gridsettingsdialog.h | 78 +++++++ 17 files changed, 1109 insertions(+), 12 deletions(-) create mode 100644 src/core/gridcontroller.cpp create mode 100644 src/core/gridcontroller.h create mode 100644 src/ui/dialogs/gridsettingsdialog.cpp create mode 100644 src/ui/dialogs/gridsettingsdialog.h 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..ab1db8363 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,8 @@ MainWindow::MainWindow(Document& document, , mInvertSelAct(nullptr) , mClearSelAct(nullptr) , mAddKeyAct(nullptr) + , mSnapToGridAct(nullptr) + , mGridSettingsAct(nullptr) , mAddToQueAct(nullptr) , mViewFullScreenAct(nullptr) , mFontWidget(nullptr) @@ -160,6 +164,11 @@ MainWindow::MainWindow(Document& document, Q_ASSERT(!sInstance); sInstance = this; + connect(&mDocument, &Document::gridSettingsChanged, + this, &MainWindow::onGridSettingsChanged); + connect(&mDocument, &Document::gridSnapEnabledChanged, + this, &MainWindow::onGridSnapEnabledChanged); + setWindowIcon(QIcon::fromTheme(AppSupport::getAppID())); setContextMenuPolicy(Qt::NoContextMenu); @@ -208,6 +217,32 @@ BoundingBox *MainWindow::getCurrentBox() return box; } +void MainWindow::openGridSettingsDialog() +{ + GridSettingsDialog dialog(this); + dialog.setWindowTitle(tr("Grid Settings")); + dialog.setSettings(mDocument.gridController().settings); + if (dialog.exec() == QDialog::Accepted) { + auto settings = dialog.settings(); + settings.enabled = mDocument.gridController().settings.enabled; + mDocument.setGridSettings(settings); + } +} + +void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& settings) +{ + onGridSnapEnabledChanged(settings.enabled); +} + +void MainWindow::onGridSnapEnabledChanged(bool enabled) +{ + if (!mSnapToGridAct) { return; } + QSignalBlocker blocker(mSnapToGridAct); + if (mSnapToGridAct->isChecked() != enabled) { + mSnapToGridAct->setChecked(enabled); + } +} + void MainWindow::checkAutoSaveTimer() { if (mShutdown) { return; } diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 37ba7eaf7..ffe1f9eb7 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,9 @@ class MainWindow : public QMainWindow void openWelcomeDialog(); void closeWelcomeDialog(); + void openGridSettingsDialog(); + void onGridSettingsChanged(const Friction::Core::GridSettings& settings); + void onGridSnapEnabledChanged(bool enabled); eKeyFilter* mNumericFilter = eKeyFilter::sCreateNumberFilter(this); eKeyFilter* mLineFilter = eKeyFilter::sCreateLineFilter(this); @@ -255,6 +259,8 @@ class MainWindow : public QMainWindow QAction *mZoomInAction; QAction *mZoomOutAction; QAction *mFitViewAction; + QAction *mSnapToGridAct; + QAction *mGridSettingsAct; QAction *mNoneQuality; QAction *mLowQuality; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index 8792d10f9..c2776bbd9 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -335,6 +335,22 @@ void MainWindow::setupMenuBar() mViewMenu = mMenuBar->addMenu(tr("View", "MenuBar")); + mSnapToGridAct = mViewMenu->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); + + mGridSettingsAct = mViewMenu->addAction(tr("Grid Settings...")); + connect(mGridSettingsAct, &QAction::triggered, this, &MainWindow::openGridSettingsDialog); + cmdAddAction(mGridSettingsAct); + + mViewMenu->addSeparator(); + onGridSettingsChanged(mDocument.gridController().settings); + + mObjectMenu = mMenuBar->addMenu(tr("Object", "MenuBar")); mObjectMenu->addSeparator(); @@ -947,4 +963,4 @@ void MainWindow::setupMenuScene() tr("Edit Markers"), this, [this]() { openMarkerEditor(); }); -} +} \ No newline at end of file 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..5d0e61515 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,143 @@ 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.majorEvery < 1) { settings.majorEvery = 1; } + if (settings.snapThresholdPx < 0) { settings.snapThresholdPx = 0; } + if (!settings.colorAnimator) { settings.colorAnimator = enve::make_shared(); } + QColor color = settings.colorAnimator->getColor(); + if (!color.isValid()) { color = QColor(255, 255, 255, 96); } + int alpha = color.alpha(); + if (alpha < 0) { alpha = 0; } + if (alpha > 255) { alpha = 255; } + color.setAlpha(alpha); + settings.colorAnimator->setColor(color); + return settings; +} + +void Document::setGridSnapEnabled(const bool enabled) +{ + auto updated = mGridController.settings; + if (updated.enabled == enabled) { return; } + updated.enabled = enabled; + applyGridSettings(updated, false, false); +} + +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; + 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.majorEvery = AppSupport::getSettings("grid", "majorEvery", defaults.majorEvery).toInt(); + const QVariant colorVariant = AppSupport::getSettings("grid", "color", defaults.colorAnimator->getColor()); + QColor storedColor; + if (colorVariant.canConvert()) { + storedColor = colorVariant.value(); + } else { + storedColor = QColor(colorVariant.toString()); + } + if (!storedColor.isValid()) { storedColor = QColor(255, 255, 255, 96); } + if (!loaded.colorAnimator) { loaded.colorAnimator = enve::make_shared(); } + loaded.colorAnimator->setColor(storedColor); + applyGridSettings(loaded, true, true); +} + +void Document::saveGridSettingsToSettings() const +{ + const auto& s = mGridController.settings; + AppSupport::setSettings("grid", "sizeX", s.sizeX); + AppSupport::setSettings("grid", "sizeY", s.sizeY); + AppSupport::setSettings("grid", "originX", s.originX); + AppSupport::setSettings("grid", "originY", s.originY); + AppSupport::setSettings("grid", "snapThresholdPx", s.snapThresholdPx); + AppSupport::setSettings("grid", "enabled", s.enabled); + AppSupport::setSettings("grid", "show", s.show); + AppSupport::setSettings("grid", "majorEvery", s.majorEvery); + const QColor color = s.colorAnimator ? s.colorAnimator->getColor() : QColor(255, 255, 255, 96); + AppSupport::setSettings("grid", "color", color); +} + +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) { + if (!skipSave) { saveGridSettingsToSettings(); } + 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.majorEvery != sanitized.majorEvery; + const QColor previousColor = previous.colorAnimator ? previous.colorAnimator->getColor() : QColor(); + const QColor sanitizedColor = sanitized.colorAnimator ? sanitized.colorAnimator->getColor() : QColor(); + const bool colorChanged = previousColor != sanitizedColor; + + mGridController.settings = sanitized; + + if (!skipSave) { saveGridSettingsToSettings(); } + + if (silent) { return; } + + emit gridSettingsChanged(mGridController.settings); + if (snapChanged) { + emit gridSnapEnabledChanged(mGridController.settings.enabled); + } + + if (showChanged || (mGridController.settings.show && (metricsChanged || colorChanged))) { + updateScenes(); + } +} + + Clipboard *Document::getClipboard(const ClipboardType type) const { if(!fClipboardContainer) return nullptr; if(type == fClipboardContainer->getType()) diff --git a/src/core/Private/document.h b/src/core/Private/document.h index 729f88fe2..aa3c4bc65 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,13 @@ 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 setGridSettings(const Friction::Core::GridSettings& settings); + stdsptr fClipboardContainer; QString fEvFile; @@ -109,6 +117,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 +208,19 @@ 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; + 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 canvasModeSet(CanvasMode); void gizmoVisibilityChanged(const Friction::Core::Gizmos::Interact &ti, diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 9fa04b075..90ba869c9 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,26 @@ 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.majorEvery; + const QColor color = s.colorAnimator ? s.colorAnimator->getColor() : QColor(255, 255, 255, 96); + dst << color; +} + + void Document::writeScenes(eWriteStream& dst) const { + writeGridSettings(dst); + dst.writeCheckpoint(); + writeBookmarked(dst); dst.writeCheckpoint(); @@ -72,6 +97,31 @@ 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.majorEvery; + QColor color; + src >> color; + settings.enabled = enabled; + settings.show = show; + if (!settings.colorAnimator) { + settings.colorAnimator = enve::make_shared(); + } + settings.colorAnimator->setColor(color); + applyGridSettings(settings, false, true); +} + + void Document::readGradients(eReadStream& src) { int nGrads; src >> nGrads; for(int i = 0; i < nGrads; i++) { @@ -80,6 +130,10 @@ void Document::readGradients(eReadStream& src) { } void Document::readScenes(eReadStream& src) { + if (src.evFileVersion() >= EvFormat::gridSettings) { + readGridSettings(src); + src.readCheckpoint("Error reading grid settings"); + } if(src.evFileVersion() > 1) { readBookmarked(src); src.readCheckpoint("Error reading bookmarks"); @@ -106,6 +160,31 @@ 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"; + settings.majorEvery = element.attribute("majorEvery", QString::number(settings.majorEvery)).toInt(); + 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); + } + } + applyGridSettings(settings, false, true); +} + + void Document::writeDoxumentXEV(QDomDocument& doc) const { auto document = doc.createElement("Document"); document.setAttribute("format-version", XevFormat::version); @@ -125,6 +204,20 @@ 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("majorEvery", QString::number(grid.majorEvery)); + const QColor gridColor = grid.colorAnimator ? grid.colorAnimator->getColor() : QColor(255, 255, 255, 96); + gridSettings.setAttribute("color", gridColor.name(QColor::HexArgb)); + document.appendChild(gridSettings); + auto scenes = doc.createElement("Scenes"); for(const auto &s : fScenes) { auto scene = doc.createElement("Scene"); @@ -183,6 +276,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/ReadWrite/evformat.h b/src/core/ReadWrite/evformat.h index 5d45c54ff..843f480f0 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, + gridSettings = 34, nextVersion }; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index a2a5ae8cf..cb0ed7603 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,25 @@ 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 bool gridVisible = mDocument.gridController().settings.show && + (!haveWorldTransform || !gridViewport.isEmpty()); + const bool drawCanvas = mSceneFrame && mSceneFrame->fBoxState == mStateId; canvas->concat(skViewTrans); if(isPreviewingOrRendering()) { @@ -272,7 +301,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 +308,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) { + 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,7 +340,7 @@ void Canvas::renderSk(SkCanvas* const canvas, mSceneFrame->drawImage(canvas, filter); canvas->restore(); } - + canvas->restore(); canvas->restore(); if (!enve_cast(mCurrentContainer)) { diff --git a/src/core/canvas.h b/src/core/canvas.h index b1ae35d5c..e88c61ae8 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -46,6 +46,7 @@ #include #include #include +#include #include "gizmos.h" @@ -187,6 +188,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(); @@ -824,6 +826,12 @@ class CORE_EXPORT Canvas : public CanvasBase protected: Document& mDocument; + QTransform mWorldToScreen; + QTransform mScreenToWorld; + bool mHasWorldToScreen = false; + qreal mDevicePixelRatio = 1.0; + QPointF mGridMoveStartPivot; + bool mDrawnSinceQue = true; qsptr mUndoRedoStack; diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 205219c22..a9eef022b 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -883,7 +883,25 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { mPressedBox = nullptr; } - const auto moveBy = getMoveByValueForEvent(e); + if (mStartTransform && !mSelectedBoxes.isEmpty()) { + mGridMoveStartPivot = getSelectedBoxesAbsPivotPos(); + } + + auto moveBy = getMoveByValueForEvent(e); + const bool bypassSnap = e.fModifiers & Qt::AltModifier; + const bool forceSnap = e.fModifiers & Qt::ControlModifier; + if (!mSelectedBoxes.isEmpty() && mHasWorldToScreen && + (mDocument.gridController().settings.enabled || forceSnap)) { + const QPointF targetPivot = mGridMoveStartPivot + moveBy; + const auto snapped = mDocument.gridController().maybeSnapPivot(targetPivot, + mWorldToScreen, + forceSnap, + bypassSnap); + 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..46489907a --- /dev/null +++ b/src/core/gridcontroller.cpp @@ -0,0 +1,306 @@ +/* +# +# 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 + +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.majorEvery < 1) { copy.majorEvery = 1; } + if (copy.snapThresholdPx < 0) { copy.snapThresholdPx = 0; } + if (!copy.colorAnimator) { + copy.colorAnimator = enve::make_shared(); + } + QColor color = copy.colorAnimator->getColor(); + if (!color.isValid()) { + color = QColor(255, 255, 255, 96); + } + const double alpha = clampToRange(static_cast(color.alpha()), 0.0, 255.0); + color.setAlpha(static_cast(alpha)); + copy.colorAnimator->setColor(color); + 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(); + 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 && + majorEvery == other.majorEvery && + thisColor == otherColor; +} + + + + +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 QColor baseColor = sanitizedSettings.colorAnimator->getColor(); + QColor majorBase = baseColor; + QColor minorBase = baseColor; + minorBase.setAlphaF(baseColor.alphaF() * 0.5); + + 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 QColor baseColor = sanitizedSettings.colorAnimator->getColor(); + QColor majorBase = baseColor; + QColor minorBase = baseColor; + minorBase.setAlphaF(baseColor.alphaF() * 0.5); + 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 +{ + const GridSettings sanitizedSettings = sanitizeSettings(settings); + + if (!sanitizedSettings.enabled || bypassSnap) { + return pivotWorld; + } + + const double sizeX = sanitizedSettings.sizeX; + const double sizeY = sanitizedSettings.sizeY; + if (sizeX <= 0.0 || sizeY <= 0.0) { return pivotWorld; } + + const double gx = sanitizedSettings.originX + + std::round((pivotWorld.x() - sanitizedSettings.originX) / sizeX) * sizeX; + const double gy = sanitizedSettings.originY + + std::round((pivotWorld.y() - sanitizedSettings.originY) / sizeY) * sizeY; + const QPointF snapped(gx, gy); + + if (forceSnap) { return snapped; } + + const QPointF screenPivot = worldToScreen.map(pivotWorld); + const QPointF screenSnap = worldToScreen.map(snapped); + const double dist = QLineF(screenPivot, screenSnap).length(); + + if (dist <= sanitizedSettings.snapThresholdPx) { + return snapped; + } + 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 majorEvery = std::max(1, sanitizedSettings.majorEvery); + + const double spacingX = lineSpacingPx(worldToScreen, devicePixelRatio, {sizeX, 0.0}); + const double spacingY = lineSpacingPx(worldToScreen, devicePixelRatio, {0.0, sizeY}); + const double majorSpacingX = spacingX * majorEvery; + const double majorSpacingY = spacingY * majorEvery; + + 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 % majorEvery) == 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 % majorEvery) == 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..82736b19b --- /dev/null +++ b/src/core/gridcontroller.h @@ -0,0 +1,101 @@ +/* +# +# 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 + +class QPainter; +class SkCanvas; + +namespace Friction { +namespace Core { + +struct CORE_EXPORT GridSettings { + GridSettings() + : colorAnimator(enve::make_shared()) + { + colorAnimator->setColor(QColor(255, 255, 255, 96)); + } + + double sizeX = 50.0; + double sizeY = 50.0; + double originX = 0.0; + double originY = 0.0; + int snapThresholdPx = 8; + bool enabled = true; + bool show = true; + int majorEvery = 5; + qsptr colorAnimator; + + bool operator==(const GridSettings& other) const; + bool operator!=(const GridSettings& other) const { return !(*this == other); } +}; + +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; + +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/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..237402523 --- /dev/null +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -0,0 +1,221 @@ +/* +# +# 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 "gridcontroller.h" + +#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) + : QDialog(parent) + , mSizeX(nullptr) + , mSizeY(nullptr) + , mOriginX(nullptr) + , mOriginY(nullptr) + , mSnapThreshold(nullptr) + , mMajorEvery(nullptr) + , mShowGrid(nullptr) + , mButtonBox(nullptr) + , mColorButton(nullptr) + , mAlphaSpin(nullptr) + , mColorAnimator(enve::make_shared()) + , mSnapEnabled(true) + , mCurrentColor(QColor(255, 255, 255, 96)) +{ + mColorAnimator->setColor(mCurrentColor); + setupUi(); +} + +void GridSettingsDialog::setupUi() +{ + setWindowTitle(tr("Grid Settings")); + auto* layout = new QVBoxLayout(this); + + auto* form = new QFormLayout(); + form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + + mSizeX = new QDoubleSpinBox(this); + mSizeX->setDecimals(2); + mSizeX->setRange(kMinSpacing, kMaxSpacing); + mSizeX->setSingleStep(1.0); + form->addRow(tr("Spacing X"), mSizeX); + + mSizeY = new QDoubleSpinBox(this); + mSizeY->setDecimals(2); + mSizeY->setRange(kMinSpacing, kMaxSpacing); + mSizeY->setSingleStep(1.0); + form->addRow(tr("Spacing Y"), mSizeY); + + mOriginX = new QDoubleSpinBox(this); + mOriginX->setDecimals(2); + mOriginX->setRange(-kOriginRange, kOriginRange); + mOriginX->setSingleStep(1.0); + form->addRow(tr("Origin X"), mOriginX); + + mOriginY = new QDoubleSpinBox(this); + mOriginY->setDecimals(2); + mOriginY->setRange(-kOriginRange, kOriginRange); + mOriginY->setSingleStep(1.0); + form->addRow(tr("Origin Y"), mOriginY); + + mSnapThreshold = new QSpinBox(this); + mSnapThreshold->setRange(0, kMaxSnapThreshold); + mSnapThreshold->setSingleStep(1); + form->addRow(tr("Snap Threshold (px)"), mSnapThreshold); + + mMajorEvery = new QSpinBox(this); + mMajorEvery->setRange(1, kMaxMajorEvery); + mMajorEvery->setSingleStep(1); + form->addRow(tr("Major Line Every"), mMajorEvery); + + auto* colorLayout = new QHBoxLayout(); + mColorButton = new QPushButton(tr("Select Color"), this); + mColorButton->setToolTip(tr("Pick grid line color")); + colorLayout->addWidget(mColorButton); + mAlphaSpin = new QSpinBox(this); + mAlphaSpin->setRange(0, 255); + mAlphaSpin->setSingleStep(1); + mAlphaSpin->setToolTip(tr("Opacity (0-255)")); + mAlphaSpin->setValue(mCurrentColor.alpha()); + colorLayout->addWidget(mAlphaSpin); + colorLayout->addStretch(); + form->addRow(tr("Grid Color"), colorLayout); + + mShowGrid = new QCheckBox(tr("Show grid"), this); + form->addRow(QString(), mShowGrid); + + layout->addLayout(form); + + mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + auto* restoreButton = mButtonBox->addButton(tr("Restore Defaults"), QDialogButtonBox::ResetRole); + connect(restoreButton, &QPushButton::clicked, this, &GridSettingsDialog::restoreDefaults); + connect(mColorButton, &QPushButton::clicked, this, &GridSettingsDialog::chooseColor); + mAlphaSpin->setValue(mCurrentColor.alpha()); + connect(mAlphaSpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int){ refreshColorButton(); }); + connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + layout->addWidget(mButtonBox); + refreshColorButton(); +} + +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); + mMajorEvery->setValue(settings.majorEvery); + mShowGrid->setChecked(settings.show); + + mColorAnimator = enve::make_shared(); + if (settings.colorAnimator) { + mColorAnimator->setColor(settings.colorAnimator->getColor()); + } else { + mColorAnimator->setColor(QColor(255, 255, 255, 96)); + } + mCurrentColor = mColorAnimator->getColor(); + if (mAlphaSpin) { + mAlphaSpin->setValue(mCurrentColor.alpha()); + } + refreshColorButton(); +} + +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.majorEvery = mMajorEvery->value(); + result.show = mShowGrid->isChecked(); + + QColor finalColor = mCurrentColor; + if (mAlphaSpin) { + finalColor.setAlpha(mAlphaSpin->value()); + } + result.colorAnimator = enve::make_shared(); + result.colorAnimator->setColor(finalColor); + return result; +} + +void GridSettingsDialog::restoreDefaults() +{ + setSettings(GridSettings{}); +} + +void GridSettingsDialog::chooseColor() +{ + const QColor chosen = QColorDialog::getColor(mCurrentColor, this, tr("Select Grid Color"), QColorDialog::ShowAlphaChannel); + if (!chosen.isValid()) { return; } + mCurrentColor = chosen; + if (mAlphaSpin) { + mAlphaSpin->setValue(chosen.alpha()); + } + if (mColorAnimator) { + mColorAnimator->setColor(mCurrentColor); + } + refreshColorButton(); +} + +void GridSettingsDialog::refreshColorButton() +{ + if (!mColorButton) { return; } + QColor display = mCurrentColor; + if (mAlphaSpin) { + display.setAlpha(mAlphaSpin->value()); + } + const QColor textColor = display.lightness() > 128 ? Qt::black : Qt::white; + const QString stylesheet = QStringLiteral("color: %1; background-color: %2; border: 1px solid gray;") + .arg(textColor.name(), display.name(QColor::HexArgb)); + mColorButton->setStyleSheet(stylesheet); + mColorButton->setText(display.name(QColor::HexArgb)); + mCurrentColor = display; + if (mColorAnimator) { + mColorAnimator->setColor(display); + } +} diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h new file mode 100644 index 000000000..cb8326c96 --- /dev/null +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -0,0 +1,78 @@ +/* +# +# 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 +#include +#include "Animators/coloranimator.h" +#include "smartPointers/ememory.h" + + +class QDoubleSpinBox; +class QSpinBox; +class QCheckBox; +class QDialogButtonBox; +class QPushButton; + +namespace Friction { +namespace Core { +struct GridSettings; +} +} + +class GridSettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit GridSettingsDialog(QWidget* parent = nullptr); + + void setSettings(const Friction::Core::GridSettings& settings); + Friction::Core::GridSettings settings() const; + +private slots: + void restoreDefaults(); + void chooseColor(); + +private: + void setupUi(); + void refreshColorButton(); + + QDoubleSpinBox* mSizeX; + QDoubleSpinBox* mSizeY; + QDoubleSpinBox* mOriginX; + QDoubleSpinBox* mOriginY; + QSpinBox* mSnapThreshold; + QSpinBox* mMajorEvery; + QCheckBox* mShowGrid; + QDialogButtonBox* mButtonBox; + QPushButton* mColorButton; + QSpinBox* mAlphaSpin; + qsptr mColorAnimator; + bool mSnapEnabled = true; + QColor mCurrentColor; +}; + +#endif // GRIDSETTINGSDIALOG_H From cf2898c1cc1d0ebffc7973624dae97090f491fee Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 16 Oct 2025 10:29:59 +0200 Subject: [PATCH 02/37] Grid: use colorAnimationButton for grid color selection --- src/ui/dialogs/gridsettingsdialog.cpp | 86 ++++++--------------------- src/ui/dialogs/gridsettingsdialog.h | 9 +-- 2 files changed, 20 insertions(+), 75 deletions(-) diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 237402523..d7be26b3e 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -22,8 +22,9 @@ */ #include "gridsettingsdialog.h" + #include "gridcontroller.h" -#include "gridcontroller.h" +#include "GUI/coloranimatorbutton.h" #include #include @@ -31,9 +32,8 @@ #include #include #include +#include #include -#include -#include using Friction::Core::GridSettings; @@ -56,12 +56,10 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mShowGrid(nullptr) , mButtonBox(nullptr) , mColorButton(nullptr) - , mAlphaSpin(nullptr) , mColorAnimator(enve::make_shared()) , mSnapEnabled(true) - , mCurrentColor(QColor(255, 255, 255, 96)) { - mColorAnimator->setColor(mCurrentColor); + mColorAnimator->setColor(QColor(255, 255, 255, 96)); setupUi(); } @@ -107,18 +105,8 @@ void GridSettingsDialog::setupUi() mMajorEvery->setSingleStep(1); form->addRow(tr("Major Line Every"), mMajorEvery); - auto* colorLayout = new QHBoxLayout(); - mColorButton = new QPushButton(tr("Select Color"), this); - mColorButton->setToolTip(tr("Pick grid line color")); - colorLayout->addWidget(mColorButton); - mAlphaSpin = new QSpinBox(this); - mAlphaSpin->setRange(0, 255); - mAlphaSpin->setSingleStep(1); - mAlphaSpin->setToolTip(tr("Opacity (0-255)")); - mAlphaSpin->setValue(mCurrentColor.alpha()); - colorLayout->addWidget(mAlphaSpin); - colorLayout->addStretch(); - form->addRow(tr("Grid Color"), colorLayout); + mColorButton = new ColorAnimatorButton(mColorAnimator.get(), this); + form->addRow(tr("Grid Color"), mColorButton); mShowGrid = new QCheckBox(tr("Show grid"), this); form->addRow(QString(), mShowGrid); @@ -128,14 +116,10 @@ void GridSettingsDialog::setupUi() mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); auto* restoreButton = mButtonBox->addButton(tr("Restore Defaults"), QDialogButtonBox::ResetRole); connect(restoreButton, &QPushButton::clicked, this, &GridSettingsDialog::restoreDefaults); - connect(mColorButton, &QPushButton::clicked, this, &GridSettingsDialog::chooseColor); - mAlphaSpin->setValue(mCurrentColor.alpha()); - connect(mAlphaSpin, QOverload::of(&QSpinBox::valueChanged), this, [this](int){ refreshColorButton(); }); connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(mButtonBox); - refreshColorButton(); } void GridSettingsDialog::setSettings(const GridSettings& settings) @@ -149,17 +133,16 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mMajorEvery->setValue(settings.majorEvery); mShowGrid->setChecked(settings.show); - mColorAnimator = enve::make_shared(); - if (settings.colorAnimator) { - mColorAnimator->setColor(settings.colorAnimator->getColor()); - } else { - mColorAnimator->setColor(QColor(255, 255, 255, 96)); + if (!mColorAnimator) { + mColorAnimator = enve::make_shared(); + if (mColorButton) { + mColorButton->setColorTarget(mColorAnimator.get()); + } } - mCurrentColor = mColorAnimator->getColor(); - if (mAlphaSpin) { - mAlphaSpin->setValue(mCurrentColor.alpha()); - } - refreshColorButton(); + const QColor appliedColor = settings.colorAnimator + ? settings.colorAnimator->getColor() + : QColor(255, 255, 255, 96); + mColorAnimator->setColor(appliedColor); } GridSettings GridSettingsDialog::settings() const @@ -174,10 +157,9 @@ GridSettings GridSettingsDialog::settings() const result.majorEvery = mMajorEvery->value(); result.show = mShowGrid->isChecked(); - QColor finalColor = mCurrentColor; - if (mAlphaSpin) { - finalColor.setAlpha(mAlphaSpin->value()); - } + const QColor finalColor = mColorAnimator + ? mColorAnimator->getColor() + : QColor(255, 255, 255, 96); result.colorAnimator = enve::make_shared(); result.colorAnimator->setColor(finalColor); return result; @@ -187,35 +169,3 @@ void GridSettingsDialog::restoreDefaults() { setSettings(GridSettings{}); } - -void GridSettingsDialog::chooseColor() -{ - const QColor chosen = QColorDialog::getColor(mCurrentColor, this, tr("Select Grid Color"), QColorDialog::ShowAlphaChannel); - if (!chosen.isValid()) { return; } - mCurrentColor = chosen; - if (mAlphaSpin) { - mAlphaSpin->setValue(chosen.alpha()); - } - if (mColorAnimator) { - mColorAnimator->setColor(mCurrentColor); - } - refreshColorButton(); -} - -void GridSettingsDialog::refreshColorButton() -{ - if (!mColorButton) { return; } - QColor display = mCurrentColor; - if (mAlphaSpin) { - display.setAlpha(mAlphaSpin->value()); - } - const QColor textColor = display.lightness() > 128 ? Qt::black : Qt::white; - const QString stylesheet = QStringLiteral("color: %1; background-color: %2; border: 1px solid gray;") - .arg(textColor.name(), display.name(QColor::HexArgb)); - mColorButton->setStyleSheet(stylesheet); - mColorButton->setText(display.name(QColor::HexArgb)); - mCurrentColor = display; - if (mColorAnimator) { - mColorAnimator->setColor(display); - } -} diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index cb8326c96..d23afc7ac 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -25,7 +25,6 @@ #define GRIDSETTINGSDIALOG_H #include -#include #include "Animators/coloranimator.h" #include "smartPointers/ememory.h" @@ -34,7 +33,7 @@ class QDoubleSpinBox; class QSpinBox; class QCheckBox; class QDialogButtonBox; -class QPushButton; +class ColorAnimatorButton; namespace Friction { namespace Core { @@ -54,11 +53,9 @@ class GridSettingsDialog : public QDialog private slots: void restoreDefaults(); - void chooseColor(); private: void setupUi(); - void refreshColorButton(); QDoubleSpinBox* mSizeX; QDoubleSpinBox* mSizeY; @@ -68,11 +65,9 @@ private slots: QSpinBox* mMajorEvery; QCheckBox* mShowGrid; QDialogButtonBox* mButtonBox; - QPushButton* mColorButton; - QSpinBox* mAlphaSpin; + ColorAnimatorButton* mColorButton; qsptr mColorAnimator; bool mSnapEnabled = true; - QColor mCurrentColor; }; #endif // GRIDSETTINGSDIALOG_H From fae7130215c4ce9e15afae906ae2b74c929a644f Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 16 Oct 2025 10:46:45 +0200 Subject: [PATCH 03/37] Grid: mayor lines now use independent color and alpha --- src/core/ReadWrite/evformat.h | 1 + src/core/gridcontroller.cpp | 54 +++++++++++++++++---------- src/core/gridcontroller.h | 3 ++ src/ui/dialogs/gridsettingsdialog.cpp | 35 ++++++++++++++--- src/ui/dialogs/gridsettingsdialog.h | 2 + 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/core/ReadWrite/evformat.h b/src/core/ReadWrite/evformat.h index 843f480f0..8fe68fd45 100644 --- a/src/core/ReadWrite/evformat.h +++ b/src/core/ReadWrite/evformat.h @@ -47,6 +47,7 @@ namespace EvFormat { subPathOffset = 32, avStretch = 33, gridSettings = 34, + gridSettingsMajorColor = 35, nextVersion }; diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index 46489907a..bc30f3a8b 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -61,16 +61,25 @@ GridSettings sanitizeSettings(const GridSettings& in) if (copy.sizeY <= 0.0) { copy.sizeY = 1.0; } if (copy.majorEvery < 1) { copy.majorEvery = 1; } if (copy.snapThresholdPx < 0) { copy.snapThresholdPx = 0; } - if (!copy.colorAnimator) { - copy.colorAnimator = enve::make_shared(); - } - QColor color = copy.colorAnimator->getColor(); - if (!color.isValid()) { - color = QColor(255, 255, 255, 96); - } - const double alpha = clampToRange(static_cast(color.alpha()), 0.0, 255.0); - color.setAlpha(static_cast(alpha)); - copy.colorAnimator->setColor(color); + 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 QColor minorFallback(255, 255, 255, 96); + const QColor majorFallback(255, 255, 255, 160); + ensureAnimatorColor(copy.colorAnimator, minorFallback); + ensureAnimatorColor(copy.majorColorAnimator, majorFallback); return copy; } @@ -114,6 +123,8 @@ 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) && @@ -122,7 +133,8 @@ bool GridSettings::operator==(const GridSettings& other) const enabled == other.enabled && show == other.show && majorEvery == other.majorEvery && - thisColor == otherColor; + thisColor == otherColor && + thisMajorColor == otherMajorColor; } @@ -136,10 +148,12 @@ void GridController::drawGrid(QPainter* painter, const GridSettings sanitizedSettings = sanitizeSettings(settings); if (!painter || !sanitizedSettings.show) { return; } - const QColor baseColor = sanitizedSettings.colorAnimator->getColor(); - QColor majorBase = baseColor; - QColor minorBase = baseColor; - minorBase.setAlphaF(baseColor.alphaF() * 0.5); + const QColor minorBase = sanitizedSettings.colorAnimator + ? sanitizedSettings.colorAnimator->getColor() + : QColor(255, 255, 255, 96); + const QColor majorBase = sanitizedSettings.majorColorAnimator + ? sanitizedSettings.majorColorAnimator->getColor() + : minorBase; auto drawLine = [&](const QPointF& a, const QPointF& b, @@ -166,10 +180,12 @@ void GridController::drawGrid(SkCanvas* canvas, const GridSettings sanitizedSettings = sanitizeSettings(settings); if (!canvas || !sanitizedSettings.show) { return; } - const QColor baseColor = sanitizedSettings.colorAnimator->getColor(); - QColor majorBase = baseColor; - QColor minorBase = baseColor; - minorBase.setAlphaF(baseColor.alphaF() * 0.5); + const QColor minorBase = sanitizedSettings.colorAnimator + ? sanitizedSettings.colorAnimator->getColor() + : QColor(255, 255, 255, 96); + const QColor majorBase = sanitizedSettings.majorColorAnimator + ? sanitizedSettings.majorColorAnimator->getColor() + : minorBase; const float strokeWidth = static_cast( devicePixelRatio / effectiveScale(worldToScreen)); diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 82736b19b..2229165b3 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -45,8 +45,10 @@ namespace Core { struct CORE_EXPORT GridSettings { GridSettings() : colorAnimator(enve::make_shared()) + , majorColorAnimator(enve::make_shared()) { colorAnimator->setColor(QColor(255, 255, 255, 96)); + majorColorAnimator->setColor(QColor(255, 255, 255, 160)); } double sizeX = 50.0; @@ -58,6 +60,7 @@ struct CORE_EXPORT GridSettings { bool show = true; int majorEvery = 5; qsptr colorAnimator; + qsptr majorColorAnimator; bool operator==(const GridSettings& other) const; bool operator!=(const GridSettings& other) const { return !(*this == other); } diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index d7be26b3e..8ea286611 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -56,10 +56,13 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mShowGrid(nullptr) , mButtonBox(nullptr) , mColorButton(nullptr) + , mMajorColorButton(nullptr) , mColorAnimator(enve::make_shared()) + , mMajorColorAnimator(enve::make_shared()) , mSnapEnabled(true) { mColorAnimator->setColor(QColor(255, 255, 255, 96)); + mMajorColorAnimator->setColor(QColor(255, 255, 255, 160)); setupUi(); } @@ -108,6 +111,9 @@ void GridSettingsDialog::setupUi() mColorButton = new ColorAnimatorButton(mColorAnimator.get(), this); form->addRow(tr("Grid Color"), mColorButton); + mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); + form->addRow(tr("Major Line Color"), mMajorColorButton); + mShowGrid = new QCheckBox(tr("Show grid"), this); form->addRow(QString(), mShowGrid); @@ -133,16 +139,29 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mMajorEvery->setValue(settings.majorEvery); mShowGrid->setChecked(settings.show); - if (!mColorAnimator) { - mColorAnimator = enve::make_shared(); - if (mColorButton) { - mColorButton->setColorTarget(mColorAnimator.get()); + 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() : QColor(255, 255, 255, 96); mColorAnimator->setColor(appliedColor); + + const QColor appliedMajorColor = settings.majorColorAnimator + ? settings.majorColorAnimator->getColor() + : QColor(255, 255, 255, 160); + mMajorColorAnimator->setColor(appliedMajorColor); } GridSettings GridSettingsDialog::settings() const @@ -162,6 +181,12 @@ GridSettings GridSettingsDialog::settings() const : QColor(255, 255, 255, 96); result.colorAnimator = enve::make_shared(); result.colorAnimator->setColor(finalColor); + + const QColor finalMajorColor = mMajorColorAnimator + ? mMajorColorAnimator->getColor() + : QColor(255, 255, 255, 160); + result.majorColorAnimator = enve::make_shared(); + result.majorColorAnimator->setColor(finalMajorColor); return result; } diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index d23afc7ac..593613f0b 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -66,7 +66,9 @@ private slots: QCheckBox* mShowGrid; QDialogButtonBox* mButtonBox; ColorAnimatorButton* mColorButton; + ColorAnimatorButton* mMajorColorButton; qsptr mColorAnimator; + qsptr mMajorColorAnimator; bool mSnapEnabled = true; }; From fed233a35aebbc400d67086d292c408fceeb2919 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 16 Oct 2025 10:54:22 +0200 Subject: [PATCH 04/37] Grid: "show grid" is now a menu option --- src/app/GUI/mainwindow.cpp | 6 ++++++ src/app/GUI/mainwindow.h | 1 + src/app/GUI/menu.cpp | 10 +++++++++- src/ui/dialogs/gridsettingsdialog.cpp | 13 +++++-------- src/ui/dialogs/gridsettingsdialog.h | 3 +-- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index ab1db8363..5724c82ca 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -232,6 +232,12 @@ void MainWindow::openGridSettingsDialog() 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); + } + } } void MainWindow::onGridSnapEnabledChanged(bool enabled) diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index ffe1f9eb7..24db625cd 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -259,6 +259,7 @@ class MainWindow : public QMainWindow QAction *mZoomInAction; QAction *mZoomOutAction; QAction *mFitViewAction; + QAction *mShowGridAct; QAction *mSnapToGridAct; QAction *mGridSettingsAct; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index c2776bbd9..5de4a2e05 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -335,6 +335,14 @@ void MainWindow::setupMenuBar() mViewMenu = mMenuBar->addMenu(tr("View", "MenuBar")); + mShowGridAct = mViewMenu->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); + mSnapToGridAct = mViewMenu->addAction(tr("Snap to Grid")); mSnapToGridAct->setCheckable(true); mSnapToGridAct->setChecked(mDocument.gridController().settings.enabled); @@ -963,4 +971,4 @@ void MainWindow::setupMenuScene() tr("Edit Markers"), this, [this]() { openMarkerEditor(); }); -} \ No newline at end of file +} diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 8ea286611..6d5b013f1 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include @@ -53,7 +52,6 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mOriginY(nullptr) , mSnapThreshold(nullptr) , mMajorEvery(nullptr) - , mShowGrid(nullptr) , mButtonBox(nullptr) , mColorButton(nullptr) , mMajorColorButton(nullptr) @@ -114,9 +112,6 @@ void GridSettingsDialog::setupUi() mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); form->addRow(tr("Major Line Color"), mMajorColorButton); - mShowGrid = new QCheckBox(tr("Show grid"), this); - form->addRow(QString(), mShowGrid); - layout->addLayout(form); mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); @@ -137,7 +132,7 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mOriginY->setValue(settings.originY); mSnapThreshold->setValue(settings.snapThresholdPx); mMajorEvery->setValue(settings.majorEvery); - mShowGrid->setChecked(settings.show); + mStoredShow = settings.show; const auto ensureAnimator = [](qsptr& animator, ColorAnimatorButton* button) @@ -174,7 +169,7 @@ GridSettings GridSettingsDialog::settings() const result.originY = mOriginY->value(); result.snapThresholdPx = mSnapThreshold->value(); result.majorEvery = mMajorEvery->value(); - result.show = mShowGrid->isChecked(); + result.show = mStoredShow; const QColor finalColor = mColorAnimator ? mColorAnimator->getColor() @@ -192,5 +187,7 @@ GridSettings GridSettingsDialog::settings() const void GridSettingsDialog::restoreDefaults() { - setSettings(GridSettings{}); + auto defaults = GridSettings{}; + defaults.show = mStoredShow; + setSettings(defaults); } diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 593613f0b..23364acdc 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -31,7 +31,6 @@ class QDoubleSpinBox; class QSpinBox; -class QCheckBox; class QDialogButtonBox; class ColorAnimatorButton; @@ -63,13 +62,13 @@ private slots: QDoubleSpinBox* mOriginY; QSpinBox* mSnapThreshold; QSpinBox* mMajorEvery; - QCheckBox* mShowGrid; QDialogButtonBox* mButtonBox; ColorAnimatorButton* mColorButton; ColorAnimatorButton* mMajorColorButton; qsptr mColorAnimator; qsptr mMajorColorAnimator; bool mSnapEnabled = true; + bool mStoredShow = true; }; #endif // GRIDSETTINGSDIALOG_H From 122e94e6d8c47f70bfb5cb16eccfc99128b9e3f3 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 16 Oct 2025 16:54:53 +0200 Subject: [PATCH 05/37] Grid: working persistence for settings --- src/app/GUI/mainwindow.cpp | 3 + src/core/Private/document.cpp | 104 +++++++++++++++++--------- src/core/Private/document.h | 3 +- src/core/Private/documentrw.cpp | 20 +++++ src/core/Private/esettings.cpp | 9 +++ src/core/Private/esettings.h | 4 + src/ui/dialogs/gridsettingsdialog.cpp | 25 ++++++- src/ui/dialogs/gridsettingsdialog.h | 3 + 8 files changed, 134 insertions(+), 37 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index 5724c82ca..a795f535b 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -226,6 +226,9 @@ void MainWindow::openGridSettingsDialog() auto settings = dialog.settings(); settings.enabled = mDocument.gridController().settings.enabled; mDocument.setGridSettings(settings); + if (dialog.saveAsDefault()) { + mDocument.saveGridSettingsAsDefault(mDocument.gridController().settings); + } } } diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 5d0e61515..ae21119aa 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -93,14 +93,23 @@ static GridSettings sanitizedGridSettings(GridSettings settings) if (settings.sizeY <= 0.0) { settings.sizeY = 1.0; } if (settings.majorEvery < 1) { settings.majorEvery = 1; } if (settings.snapThresholdPx < 0) { settings.snapThresholdPx = 0; } - if (!settings.colorAnimator) { settings.colorAnimator = enve::make_shared(); } - QColor color = settings.colorAnimator->getColor(); - if (!color.isValid()) { color = QColor(255, 255, 255, 96); } - int alpha = color.alpha(); - if (alpha < 0) { alpha = 0; } - if (alpha > 255) { alpha = 255; } - color.setAlpha(alpha); - settings.colorAnimator->setColor(color); + 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(255, 255, 255, 96); + const QColor majorFallback(255, 255, 255, 160); + ensureAnimatorColor(settings.colorAnimator, minorFallback); + ensureAnimatorColor(settings.majorColorAnimator, majorFallback); return settings; } @@ -139,32 +148,61 @@ void Document::loadGridSettingsFromSettings() loaded.enabled = AppSupport::getSettings("grid", "enabled", defaults.enabled).toBool(); loaded.show = AppSupport::getSettings("grid", "show", defaults.show).toBool(); loaded.majorEvery = AppSupport::getSettings("grid", "majorEvery", defaults.majorEvery).toInt(); - const QVariant colorVariant = AppSupport::getSettings("grid", "color", defaults.colorAnimator->getColor()); - QColor storedColor; - if (colorVariant.canConvert()) { - storedColor = colorVariant.value(); - } else { - storedColor = QColor(colorVariant.toString()); + 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()), + QColor(255, 255, 255, 96)); + QColor storedMajor = readColor( + AppSupport::getSettings("grid", "majorColor", defaults.majorColorAnimator->getColor()), + QColor(255, 255, 255, 160)); + if (auto* settingsMgr = eSettings::sInstance) { + storedMinor = settingsMgr->fGridColor; + storedMajor = settingsMgr->fGridMajorColor; } - if (!storedColor.isValid()) { storedColor = QColor(255, 255, 255, 96); } if (!loaded.colorAnimator) { loaded.colorAnimator = enve::make_shared(); } - loaded.colorAnimator->setColor(storedColor); + loaded.colorAnimator->setColor(storedMinor); + if (!loaded.majorColorAnimator) { loaded.majorColorAnimator = enve::make_shared(); } + loaded.majorColorAnimator->setColor(storedMajor); applyGridSettings(loaded, true, true); } -void Document::saveGridSettingsToSettings() const +void Document::saveGridSettingsToSettings(const GridSettings& settings) const { - const auto& s = mGridController.settings; - AppSupport::setSettings("grid", "sizeX", s.sizeX); - AppSupport::setSettings("grid", "sizeY", s.sizeY); - AppSupport::setSettings("grid", "originX", s.originX); - AppSupport::setSettings("grid", "originY", s.originY); - AppSupport::setSettings("grid", "snapThresholdPx", s.snapThresholdPx); - AppSupport::setSettings("grid", "enabled", s.enabled); - AppSupport::setSettings("grid", "show", s.show); - AppSupport::setSettings("grid", "majorEvery", s.majorEvery); - const QColor color = s.colorAnimator ? s.colorAnimator->getColor() : QColor(255, 255, 255, 96); + 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", "majorEvery", settings.majorEvery); + const QColor color = settings.colorAnimator ? settings.colorAnimator->getColor() : QColor(255, 255, 255, 96); + const QColor majorColor = settings.majorColorAnimator ? settings.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); 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() : QColor(255, 255, 255, 96); + settingsMgr->fGridMajorColor = sanitized.majorColorAnimator ? sanitized.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); + settingsMgr->saveKeyToFile("gridColor"); + settingsMgr->saveKeyToFile("gridMajorColor"); + } + saveGridSettingsToSettings(sanitized); } void Document::applyGridSettings(const GridSettings& settings, @@ -173,10 +211,7 @@ void Document::applyGridSettings(const GridSettings& settings, { const GridSettings sanitized = sanitizedGridSettings(settings); const auto previous = mGridController.settings; - if (previous == sanitized) { - if (!skipSave) { saveGridSettingsToSettings(); } - return; - } + if (previous == sanitized) { return; } const bool snapChanged = previous.enabled != sanitized.enabled; const bool showChanged = previous.show != sanitized.show; @@ -188,12 +223,13 @@ void Document::applyGridSettings(const GridSettings& settings, previous.majorEvery != sanitized.majorEvery; const QColor previousColor = previous.colorAnimator ? previous.colorAnimator->getColor() : QColor(); const QColor sanitizedColor = sanitized.colorAnimator ? sanitized.colorAnimator->getColor() : QColor(); - const bool colorChanged = previousColor != sanitizedColor; + 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; mGridController.settings = sanitized; - if (!skipSave) { saveGridSettingsToSettings(); } - if (silent) { return; } emit gridSettingsChanged(mGridController.settings); diff --git a/src/core/Private/document.h b/src/core/Private/document.h index aa3c4bc65..95986c7bd 100644 --- a/src/core/Private/document.h +++ b/src/core/Private/document.h @@ -77,6 +77,7 @@ class CORE_EXPORT Document : public SingleWidgetTarget { void setGridSnapEnabled(bool enabled); void setGridVisible(bool visible); void setGridSettings(const Friction::Core::GridSettings& settings); + void saveGridSettingsAsDefault(const Friction::Core::GridSettings& settings); stdsptr fClipboardContainer; @@ -212,7 +213,7 @@ class CORE_EXPORT Document : public SingleWidgetTarget { void readGridSettings(eReadStream &src); void readGridSettings(const QDomElement& element); void loadGridSettingsFromSettings(); - void saveGridSettingsToSettings() const; + void saveGridSettingsToSettings(const Friction::Core::GridSettings& settings) const; void applyGridSettings(const Friction::Core::GridSettings& settings, bool silent, bool skipSave); diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 90ba869c9..96239ca36 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -64,7 +64,9 @@ void Document::writeGridSettings(eWriteStream &dst) const dst << s.show; dst << s.majorEvery; const QColor color = s.colorAnimator ? s.colorAnimator->getColor() : QColor(255, 255, 255, 96); + const QColor majorColor = s.majorColorAnimator ? s.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); dst << color; + dst << majorColor; } @@ -112,12 +114,20 @@ void Document::readGridSettings(eReadStream &src) src >> settings.majorEvery; QColor color; src >> color; + QColor majorColor = color; + if (src.evFileVersion() >= EvFormat::gridSettingsMajorColor) { + 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); } @@ -181,6 +191,14 @@ void Document::readGridSettings(const QDomElement& element) 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); } @@ -215,7 +233,9 @@ void Document::writeDoxumentXEV(QDomDocument& doc) const { gridSettings.setAttribute("show", grid.show ? "true" : "false"); gridSettings.setAttribute("majorEvery", QString::number(grid.majorEvery)); const QColor gridColor = grid.colorAnimator ? grid.colorAnimator->getColor() : QColor(255, 255, 255, 96); + const QColor gridMajorColor = grid.majorColorAnimator ? grid.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); gridSettings.setAttribute("color", gridColor.name(QColor::HexArgb)); + gridSettings.setAttribute("majorColor", gridMajorColor.name(QColor::HexArgb)); document.appendChild(gridSettings); auto scenes = doc.createElement("Scenes"); diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index a35dbfb76..d2b017564 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -268,6 +268,15 @@ 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", + QColor(255, 255, 255, 96)); + gSettings << std::make_shared( + fGridMajorColor, + "gridMajorColor", + QColor(255, 255, 255, 160)); gSettings << std::make_shared( fObjectKeyframeColor, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 7c4d92118..413a99730 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -152,6 +152,10 @@ class CORE_EXPORT eSettings : public QObject bool fTimelineHighlightRow = true; QColor fTimelineHighlightRowColor = ThemeSupport::getThemeHighlightColor(15); + // Grid default colors + QColor fGridColor = QColor(255, 255, 255, 96); + QColor fGridMajorColor = QColor(255, 255, 255, 160); + QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; QColor fPropertyKeyframeColor; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 6d5b013f1..b285cab3a 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -25,11 +25,13 @@ #include "gridcontroller.h" #include "GUI/coloranimatorbutton.h" +#include "Private/esettings.h" #include #include #include #include +#include #include #include #include @@ -53,14 +55,22 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mSnapThreshold(nullptr) , mMajorEvery(nullptr) , mButtonBox(nullptr) + , mSaveAsDefault(nullptr) , mColorButton(nullptr) , mMajorColorButton(nullptr) , mColorAnimator(enve::make_shared()) , mMajorColorAnimator(enve::make_shared()) , mSnapEnabled(true) { - mColorAnimator->setColor(QColor(255, 255, 255, 96)); - mMajorColorAnimator->setColor(QColor(255, 255, 255, 160)); + const QColor defaultMinor(255, 255, 255, 96); + const QColor defaultMajor(255, 255, 255, 160); + if (auto* settings = eSettings::sInstance) { + mColorAnimator->setColor(settings->fGridColor); + mMajorColorAnimator->setColor(settings->fGridMajorColor); + } else { + mColorAnimator->setColor(defaultMinor); + mMajorColorAnimator->setColor(defaultMajor); + } setupUi(); } @@ -112,6 +122,9 @@ void GridSettingsDialog::setupUi() mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); form->addRow(tr("Major Line Color"), mMajorColorButton); + mSaveAsDefault = new QCheckBox(tr("Save as default"), this); + form->addRow(QString(), mSaveAsDefault); + layout->addLayout(form); mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); @@ -133,6 +146,9 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mSnapThreshold->setValue(settings.snapThresholdPx); mMajorEvery->setValue(settings.majorEvery); mStoredShow = settings.show; + if (mSaveAsDefault) { + mSaveAsDefault->setChecked(false); + } const auto ensureAnimator = [](qsptr& animator, ColorAnimatorButton* button) @@ -191,3 +207,8 @@ void GridSettingsDialog::restoreDefaults() defaults.show = mStoredShow; setSettings(defaults); } + +bool GridSettingsDialog::saveAsDefault() const +{ + return mSaveAsDefault && mSaveAsDefault->isChecked(); +} diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 23364acdc..922898562 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -32,6 +32,7 @@ class QDoubleSpinBox; class QSpinBox; class QDialogButtonBox; +class QCheckBox; class ColorAnimatorButton; namespace Friction { @@ -49,6 +50,7 @@ class GridSettingsDialog : public QDialog void setSettings(const Friction::Core::GridSettings& settings); Friction::Core::GridSettings settings() const; + bool saveAsDefault() const; private slots: void restoreDefaults(); @@ -63,6 +65,7 @@ private slots: QSpinBox* mSnapThreshold; QSpinBox* mMajorEvery; QDialogButtonBox* mButtonBox; + QCheckBox* mSaveAsDefault; ColorAnimatorButton* mColorButton; ColorAnimatorButton* mMajorColorButton; qsptr mColorAnimator; From fb10f827b06ef12f8f0779958d569ce1919ff77d Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 16 Oct 2025 17:39:19 +0200 Subject: [PATCH 06/37] Grid: fix working persistence for settings --- src/core/Private/document.cpp | 2 ++ src/ui/dialogs/gridsettingsdialog.cpp | 22 ++++++++-------------- src/ui/dialogs/gridsettingsdialog.h | 3 --- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index ae21119aa..375f9130e 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -556,6 +556,8 @@ void Document::clear() { removeBookmarkColor(color); } fColors.clear(); + + loadGridSettingsFromSettings(); } void Document::SWT_setupAbstraction(SWT_Abstraction * const abstraction, diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index b285cab3a..3b67f4ffd 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -34,7 +34,6 @@ #include #include #include -#include using Friction::Core::GridSettings; @@ -122,18 +121,20 @@ void GridSettingsDialog::setupUi() mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); form->addRow(tr("Major Line Color"), mMajorColorButton); - mSaveAsDefault = new QCheckBox(tr("Save as default"), this); - form->addRow(QString(), mSaveAsDefault); - layout->addLayout(form); + auto* buttonLayout = new QHBoxLayout(); + mSaveAsDefault = new QCheckBox(tr("Save as default"), this); + buttonLayout->addWidget(mSaveAsDefault); + mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - auto* restoreButton = mButtonBox->addButton(tr("Restore Defaults"), QDialogButtonBox::ResetRole); - connect(restoreButton, &QPushButton::clicked, this, &GridSettingsDialog::restoreDefaults); connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - layout->addWidget(mButtonBox); + buttonLayout->addStretch(); + buttonLayout->addWidget(mButtonBox); + + layout->addLayout(buttonLayout); } void GridSettingsDialog::setSettings(const GridSettings& settings) @@ -201,13 +202,6 @@ GridSettings GridSettingsDialog::settings() const return result; } -void GridSettingsDialog::restoreDefaults() -{ - auto defaults = GridSettings{}; - defaults.show = mStoredShow; - setSettings(defaults); -} - bool GridSettingsDialog::saveAsDefault() const { return mSaveAsDefault && mSaveAsDefault->isChecked(); diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 922898562..21237cecd 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -52,9 +52,6 @@ class GridSettingsDialog : public QDialog Friction::Core::GridSettings settings() const; bool saveAsDefault() const; -private slots: - void restoreDefaults(); - private: void setupUi(); From 8af7fc030a3320eeb59c3d9e7ab96f494f09950a Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 16 Oct 2025 18:13:59 +0200 Subject: [PATCH 07/37] Grid: add option to draw grid on top or behind --- src/core/Private/document.cpp | 10 +++++++++- src/core/Private/esettings.cpp | 4 ++++ src/core/Private/esettings.h | 1 + src/core/canvas.cpp | 12 +++++++++--- src/core/gridcontroller.cpp | 1 + src/core/gridcontroller.h | 1 + src/ui/dialogs/gridsettingsdialog.cpp | 9 +++++++++ src/ui/dialogs/gridsettingsdialog.h | 1 + 8 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 375f9130e..124e6814c 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -139,6 +139,9 @@ void Document::setGridSettings(const GridSettings& settings) void Document::loadGridSettingsFromSettings() { GridSettings defaults; + if (auto* settingsMgr = eSettings::sInstance) { + defaults.drawOnTop = settingsMgr->fGridDrawOnTop; + } GridSettings loaded = defaults; loaded.sizeX = AppSupport::getSettings("grid", "sizeX", defaults.sizeX).toDouble(); loaded.sizeY = AppSupport::getSettings("grid", "sizeY", defaults.sizeY).toDouble(); @@ -147,6 +150,7 @@ void Document::loadGridSettingsFromSettings() 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.majorEvery = AppSupport::getSettings("grid", "majorEvery", defaults.majorEvery).toInt(); auto readColor = [](const QVariant& variant, const QColor& fallback) @@ -186,6 +190,7 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const 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", "majorEvery", settings.majorEvery); const QColor color = settings.colorAnimator ? settings.colorAnimator->getColor() : QColor(255, 255, 255, 96); const QColor majorColor = settings.majorColorAnimator ? settings.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); @@ -199,8 +204,10 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) if (auto* settingsMgr = eSettings::sInstance) { settingsMgr->fGridColor = sanitized.colorAnimator ? sanitized.colorAnimator->getColor() : QColor(255, 255, 255, 96); settingsMgr->fGridMajorColor = sanitized.majorColorAnimator ? sanitized.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); + settingsMgr->fGridDrawOnTop = sanitized.drawOnTop; settingsMgr->saveKeyToFile("gridColor"); settingsMgr->saveKeyToFile("gridMajorColor"); + settingsMgr->saveKeyToFile("gridDrawOnTop"); } saveGridSettingsToSettings(sanitized); } @@ -227,6 +234,7 @@ void Document::applyGridSettings(const GridSettings& settings, 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; @@ -237,7 +245,7 @@ void Document::applyGridSettings(const GridSettings& settings, emit gridSnapEnabledChanged(mGridController.settings.enabled); } - if (showChanged || (mGridController.settings.show && (metricsChanged || colorChanged))) { + if (showChanged || (mGridController.settings.show && (metricsChanged || colorChanged || orderChanged))) { updateScenes(); } } diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index d2b017564..94b88d5b8 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -277,6 +277,10 @@ eSettings::eSettings(const int cpuThreads, fGridMajorColor, "gridMajorColor", QColor(255, 255, 255, 160)); + gSettings << std::make_shared( + fGridDrawOnTop, + "gridDrawOnTop", + true); gSettings << std::make_shared( fObjectKeyframeColor, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 413a99730..733e2c8ab 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -155,6 +155,7 @@ class CORE_EXPORT eSettings : public QObject // Grid default colors QColor fGridColor = QColor(255, 255, 255, 96); QColor fGridMajorColor = QColor(255, 255, 255, 160); + bool fGridDrawOnTop = true; QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index cb0ed7603..abb99daa5 100644 --- a/src/core/canvas.cpp +++ b/src/core/canvas.cpp @@ -280,8 +280,10 @@ void Canvas::renderSk(SkCanvas* const canvas, : QRectF(QPointF(0.0, 0.0), QSizeF(drawRect.width(), drawRect.height())); QRectF gridViewport = worldViewport.normalized(); const qreal gridPixelRatio = haveWorldTransform ? mDevicePixelRatio : pixelRatio; - const bool gridVisible = mDocument.gridController().settings.show && - (!haveWorldTransform || !gridViewport.isEmpty()); + 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); @@ -319,7 +321,7 @@ void Canvas::renderSk(SkCanvas* const canvas, canvas->drawRect(canvasRect, bgPaint); } } - if (gridVisible) { + if (gridVisible && !gridOnTop) { mDocument.gridController().drawGrid(canvas, gridViewport, worldToScreenTransform, gridPixelRatio); } canvas->save(); @@ -343,6 +345,10 @@ void Canvas::renderSk(SkCanvas* const canvas, 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/gridcontroller.cpp b/src/core/gridcontroller.cpp index bc30f3a8b..62f48c5c5 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -132,6 +132,7 @@ bool GridSettings::operator==(const GridSettings& other) const snapThresholdPx == other.snapThresholdPx && enabled == other.enabled && show == other.show && + drawOnTop == other.drawOnTop && majorEvery == other.majorEvery && thisColor == otherColor && thisMajorColor == otherMajorColor; diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 2229165b3..cd5e631ed 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -58,6 +58,7 @@ struct CORE_EXPORT GridSettings { int snapThresholdPx = 8; bool enabled = true; bool show = true; + bool drawOnTop = true; int majorEvery = 5; qsptr colorAnimator; qsptr majorColorAnimator; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 3b67f4ffd..7e5cc16f6 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -55,6 +55,7 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mMajorEvery(nullptr) , mButtonBox(nullptr) , mSaveAsDefault(nullptr) + , mDrawOnTop(nullptr) , mColorButton(nullptr) , mMajorColorButton(nullptr) , mColorAnimator(enve::make_shared()) @@ -121,6 +122,10 @@ void GridSettingsDialog::setupUi() mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); form->addRow(tr("Major Line Color"), mMajorColorButton); + mDrawOnTop = new QCheckBox(tr("Always draw grid above geometry"), this); + mDrawOnTop->setChecked(true); + form->addRow(mDrawOnTop); + layout->addLayout(form); auto* buttonLayout = new QHBoxLayout(); @@ -150,6 +155,9 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) if (mSaveAsDefault) { mSaveAsDefault->setChecked(false); } + if (mDrawOnTop) { + mDrawOnTop->setChecked(settings.drawOnTop); + } const auto ensureAnimator = [](qsptr& animator, ColorAnimatorButton* button) @@ -187,6 +195,7 @@ GridSettings GridSettingsDialog::settings() const result.snapThresholdPx = mSnapThreshold->value(); result.majorEvery = mMajorEvery->value(); result.show = mStoredShow; + result.drawOnTop = mDrawOnTop && mDrawOnTop->isChecked(); const QColor finalColor = mColorAnimator ? mColorAnimator->getColor() diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 21237cecd..cbedb80ea 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -63,6 +63,7 @@ class GridSettingsDialog : public QDialog QSpinBox* mMajorEvery; QDialogButtonBox* mButtonBox; QCheckBox* mSaveAsDefault; + QCheckBox* mDrawOnTop; ColorAnimatorButton* mColorButton; ColorAnimatorButton* mMajorColorButton; qsptr mColorAnimator; From 69e966fe950be21ee230d10469c98293dda91897 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 08:58:56 +0200 Subject: [PATCH 08/37] Grid: View / Grid menu rearranged and Grid settings tweaks --- src/app/GUI/mainwindow.cpp | 8 ++++ src/app/GUI/mainwindow.h | 2 + src/app/GUI/menu.cpp | 61 ++++++++++++++++----------- src/ui/dialogs/gridsettingsdialog.cpp | 54 +++++++++++++++--------- src/ui/dialogs/gridsettingsdialog.h | 7 +-- 5 files changed, 84 insertions(+), 48 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index a795f535b..e2d284765 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -119,8 +119,10 @@ MainWindow::MainWindow(Document& document, , mInvertSelAct(nullptr) , mClearSelAct(nullptr) , mAddKeyAct(nullptr) + , mShowGridAct(nullptr) , mSnapToGridAct(nullptr) , mGridSettingsAct(nullptr) + , mGridDrawOnTopAct(nullptr) , mAddToQueAct(nullptr) , mViewFullScreenAct(nullptr) , mFontWidget(nullptr) @@ -241,6 +243,12 @@ void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& setti mShowGridAct->setChecked(settings.show); } } + if (mGridDrawOnTopAct) { + QSignalBlocker blocker(mGridDrawOnTopAct); + if (mGridDrawOnTopAct->isChecked() != settings.drawOnTop) { + mGridDrawOnTopAct->setChecked(settings.drawOnTop); + } + } } void MainWindow::onGridSnapEnabledChanged(bool enabled) diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 24db625cd..7440df463 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -262,6 +262,7 @@ class MainWindow : public QMainWindow QAction *mShowGridAct; QAction *mSnapToGridAct; QAction *mGridSettingsAct; + QAction *mGridDrawOnTopAct; QAction *mNoneQuality; QAction *mLowQuality; @@ -289,6 +290,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 5de4a2e05..d798a878f 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -335,30 +335,6 @@ void MainWindow::setupMenuBar() mViewMenu = mMenuBar->addMenu(tr("View", "MenuBar")); - mShowGridAct = mViewMenu->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); - - mSnapToGridAct = mViewMenu->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); - - mGridSettingsAct = mViewMenu->addAction(tr("Grid Settings...")); - connect(mGridSettingsAct, &QAction::triggered, this, &MainWindow::openGridSettingsDialog); - cmdAddAction(mGridSettingsAct); - - mViewMenu->addSeparator(); - onGridSettingsChanged(mDocument.gridController().settings); - - mObjectMenu = mMenuBar->addMenu(tr("Object", "MenuBar")); mObjectMenu->addSeparator(); @@ -590,6 +566,43 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mResetZoomAction); + // TODO: custom icon for Grid menu + mGridMenu = mViewMenu->addMenu(QIcon::fromTheme("rectCreate"), tr("Grid", "MenuBar_View")); + + 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); + + 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); + + 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/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 7e5cc16f6..7690fa874 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -25,14 +25,16 @@ #include "gridcontroller.h" #include "GUI/coloranimatorbutton.h" +#include "GUI/global.h" #include "Private/esettings.h" #include #include -#include +#include #include #include #include +#include #include using Friction::Core::GridSettings; @@ -53,9 +55,9 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mOriginY(nullptr) , mSnapThreshold(nullptr) , mMajorEvery(nullptr) - , mButtonBox(nullptr) , mSaveAsDefault(nullptr) - , mDrawOnTop(nullptr) + , mOkButton(nullptr) + , mCancelButton(nullptr) , mColorButton(nullptr) , mMajorColorButton(nullptr) , mColorAnimator(enve::make_shared()) @@ -117,29 +119,41 @@ void GridSettingsDialog::setupUi() form->addRow(tr("Major Line Every"), mMajorEvery); mColorButton = new ColorAnimatorButton(mColorAnimator.get(), this); - form->addRow(tr("Grid Color"), mColorButton); + auto* minorColorContainer = new QWidget(this); + auto* minorColorLayout = new QHBoxLayout(minorColorContainer); + minorColorLayout->setContentsMargins(0, 0, 0, 0); + minorColorLayout->addStretch(); + minorColorLayout->addWidget(mColorButton); + form->addRow(tr("Grid Color"), minorColorContainer); mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); - form->addRow(tr("Major Line Color"), mMajorColorButton); - - mDrawOnTop = new QCheckBox(tr("Always draw grid above geometry"), this); - mDrawOnTop->setChecked(true); - form->addRow(mDrawOnTop); + auto* majorColorContainer = new QWidget(this); + auto* majorColorLayout = new QHBoxLayout(majorColorContainer); + majorColorLayout->setContentsMargins(0, 0, 0, 0); + majorColorLayout->addStretch(); + majorColorLayout->addWidget(mMajorColorButton); + form->addRow(tr("Major Line Color"), majorColorContainer); layout->addLayout(form); + eSizesUI::widget.addSpacing(layout); - auto* buttonLayout = new QHBoxLayout(); mSaveAsDefault = new QCheckBox(tr("Save as default"), this); - buttonLayout->addWidget(mSaveAsDefault); + layout->addWidget(mSaveAsDefault); - mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - connect(mButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - - buttonLayout->addStretch(); - buttonLayout->addWidget(mButtonBox); + 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(this, &QDialog::rejected, this, &QDialog::close); } void GridSettingsDialog::setSettings(const GridSettings& settings) @@ -152,12 +166,10 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mSnapThreshold->setValue(settings.snapThresholdPx); mMajorEvery->setValue(settings.majorEvery); mStoredShow = settings.show; + mStoredDrawOnTop = settings.drawOnTop; if (mSaveAsDefault) { mSaveAsDefault->setChecked(false); } - if (mDrawOnTop) { - mDrawOnTop->setChecked(settings.drawOnTop); - } const auto ensureAnimator = [](qsptr& animator, ColorAnimatorButton* button) @@ -195,7 +207,7 @@ GridSettings GridSettingsDialog::settings() const result.snapThresholdPx = mSnapThreshold->value(); result.majorEvery = mMajorEvery->value(); result.show = mStoredShow; - result.drawOnTop = mDrawOnTop && mDrawOnTop->isChecked(); + result.drawOnTop = mStoredDrawOnTop; const QColor finalColor = mColorAnimator ? mColorAnimator->getColor() diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index cbedb80ea..f029b3fdb 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -31,8 +31,8 @@ class QDoubleSpinBox; class QSpinBox; -class QDialogButtonBox; class QCheckBox; +class QPushButton; class ColorAnimatorButton; namespace Friction { @@ -61,15 +61,16 @@ class GridSettingsDialog : public QDialog QDoubleSpinBox* mOriginY; QSpinBox* mSnapThreshold; QSpinBox* mMajorEvery; - QDialogButtonBox* mButtonBox; QCheckBox* mSaveAsDefault; - QCheckBox* mDrawOnTop; + QPushButton* mOkButton; + QPushButton* mCancelButton; ColorAnimatorButton* mColorButton; ColorAnimatorButton* mMajorColorButton; qsptr mColorAnimator; qsptr mMajorColorAnimator; bool mSnapEnabled = true; bool mStoredShow = true; + bool mStoredDrawOnTop = true; }; #endif // GRIDSETTINGSDIALOG_H From 12d05b7c6fd68995d05e5e67c590124e823b3046 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 10:01:58 +0200 Subject: [PATCH 09/37] Grid: grid visual options get saved for next sessions (show, snap and on top) --- src/core/Private/document.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 124e6814c..1583c831e 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -238,6 +238,10 @@ void Document::applyGridSettings(const GridSettings& settings, mGridController.settings = sanitized; + if (!skipSave) { + saveGridSettingsToSettings(mGridController.settings); + } + if (silent) { return; } emit gridSettingsChanged(mGridController.settings); From 1ea91c1fa0cc02d3ee5b1b135c9be033d3a0af07 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 10:10:18 +0200 Subject: [PATCH 10/37] Grid: settings labels tweaking --- src/ui/dialogs/gridsettingsdialog.cpp | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 7690fa874..c289fe16a 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -84,18 +84,6 @@ void GridSettingsDialog::setupUi() auto* form = new QFormLayout(); form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - mSizeX = new QDoubleSpinBox(this); - mSizeX->setDecimals(2); - mSizeX->setRange(kMinSpacing, kMaxSpacing); - mSizeX->setSingleStep(1.0); - form->addRow(tr("Spacing X"), mSizeX); - - mSizeY = new QDoubleSpinBox(this); - mSizeY->setDecimals(2); - mSizeY->setRange(kMinSpacing, kMaxSpacing); - mSizeY->setSingleStep(1.0); - form->addRow(tr("Spacing Y"), mSizeY); - mOriginX = new QDoubleSpinBox(this); mOriginX->setDecimals(2); mOriginX->setRange(-kOriginRange, kOriginRange); @@ -108,15 +96,27 @@ void GridSettingsDialog::setupUi() mOriginY->setSingleStep(1.0); form->addRow(tr("Origin Y"), mOriginY); + mSizeX = new QDoubleSpinBox(this); + mSizeX->setDecimals(2); + mSizeX->setRange(kMinSpacing, kMaxSpacing); + mSizeX->setSingleStep(1.0); + form->addRow(tr("Spacing X"), mSizeX); + + mSizeY = new QDoubleSpinBox(this); + mSizeY->setDecimals(2); + mSizeY->setRange(kMinSpacing, kMaxSpacing); + mSizeY->setSingleStep(1.0); + form->addRow(tr("Spacing Y"), mSizeY); + mSnapThreshold = new QSpinBox(this); mSnapThreshold->setRange(0, kMaxSnapThreshold); mSnapThreshold->setSingleStep(1); - form->addRow(tr("Snap Threshold (px)"), mSnapThreshold); + form->addRow(tr("Snap radius"), mSnapThreshold); mMajorEvery = new QSpinBox(this); mMajorEvery->setRange(1, kMaxMajorEvery); mMajorEvery->setSingleStep(1); - form->addRow(tr("Major Line Every"), mMajorEvery); + form->addRow(tr("Major line every"), mMajorEvery); mColorButton = new ColorAnimatorButton(mColorAnimator.get(), this); auto* minorColorContainer = new QWidget(this); @@ -124,7 +124,7 @@ void GridSettingsDialog::setupUi() minorColorLayout->setContentsMargins(0, 0, 0, 0); minorColorLayout->addStretch(); minorColorLayout->addWidget(mColorButton); - form->addRow(tr("Grid Color"), minorColorContainer); + form->addRow(tr("Minor grid line color"), minorColorContainer); mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); auto* majorColorContainer = new QWidget(this); @@ -132,7 +132,7 @@ void GridSettingsDialog::setupUi() majorColorLayout->setContentsMargins(0, 0, 0, 0); majorColorLayout->addStretch(); majorColorLayout->addWidget(mMajorColorButton); - form->addRow(tr("Major Line Color"), majorColorContainer); + form->addRow(tr("Major grid line color"), majorColorContainer); layout->addLayout(form); eSizesUI::widget.addSpacing(layout); From 6a36f096b3bb15f835a570299dd4b1035f80047b Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 13:07:02 +0200 Subject: [PATCH 11/37] Grid: added snapping capabilities to nodes and pivot points --- src/core/canvasmouseinteractions.cpp | 44 +++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index a9eef022b..a47ac989b 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -726,13 +726,49 @@ 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(mHasWorldToScreen && + (mDocument.gridController().settings.enabled || forceSnap)) { + const QPointF targetPos = mGridMoveStartPivot + moveBy; + const auto snapped = mDocument.gridController().maybeSnapPivot( + targetPos, mWorldToScreen, forceSnap, bypassSnap); + 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() && mHasWorldToScreen && + (mDocument.gridController().settings.enabled || forceSnap)) { + const QPointF targetPivot = mGridMoveStartPivot + moveBy; + const auto snapped = mDocument.gridController().maybeSnapPivot( + targetPivot, mWorldToScreen, forceSnap, bypassSnap); + if(snapped != targetPivot) { + moveBy = snapped - mGridMoveStartPivot; + } + } + + moveSelectedPointsByAbs(moveBy, mStartTransform); } } From 648e5335c78c23545c9e18d72a6ce6af877a8195 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 17:44:36 +0200 Subject: [PATCH 12/37] Grid: simplify version --- src/core/ReadWrite/evformat.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/ReadWrite/evformat.h b/src/core/ReadWrite/evformat.h index 8fe68fd45..989dc8277 100644 --- a/src/core/ReadWrite/evformat.h +++ b/src/core/ReadWrite/evformat.h @@ -46,8 +46,7 @@ namespace EvFormat { formatOptions2 = 31, subPathOffset = 32, avStretch = 33, - gridSettings = 34, - gridSettingsMajorColor = 35, + grids = 34, nextVersion }; From 79ca1494842a95a93eca09f1e8b12dabe5a06d21 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 17:46:11 +0200 Subject: [PATCH 13/37] Grid: divide "Major line every" in X and Y for more flexibility --- src/core/Private/document.cpp | 41 +++++++++++++++++++++--- src/core/Private/documentrw.cpp | 46 ++++++++++++++++++++++++--- src/core/gridcontroller.cpp | 17 ++++++---- src/core/gridcontroller.h | 3 +- src/ui/dialogs/gridsettingsdialog.cpp | 22 +++++++++---- src/ui/dialogs/gridsettingsdialog.h | 3 +- 6 files changed, 108 insertions(+), 24 deletions(-) diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 1583c831e..68237ccc1 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -91,7 +91,8 @@ 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.majorEvery < 1) { settings.majorEvery = 1; } + 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) @@ -151,7 +152,35 @@ void Document::loadGridSettingsFromSettings() 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.majorEvery = AppSupport::getSettings("grid", "majorEvery", defaults.majorEvery).toInt(); + 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) { @@ -191,7 +220,10 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const AppSupport::setSettings("grid", "enabled", settings.enabled); AppSupport::setSettings("grid", "show", settings.show); AppSupport::setSettings("grid", "drawOnTop", settings.drawOnTop); - AppSupport::setSettings("grid", "majorEvery", settings.majorEvery); + AppSupport::setSettings("grid", "majorEveryX", settings.majorEveryX); + AppSupport::setSettings("grid", "majorEveryY", settings.majorEveryY); + // Maintain legacy key for older installations that still expect a single value. + AppSupport::setSettings("grid", "majorEvery", settings.majorEveryX); const QColor color = settings.colorAnimator ? settings.colorAnimator->getColor() : QColor(255, 255, 255, 96); const QColor majorColor = settings.majorColorAnimator ? settings.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); AppSupport::setSettings("grid", "color", color); @@ -227,7 +259,8 @@ void Document::applyGridSettings(const GridSettings& settings, gridNearlyEqual(previous.sizeY, sanitized.sizeY) == false || gridNearlyEqual(previous.originX, sanitized.originX) == false || gridNearlyEqual(previous.originY, sanitized.originY) == false || - previous.majorEvery != sanitized.majorEvery; + 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(); diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 96239ca36..1f3e05f67 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -62,7 +62,8 @@ void Document::writeGridSettings(eWriteStream &dst) const dst << s.snapThresholdPx; dst << s.enabled; dst << s.show; - dst << s.majorEvery; + dst << s.majorEveryX; + dst << s.majorEveryY; const QColor color = s.colorAnimator ? s.colorAnimator->getColor() : QColor(255, 255, 255, 96); const QColor majorColor = s.majorColorAnimator ? s.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); dst << color; @@ -111,7 +112,15 @@ void Document::readGridSettings(eReadStream &src) bool show = mGridController.settings.show; src >> enabled; src >> show; - src >> settings.majorEvery; + if (src.evFileVersion() >= EvFormat::gridSettingsMajorAxes) { + src >> settings.majorEveryX; + src >> settings.majorEveryY; + } else { + int legacyMajor = settings.majorEveryX; + src >> legacyMajor; + settings.majorEveryX = legacyMajor; + settings.majorEveryY = legacyMajor; + } QColor color; src >> color; QColor majorColor = color; @@ -182,7 +191,33 @@ void Document::readGridSettings(const QDomElement& element) 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"; - settings.majorEvery = element.attribute("majorEvery", QString::number(settings.majorEvery)).toInt(); + 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); @@ -231,7 +266,10 @@ void Document::writeDoxumentXEV(QDomDocument& doc) const { gridSettings.setAttribute("snapThresholdPx", QString::number(grid.snapThresholdPx)); gridSettings.setAttribute("enabled", grid.enabled ? "true" : "false"); gridSettings.setAttribute("show", grid.show ? "true" : "false"); - gridSettings.setAttribute("majorEvery", QString::number(grid.majorEvery)); + gridSettings.setAttribute("majorEveryX", QString::number(grid.majorEveryX)); + gridSettings.setAttribute("majorEveryY", QString::number(grid.majorEveryY)); + // Legacy attribute for older consumers expecting a unified value. + gridSettings.setAttribute("majorEvery", QString::number(grid.majorEveryX)); const QColor gridColor = grid.colorAnimator ? grid.colorAnimator->getColor() : QColor(255, 255, 255, 96); const QColor gridMajorColor = grid.majorColorAnimator ? grid.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); gridSettings.setAttribute("color", gridColor.name(QColor::HexArgb)); diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index 62f48c5c5..1bc20ff76 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -59,7 +59,8 @@ 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.majorEvery < 1) { copy.majorEvery = 1; } + 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) @@ -133,7 +134,8 @@ bool GridSettings::operator==(const GridSettings& other) const enabled == other.enabled && show == other.show && drawOnTop == other.drawOnTop && - majorEvery == other.majorEvery && + majorEveryX == other.majorEveryX && + majorEveryY == other.majorEveryY && thisColor == otherColor && thisMajorColor == otherMajorColor; } @@ -265,12 +267,13 @@ void GridController::forEachGridLine(const QRectF& viewport, const double expandY = std::max(baseView.height(), sizeY); QRectF view = baseView.adjusted(-expandX, -expandY, expandX, expandY); - const int majorEvery = std::max(1, sanitizedSettings.majorEvery); + 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 * majorEvery; - const double majorSpacingY = spacingY * majorEvery; + const double majorSpacingX = spacingX * majorEveryX; + const double majorSpacingY = spacingY * majorEveryY; const double majorAlphaX = fadeFactor(majorSpacingX); const double majorAlphaY = fadeFactor(majorSpacingY); @@ -299,7 +302,7 @@ void GridController::forEachGridLine(const QRectF& viewport, for (double x = xBegin; x <= xEnd; x += sizeX) { const long long index = static_cast( std::llround((x - originX) / sizeX)); - const bool major = (index % majorEvery) == 0; + const bool major = (index % majorEveryX) == 0; const double alpha = major ? majorAlphaX : minorAlphaX; if (!major && alpha <= 0.0) { continue; } const QPointF top(x, view.top()); @@ -313,7 +316,7 @@ void GridController::forEachGridLine(const QRectF& viewport, for (double y = yBegin; y <= yEnd; y += sizeY) { const long long index = static_cast( std::llround((y - originY) / sizeY)); - const bool major = (index % majorEvery) == 0; + const bool major = (index % majorEveryY) == 0; const double alpha = major ? majorAlphaY : minorAlphaY; if (!major && alpha <= 0.0) { continue; } const QPointF left(view.left(), y); diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index cd5e631ed..c70016068 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -59,7 +59,8 @@ struct CORE_EXPORT GridSettings { bool enabled = true; bool show = true; bool drawOnTop = true; - int majorEvery = 5; + int majorEveryX = 5; + int majorEveryY = 5; qsptr colorAnimator; qsptr majorColorAnimator; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index c289fe16a..eca3148c3 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -54,7 +54,8 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mOriginX(nullptr) , mOriginY(nullptr) , mSnapThreshold(nullptr) - , mMajorEvery(nullptr) + , mMajorEveryX(nullptr) + , mMajorEveryY(nullptr) , mSaveAsDefault(nullptr) , mOkButton(nullptr) , mCancelButton(nullptr) @@ -113,10 +114,15 @@ void GridSettingsDialog::setupUi() mSnapThreshold->setSingleStep(1); form->addRow(tr("Snap radius"), mSnapThreshold); - mMajorEvery = new QSpinBox(this); - mMajorEvery->setRange(1, kMaxMajorEvery); - mMajorEvery->setSingleStep(1); - form->addRow(tr("Major line every"), mMajorEvery); + mMajorEveryX = new QSpinBox(this); + mMajorEveryX->setRange(1, kMaxMajorEvery); + mMajorEveryX->setSingleStep(1); + form->addRow(tr("Major line every X"), mMajorEveryX); + + mMajorEveryY = new QSpinBox(this); + mMajorEveryY->setRange(1, kMaxMajorEvery); + mMajorEveryY->setSingleStep(1); + form->addRow(tr("Major line every Y"), mMajorEveryY); mColorButton = new ColorAnimatorButton(mColorAnimator.get(), this); auto* minorColorContainer = new QWidget(this); @@ -164,7 +170,8 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mOriginX->setValue(settings.originX); mOriginY->setValue(settings.originY); mSnapThreshold->setValue(settings.snapThresholdPx); - mMajorEvery->setValue(settings.majorEvery); + mMajorEveryX->setValue(settings.majorEveryX); + mMajorEveryY->setValue(settings.majorEveryY); mStoredShow = settings.show; mStoredDrawOnTop = settings.drawOnTop; if (mSaveAsDefault) { @@ -205,7 +212,8 @@ GridSettings GridSettingsDialog::settings() const result.originX = mOriginX->value(); result.originY = mOriginY->value(); result.snapThresholdPx = mSnapThreshold->value(); - result.majorEvery = mMajorEvery->value(); + result.majorEveryX = mMajorEveryX->value(); + result.majorEveryY = mMajorEveryY->value(); result.show = mStoredShow; result.drawOnTop = mStoredDrawOnTop; diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index f029b3fdb..4e8526fec 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -60,7 +60,8 @@ class GridSettingsDialog : public QDialog QDoubleSpinBox* mOriginX; QDoubleSpinBox* mOriginY; QSpinBox* mSnapThreshold; - QSpinBox* mMajorEvery; + QSpinBox* mMajorEveryX; + QSpinBox* mMajorEveryY; QCheckBox* mSaveAsDefault; QPushButton* mOkButton; QPushButton* mCancelButton; From 18fce0fafceb7dfba124bbcda860a90e2a0f849c Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 18:19:05 +0200 Subject: [PATCH 14/37] Grid: fix simplify version commit --- src/core/Private/documentrw.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 1f3e05f67..574f6fb83 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -112,7 +112,8 @@ void Document::readGridSettings(eReadStream &src) bool show = mGridController.settings.show; src >> enabled; src >> show; - if (src.evFileVersion() >= EvFormat::gridSettingsMajorAxes) { + const int fileVersion = src.evFileVersion(); + if (fileVersion >= EvFormat::grids) { src >> settings.majorEveryX; src >> settings.majorEveryY; } else { @@ -124,7 +125,7 @@ void Document::readGridSettings(eReadStream &src) QColor color; src >> color; QColor majorColor = color; - if (src.evFileVersion() >= EvFormat::gridSettingsMajorColor) { + if (fileVersion >= EvFormat::grids) { src >> majorColor; } settings.enabled = enabled; @@ -149,7 +150,7 @@ void Document::readGradients(eReadStream& src) { } void Document::readScenes(eReadStream& src) { - if (src.evFileVersion() >= EvFormat::gridSettings) { + if (src.evFileVersion() >= EvFormat::grids) { readGridSettings(src); src.readCheckpoint("Error reading grid settings"); } From 6e8b9c43e6d4655bdf2af10fe76d48e6fa97a57e Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 17 Oct 2025 18:48:58 +0200 Subject: [PATCH 15/37] Grid: simplify documentrw.cpp --- src/core/Private/documentrw.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 574f6fb83..606cea855 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -116,11 +116,6 @@ void Document::readGridSettings(eReadStream &src) if (fileVersion >= EvFormat::grids) { src >> settings.majorEveryX; src >> settings.majorEveryY; - } else { - int legacyMajor = settings.majorEveryX; - src >> legacyMajor; - settings.majorEveryX = legacyMajor; - settings.majorEveryY = legacyMajor; } QColor color; src >> color; From 5a328c73eb0855bdb7d11c5b6b845b79c9b6219a Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Tue, 21 Oct 2025 21:41:46 +0200 Subject: [PATCH 16/37] Grid: add rectangle, circle and path now stick to the grid at creation --- src/core/canvas.h | 8 ++++++ src/core/canvashandlesmartpath.cpp | 9 ++++-- src/core/canvasmouseevents.cpp | 22 +++++++++++--- src/core/canvasmouseinteractions.cpp | 43 ++++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/core/canvas.h b/src/core/canvas.h index e88c61ae8..ab4035ea2 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -810,6 +810,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); @@ -831,6 +837,8 @@ class CORE_EXPORT Canvas : public CanvasBase bool mHasWorldToScreen = false; qreal mDevicePixelRatio = 1.0; QPointF mGridMoveStartPivot; + bool mHasCreationPressPos = false; + QPointF mCreationPressPos; bool mDrawnSinceQue = true; diff --git a/src/core/canvashandlesmartpath.cpp b/src/core/canvashandlesmartpath.cpp index edc15603f..d2e5ad747 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,18 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { mCurrentContainer->addContained(newPath); clearBoxesSelection(); addBoxToSelection(newPath.get()); - const auto relPos = newPath->mapAbsPosToRel(e.fPos); + const bool forceSnap = mDocument.gridController().settings.enabled; + const QPointF snappedPos = snapEventPos(e, forceSnap); + 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 bool forceSnap = mDocument.gridController().settings.enabled; + const QPointF snappedPos = snapEventPos(e, forceSnap); + const auto newPoint = mLastEndPoint->actionAddPointAbsPos(snappedPos); //newPoint->startTransform(); setCurrentSmartEndPoint(newPoint); } else if(!mLastEndPoint) { diff --git a/src/core/canvasmouseevents.cpp b/src/core/canvasmouseevents.cpp index c7e3bc9ee..ef8ee8812 100644 --- a/src/core/canvasmouseevents.cpp +++ b/src/core/canvasmouseevents.cpp @@ -145,19 +145,30 @@ void Canvas::mouseMoveEvent(const eMouseEvent &e) } else if(mCurrentMode == CanvasMode::pathCreate) { handleAddSmartPointMouseMove(e); } else if(mCurrentMode == CanvasMode::circleCreate) { + const bool forceSnap = mDocument.gridController().settings.enabled; + const QPointF anchor = mHasCreationPressPos + ? mCreationPressPos + : snapPosToGrid(e.fLastPressPos, e.fModifiers, forceSnap); + const QPointF current = snapEventPos(e, forceSnap); + 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 bool forceSnap = mDocument.gridController().settings.enabled; + const QPointF anchor = mHasCreationPressPos + ? mCreationPressPos + : snapPosToGrid(e.fLastPressPos, e.fModifiers, forceSnap); + const QPointF current = snapEventPos(e, forceSnap); + 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 +228,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 a47ac989b..c6942db66 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -60,6 +60,36 @@ using namespace Friction::Core; +QPointF Canvas::snapPosToGrid(const QPointF& pos, + Qt::KeyboardModifiers modifiers, + bool forceSnap) const +{ + if (!mHasWorldToScreen) { 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 shouldForce = (forceSnap && gridEnabled) || + (modifiers & Qt::ControlModifier); + + if (!gridEnabled && !shouldForce) { return pos; } + + return gridController.maybeSnapPivot(pos, + mWorldToScreen, + shouldForce, + false); +} + +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 +238,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 +305,15 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - newPath->setAbsolutePos(e.fPos); + const bool gridSnapEnabled = mDocument.gridController().settings.enabled; + const QPointF snappedPos = snapEventPos(e, gridSnapEnabled); + 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 +326,15 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - newPath->setAbsolutePos(e.fPos); + const bool gridSnapEnabled = mDocument.gridController().settings.enabled; + const QPointF snappedPos = snapEventPos(e, gridSnapEnabled); + 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); From e1888fc771600c38c1945b1f2b19ca2099dd3d2d Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Tue, 21 Oct 2025 22:11:09 +0200 Subject: [PATCH 17/37] Grid: tangents of "add path" now stick to grid --- src/core/canvashandlesmartpath.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/canvashandlesmartpath.cpp b/src/core/canvashandlesmartpath.cpp index d2e5ad747..043702667 100644 --- a/src/core/canvashandlesmartpath.cpp +++ b/src/core/canvashandlesmartpath.cpp @@ -95,11 +95,13 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { void Canvas::handleAddSmartPointMouseMove(const eMouseEvent &e) { if(!mLastEndPoint) return; if(mStartTransform) mLastEndPoint->startTransform(); + const bool forceSnap = mDocument.gridController().settings.enabled; + const QPointF snappedPos = snapEventPos(e, forceSnap); 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()) { @@ -109,9 +111,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); } } } From 46d128e0504ea2ae24023f117b56b8e056b0d7ba Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Wed, 22 Oct 2025 09:18:04 +0200 Subject: [PATCH 18/37] Grid: simplify default grid color definitions --- src/core/Private/document.cpp | 16 ++++++++-------- src/core/Private/documentrw.cpp | 8 ++++---- src/core/Private/esettings.cpp | 6 ++++-- src/core/Private/esettings.h | 5 +++-- src/core/gridcontroller.cpp | 15 +++++++++------ src/core/gridcontroller.h | 12 ++++++++++-- src/ui/dialogs/gridsettingsdialog.cpp | 12 ++++++------ 7 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 68237ccc1..4fcac884f 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -107,8 +107,8 @@ static GridSettings sanitizedGridSettings(GridSettings settings) animator->setColor(color); return color; }; - const QColor minorFallback(255, 255, 255, 96); - const QColor majorFallback(255, 255, 255, 160); + const QColor minorFallback = GridSettings::defaults().colorAnimator->getColor(); + const QColor majorFallback = GridSettings::defaults().majorColorAnimator->getColor(); ensureAnimatorColor(settings.colorAnimator, minorFallback); ensureAnimatorColor(settings.majorColorAnimator, majorFallback); return settings; @@ -195,10 +195,10 @@ void Document::loadGridSettingsFromSettings() }; QColor storedMinor = readColor( AppSupport::getSettings("grid", "color", defaults.colorAnimator->getColor()), - QColor(255, 255, 255, 96)); + GridSettings::defaults().colorAnimator->getColor()); QColor storedMajor = readColor( AppSupport::getSettings("grid", "majorColor", defaults.majorColorAnimator->getColor()), - QColor(255, 255, 255, 160)); + GridSettings::defaults().majorColorAnimator->getColor()); if (auto* settingsMgr = eSettings::sInstance) { storedMinor = settingsMgr->fGridColor; storedMajor = settingsMgr->fGridMajorColor; @@ -224,8 +224,8 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const AppSupport::setSettings("grid", "majorEveryY", settings.majorEveryY); // Maintain legacy key for older installations that still expect a single value. AppSupport::setSettings("grid", "majorEvery", settings.majorEveryX); - const QColor color = settings.colorAnimator ? settings.colorAnimator->getColor() : QColor(255, 255, 255, 96); - const QColor majorColor = settings.majorColorAnimator ? settings.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); + 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); } @@ -234,8 +234,8 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) { const GridSettings sanitized = sanitizedGridSettings(settings); if (auto* settingsMgr = eSettings::sInstance) { - settingsMgr->fGridColor = sanitized.colorAnimator ? sanitized.colorAnimator->getColor() : QColor(255, 255, 255, 96); - settingsMgr->fGridMajorColor = sanitized.majorColorAnimator ? sanitized.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); + 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->saveKeyToFile("gridColor"); settingsMgr->saveKeyToFile("gridMajorColor"); diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 606cea855..31817ffde 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -64,8 +64,8 @@ void Document::writeGridSettings(eWriteStream &dst) const dst << s.show; dst << s.majorEveryX; dst << s.majorEveryY; - const QColor color = s.colorAnimator ? s.colorAnimator->getColor() : QColor(255, 255, 255, 96); - const QColor majorColor = s.majorColorAnimator ? s.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); + 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; } @@ -266,8 +266,8 @@ void Document::writeDoxumentXEV(QDomDocument& doc) const { gridSettings.setAttribute("majorEveryY", QString::number(grid.majorEveryY)); // Legacy attribute for older consumers expecting a unified value. gridSettings.setAttribute("majorEvery", QString::number(grid.majorEveryX)); - const QColor gridColor = grid.colorAnimator ? grid.colorAnimator->getColor() : QColor(255, 255, 255, 96); - const QColor gridMajorColor = grid.majorColorAnimator ? grid.majorColorAnimator->getColor() : QColor(255, 255, 255, 160); + 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); diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index 94b88d5b8..12f5ef9b1 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" @@ -272,11 +274,11 @@ eSettings::eSettings(const int cpuThreads, gSettings << std::make_shared( fGridColor, "gridColor", - QColor(255, 255, 255, 96)); + Friction::Core::GridSettings::defaults().colorAnimator->getColor()); gSettings << std::make_shared( fGridMajorColor, "gridMajorColor", - QColor(255, 255, 255, 160)); + Friction::Core::GridSettings::defaults().majorColorAnimator->getColor()); gSettings << std::make_shared( fGridDrawOnTop, "gridDrawOnTop", diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 733e2c8ab..a7dff1c6b 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" @@ -153,8 +154,8 @@ class CORE_EXPORT eSettings : public QObject QColor fTimelineHighlightRowColor = ThemeSupport::getThemeHighlightColor(15); // Grid default colors - QColor fGridColor = QColor(255, 255, 255, 96); - QColor fGridMajorColor = QColor(255, 255, 255, 160); + QColor fGridColor = Friction::Core::GridSettings::defaults().colorAnimator->getColor(); + QColor fGridMajorColor = Friction::Core::GridSettings::defaults().majorColorAnimator->getColor(); bool fGridDrawOnTop = true; QColor fObjectKeyframeColor; diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index 1bc20ff76..f40253562 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -77,8 +77,9 @@ GridSettings sanitizeSettings(const GridSettings& in) animator->setColor(color); return color; }; - const QColor minorFallback(255, 255, 255, 96); - const QColor majorFallback(255, 255, 255, 160); + 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; @@ -151,12 +152,13 @@ void GridController::drawGrid(QPainter* painter, const GridSettings sanitizedSettings = sanitizeSettings(settings); if (!painter || !sanitizedSettings.show) { return; } + const auto& defaults = GridSettings::defaults(); const QColor minorBase = sanitizedSettings.colorAnimator ? sanitizedSettings.colorAnimator->getColor() - : QColor(255, 255, 255, 96); + : defaults.colorAnimator->getColor(); const QColor majorBase = sanitizedSettings.majorColorAnimator ? sanitizedSettings.majorColorAnimator->getColor() - : minorBase; + : defaults.majorColorAnimator->getColor(); auto drawLine = [&](const QPointF& a, const QPointF& b, @@ -183,12 +185,13 @@ void GridController::drawGrid(SkCanvas* canvas, const GridSettings sanitizedSettings = sanitizeSettings(settings); if (!canvas || !sanitizedSettings.show) { return; } + const auto& defaults = GridSettings::defaults(); const QColor minorBase = sanitizedSettings.colorAnimator ? sanitizedSettings.colorAnimator->getColor() - : QColor(255, 255, 255, 96); + : defaults.colorAnimator->getColor(); const QColor majorBase = sanitizedSettings.majorColorAnimator ? sanitizedSettings.majorColorAnimator->getColor() - : minorBase; + : defaults.majorColorAnimator->getColor(); const float strokeWidth = static_cast( devicePixelRatio / effectiveScale(worldToScreen)); diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index c70016068..d82f1915e 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -47,10 +47,12 @@ struct CORE_EXPORT GridSettings { : colorAnimator(enve::make_shared()) , majorColorAnimator(enve::make_shared()) { - colorAnimator->setColor(QColor(255, 255, 255, 96)); - majorColorAnimator->setColor(QColor(255, 255, 255, 160)); + colorAnimator->setColor(QColor(128, 127, 255, 75)); + majorColorAnimator->setColor(QColor(255, 127, 234, 125)); } + static const GridSettings& defaults(); + double sizeX = 50.0; double sizeY = 50.0; double originX = 0.0; @@ -68,6 +70,12 @@ struct CORE_EXPORT GridSettings { 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; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index eca3148c3..26cf25640 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -65,8 +65,8 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mMajorColorAnimator(enve::make_shared()) , mSnapEnabled(true) { - const QColor defaultMinor(255, 255, 255, 96); - const QColor defaultMajor(255, 255, 255, 160); + 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); @@ -194,12 +194,12 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) const QColor appliedColor = settings.colorAnimator ? settings.colorAnimator->getColor() - : QColor(255, 255, 255, 96); + : GridSettings::defaults().colorAnimator->getColor(); mColorAnimator->setColor(appliedColor); const QColor appliedMajorColor = settings.majorColorAnimator ? settings.majorColorAnimator->getColor() - : QColor(255, 255, 255, 160); + : GridSettings::defaults().majorColorAnimator->getColor(); mMajorColorAnimator->setColor(appliedMajorColor); } @@ -219,13 +219,13 @@ GridSettings GridSettingsDialog::settings() const const QColor finalColor = mColorAnimator ? mColorAnimator->getColor() - : QColor(255, 255, 255, 96); + : GridSettings::defaults().colorAnimator->getColor(); result.colorAnimator = enve::make_shared(); result.colorAnimator->setColor(finalColor); const QColor finalMajorColor = mMajorColorAnimator ? mMajorColorAnimator->getColor() - : QColor(255, 255, 255, 160); + : GridSettings::defaults().majorColorAnimator->getColor(); result.majorColorAnimator = enve::make_shared(); result.majorColorAnimator->setColor(finalMajorColor); return result; From d3a7d0b97a5d46a7385eae717629f125e87089ce Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Wed, 22 Oct 2025 11:27:43 +0200 Subject: [PATCH 19/37] Grid: rearrange dialog parameters --- src/app/GUI/mainwindow.cpp | 8 +++ src/ui/dialogs/gridsettingsdialog.cpp | 74 +++++++++++++++++++-------- src/ui/dialogs/gridsettingsdialog.h | 4 ++ 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index e2d284765..617c0d329 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -224,6 +224,14 @@ void MainWindow::openGridSettingsDialog() GridSettingsDialog dialog(this); 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); + } + }); if (dialog.exec() == QDialog::Accepted) { auto settings = dialog.settings(); settings.enabled = mDocument.gridController().settings.enabled; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 26cf25640..bb3d52501 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include using Friction::Core::GridSettings; @@ -57,6 +58,7 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mMajorEveryX(nullptr) , mMajorEveryY(nullptr) , mSaveAsDefault(nullptr) + , mApplyButton(nullptr) , mOkButton(nullptr) , mCancelButton(nullptr) , mColorButton(nullptr) @@ -86,28 +88,42 @@ void GridSettingsDialog::setupUi() form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); mOriginX = new QDoubleSpinBox(this); - mOriginX->setDecimals(2); + mOriginX->setDecimals(0); mOriginX->setRange(-kOriginRange, kOriginRange); mOriginX->setSingleStep(1.0); - form->addRow(tr("Origin X"), mOriginX); - + mOriginX->setToolTip(tr("Horizontal origin offset")); mOriginY = new QDoubleSpinBox(this); - mOriginY->setDecimals(2); + mOriginY->setDecimals(0); mOriginY->setRange(-kOriginRange, kOriginRange); mOriginY->setSingleStep(1.0); - form->addRow(tr("Origin Y"), mOriginY); + mOriginY->setToolTip(tr("Vertical origin offset")); + auto* originContainer = new QWidget(this); + auto* originLayout = new QHBoxLayout(originContainer); + originLayout->setContentsMargins(0, 0, 0, 0); + originLayout->setSpacing(8); + originLayout->addWidget(mOriginX); + originLayout->addSpacing(12); + originLayout->addWidget(mOriginY); + form->addRow(tr("Origin"), originContainer); mSizeX = new QDoubleSpinBox(this); - mSizeX->setDecimals(2); + mSizeX->setDecimals(0); mSizeX->setRange(kMinSpacing, kMaxSpacing); mSizeX->setSingleStep(1.0); - form->addRow(tr("Spacing X"), mSizeX); - + mSizeX->setToolTip(tr("Horizontal grid spacing")); mSizeY = new QDoubleSpinBox(this); - mSizeY->setDecimals(2); + mSizeY->setDecimals(0); mSizeY->setRange(kMinSpacing, kMaxSpacing); mSizeY->setSingleStep(1.0); - form->addRow(tr("Spacing Y"), mSizeY); + mSizeY->setToolTip(tr("Vertical grid spacing")); + auto* spacingContainer = new QWidget(this); + auto* spacingLayout = new QHBoxLayout(spacingContainer); + spacingLayout->setContentsMargins(0, 0, 0, 0); + spacingLayout->setSpacing(8); + spacingLayout->addWidget(mSizeX); + spacingLayout->addSpacing(12); + spacingLayout->addWidget(mSizeY); + form->addRow(tr("Spacing"), spacingContainer); mSnapThreshold = new QSpinBox(this); mSnapThreshold->setRange(0, kMaxSnapThreshold); @@ -117,12 +133,27 @@ void GridSettingsDialog::setupUi() mMajorEveryX = new QSpinBox(this); mMajorEveryX->setRange(1, kMaxMajorEvery); mMajorEveryX->setSingleStep(1); - form->addRow(tr("Major line every X"), mMajorEveryX); - + mMajorEveryX->setToolTip(tr("Horizontal major grid line interval")); mMajorEveryY = new QSpinBox(this); mMajorEveryY->setRange(1, kMaxMajorEvery); mMajorEveryY->setSingleStep(1); - form->addRow(tr("Major line every Y"), mMajorEveryY); + mMajorEveryY->setToolTip(tr("Vertical major grid line interval")); + auto* majorEveryContainer = new QWidget(this); + auto* majorEveryLayout = new QHBoxLayout(majorEveryContainer); + majorEveryLayout->setContentsMargins(0, 0, 0, 0); + majorEveryLayout->setSpacing(8); + majorEveryLayout->addWidget(mMajorEveryX); + majorEveryLayout->addSpacing(12); + majorEveryLayout->addWidget(mMajorEveryY); + form->addRow(tr("Major line every"), majorEveryContainer); + + mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); + auto* majorColorContainer = new QWidget(this); + auto* majorColorLayout = new QHBoxLayout(majorColorContainer); + majorColorLayout->setContentsMargins(0, 0, 0, 0); + majorColorLayout->addStretch(); + majorColorLayout->addWidget(mMajorColorButton); + form->addRow(tr("Major line color"), majorColorContainer); mColorButton = new ColorAnimatorButton(mColorAnimator.get(), this); auto* minorColorContainer = new QWidget(this); @@ -130,15 +161,13 @@ void GridSettingsDialog::setupUi() minorColorLayout->setContentsMargins(0, 0, 0, 0); minorColorLayout->addStretch(); minorColorLayout->addWidget(mColorButton); - form->addRow(tr("Minor grid line color"), minorColorContainer); + form->addRow(tr("Minor line color"), minorColorContainer); - mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); - auto* majorColorContainer = new QWidget(this); - auto* majorColorLayout = new QHBoxLayout(majorColorContainer); - majorColorLayout->setContentsMargins(0, 0, 0, 0); - majorColorLayout->addStretch(); - majorColorLayout->addWidget(mMajorColorButton); - form->addRow(tr("Major grid line color"), majorColorContainer); + mApplyButton = new QPushButton(QIcon::fromTheme("dialog-apply"), tr("Apply"), this); + mApplyButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + mApplyButton->setAutoDefault(false); + mApplyButton->setDefault(false); + form->addRow(mApplyButton); layout->addLayout(form); eSizesUI::widget.addSpacing(layout); @@ -159,6 +188,9 @@ void GridSettingsDialog::setupUi() 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); } diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 4e8526fec..10aa94423 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -52,6 +52,9 @@ class GridSettingsDialog : public QDialog Friction::Core::GridSettings settings() const; bool saveAsDefault() const; +signals: + void applyRequested(Friction::Core::GridSettings settings, bool saveAsDefault); + private: void setupUi(); @@ -63,6 +66,7 @@ class GridSettingsDialog : public QDialog QSpinBox* mMajorEveryX; QSpinBox* mMajorEveryY; QCheckBox* mSaveAsDefault; + QPushButton* mApplyButton; QPushButton* mOkButton; QPushButton* mCancelButton; ColorAnimatorButton* mColorButton; From f58f9b738519562759d13852d197e3eb18864560 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Wed, 22 Oct 2025 12:13:24 +0200 Subject: [PATCH 20/37] Grid: update default values --- src/core/gridcontroller.h | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index d82f1915e..3dc80bc29 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -53,16 +53,17 @@ struct CORE_EXPORT GridSettings { static const GridSettings& defaults(); - double sizeX = 50.0; - double sizeY = 50.0; - double originX = 0.0; - double originY = 0.0; - int snapThresholdPx = 8; - bool enabled = true; - bool show = true; - bool drawOnTop = true; - int majorEveryX = 5; - int majorEveryY = 5; + double sizeX = 40.0; + double sizeY = 40.0; + double originX = 640.0; + double originY = 540.0; + int snapThresholdPx = 40; + bool enabled = false; + bool show = false; + bool drawOnTop = false; + bool snapToCanvas = false; + int majorEveryX = 8; + int majorEveryY = 8; qsptr colorAnimator; qsptr majorColorAnimator; From 58191234b1f770e872fa6b07f77983c2beaf2272 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Wed, 22 Oct 2025 13:10:50 +0200 Subject: [PATCH 21/37] Grid: add canvas corners, midpoints and center as snapping points --- src/app/GUI/mainwindow.cpp | 6 ++ src/app/GUI/mainwindow.h | 1 + src/app/GUI/menu.cpp | 28 ++++++--- src/core/Private/document.cpp | 6 +- src/core/Private/esettings.cpp | 4 ++ src/core/Private/esettings.h | 1 + src/core/canvasmouseinteractions.cpp | 53 ++++++++++++++--- src/core/gridcontroller.cpp | 84 ++++++++++++++++++++++----- src/core/gridcontroller.h | 3 +- src/ui/dialogs/gridsettingsdialog.cpp | 3 + src/ui/dialogs/gridsettingsdialog.h | 1 + 11 files changed, 159 insertions(+), 31 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index 617c0d329..4ea3c8b9b 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -251,6 +251,12 @@ void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& setti mShowGridAct->setChecked(settings.show); } } + if (mSnapToCanvasAct) { + QSignalBlocker blocker(mSnapToCanvasAct); + if (mSnapToCanvasAct->isChecked() != settings.snapToCanvas) { + mSnapToCanvasAct->setChecked(settings.snapToCanvas); + } + } if (mGridDrawOnTopAct) { QSignalBlocker blocker(mGridDrawOnTopAct); if (mGridDrawOnTopAct->isChecked() != settings.drawOnTop) { diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 7440df463..90933b14c 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -261,6 +261,7 @@ class MainWindow : public QMainWindow QAction *mFitViewAction; QAction *mShowGridAct; QAction *mSnapToGridAct; + QAction *mSnapToCanvasAct; QAction *mGridSettingsAct; QAction *mGridDrawOnTopAct; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index d798a878f..3487e4fd5 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -567,15 +567,19 @@ void MainWindow::setupMenuBar() cmdAddAction(mResetZoomAction); // TODO: custom icon for Grid menu - mGridMenu = mViewMenu->addMenu(QIcon::fromTheme("rectCreate"), tr("Grid", "MenuBar_View")); + mGridMenu = mViewMenu->addMenu(QIcon::fromTheme("rectCreate"), + tr("Grid && Snapping", "MenuBar_View")); - 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); + 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(mShowGridAct); + cmdAddAction(mSnapToCanvasAct); mSnapToGridAct = mGridMenu->addAction(tr("Snap to Grid")); mSnapToGridAct->setCheckable(true); @@ -585,6 +589,16 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapToGridAct); + 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) { diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 4fcac884f..7bb04a2f1 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -142,6 +142,7 @@ void Document::loadGridSettingsFromSettings() GridSettings defaults; if (auto* settingsMgr = eSettings::sInstance) { defaults.drawOnTop = settingsMgr->fGridDrawOnTop; + defaults.snapToCanvas = settingsMgr->fGridSnapToCanvas; } GridSettings loaded = defaults; loaded.sizeX = AppSupport::getSettings("grid", "sizeX", defaults.sizeX).toDouble(); @@ -152,6 +153,7 @@ void Document::loadGridSettingsFromSettings() 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(); auto readMajorEvery = [](const QString& key, int fallback, bool& found) @@ -220,9 +222,9 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const 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", "majorEveryX", settings.majorEveryX); AppSupport::setSettings("grid", "majorEveryY", settings.majorEveryY); - // Maintain legacy key for older installations that still expect a single value. AppSupport::setSettings("grid", "majorEvery", settings.majorEveryX); const QColor color = settings.colorAnimator ? settings.colorAnimator->getColor() : GridSettings::defaults().colorAnimator->getColor(); const QColor majorColor = settings.majorColorAnimator ? settings.majorColorAnimator->getColor() : GridSettings::defaults().majorColorAnimator->getColor(); @@ -237,9 +239,11 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) 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->saveKeyToFile("gridColor"); settingsMgr->saveKeyToFile("gridMajorColor"); settingsMgr->saveKeyToFile("gridDrawOnTop"); + settingsMgr->saveKeyToFile("gridSnapToCanvas"); } saveGridSettingsToSettings(sanitized); } diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index 12f5ef9b1..3993c6ef7 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -283,6 +283,10 @@ eSettings::eSettings(const int cpuThreads, fGridDrawOnTop, "gridDrawOnTop", true); + gSettings << std::make_shared( + fGridSnapToCanvas, + "gridSnapToCanvas", + false); gSettings << std::make_shared( fObjectKeyframeColor, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index a7dff1c6b..f055a5dad 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -157,6 +157,7 @@ class CORE_EXPORT eSettings : public QObject QColor fGridColor = Friction::Core::GridSettings::defaults().colorAnimator->getColor(); QColor fGridMajorColor = Friction::Core::GridSettings::defaults().majorColorAnimator->getColor(); bool fGridDrawOnTop = true; + bool fGridSnapToCanvas = false; QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index c6942db66..08e6b38da 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -73,15 +73,25 @@ QPointF Canvas::snapPosToGrid(const QPointF& pos, if (bypassSnap) { return pos; } const bool gridEnabled = settings.enabled; - const bool shouldForce = (forceSnap && gridEnabled) || + const bool canvasSnapEnabled = settings.snapToCanvas; + const bool hasSnapSource = gridEnabled || canvasSnapEnabled; + const bool shouldForce = (forceSnap && hasSnapSource) || (modifiers & Qt::ControlModifier); - if (!gridEnabled && !shouldForce) { return pos; } + 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); + false, + canvasRectPtr); } QPointF Canvas::snapEventPos(const eMouseEvent& e, @@ -774,11 +784,19 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; + const auto& gridSettings = mDocument.gridController().settings; + const bool snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas; if(mHasWorldToScreen && - (mDocument.gridController().settings.enabled || forceSnap)) { + (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); + targetPos, mWorldToScreen, forceSnap, bypassSnap, canvasPtr); if(snapped != targetPos) { moveBy = snapped - mGridMoveStartPivot; } @@ -797,11 +815,19 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; + const auto& gridSettings = mDocument.gridController().settings; + const bool snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas; if(!mSelectedPoints_d.isEmpty() && mHasWorldToScreen && - (mDocument.gridController().settings.enabled || forceSnap)) { + (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); + targetPivot, mWorldToScreen, forceSnap, bypassSnap, canvasPtr); if(snapped != targetPivot) { moveBy = snapped - mGridMoveStartPivot; } @@ -965,13 +991,22 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { auto moveBy = getMoveByValueForEvent(e); const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; + const auto& gridSettings = mDocument.gridController().settings; + const bool snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas; if (!mSelectedBoxes.isEmpty() && mHasWorldToScreen && - (mDocument.gridController().settings.enabled || forceSnap)) { + (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); + bypassSnap, + canvasPtr); if (snapped != targetPivot) { moveBy = snapped - mGridMoveStartPivot; } diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index f40253562..04eb79769 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -36,6 +36,8 @@ #include #include +#include +#include using namespace Friction::Core; @@ -135,6 +137,7 @@ bool GridSettings::operator==(const GridSettings& other) const enabled == other.enabled && show == other.show && drawOnTop == other.drawOnTop && + snapToCanvas == other.snapToCanvas && majorEveryX == other.majorEveryX && majorEveryY == other.majorEveryY && thisColor == otherColor && @@ -221,32 +224,87 @@ void GridController::drawGrid(SkCanvas* canvas, QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, const QTransform& worldToScreen, const bool forceSnap, - const bool bypassSnap) const + const bool bypassSnap, + const QRectF* canvasRectWorld) const { const GridSettings sanitizedSettings = sanitizeSettings(settings); - if (!sanitizedSettings.enabled || bypassSnap) { + const bool snapSourcesEnabled = sanitizedSettings.enabled || + (sanitizedSettings.snapToCanvas && canvasRectWorld); + if ((!snapSourcesEnabled && !forceSnap) || bypassSnap) { return pivotWorld; } const double sizeX = sanitizedSettings.sizeX; const double sizeY = sanitizedSettings.sizeY; - if (sizeX <= 0.0 || sizeY <= 0.0) { return pivotWorld; } + const bool hasGrid = sizeX > 0.0 && sizeY > 0.0; - const double gx = sanitizedSettings.originX + - std::round((pivotWorld.x() - sanitizedSettings.originX) / sizeX) * sizeX; - const double gy = sanitizedSettings.originY + - std::round((pivotWorld.y() - sanitizedSettings.originY) / sizeY) * sizeY; - const QPointF snapped(gx, gy); + const bool canUseCanvas = sanitizedSettings.snapToCanvas && canvasRectWorld; + QRectF normalizedCanvas; + if (canUseCanvas) { + normalizedCanvas = canvasRectWorld->normalized(); + } + const bool hasCanvasTargets = canUseCanvas && !normalizedCanvas.isEmpty(); + + if (!hasGrid && !hasCanvasTargets) { + return pivotWorld; + } - if (forceSnap) { return snapped; } + std::vector candidates; + candidates.reserve(10); + + if (hasGrid && (sanitizedSettings.enabled || forceSnap)) { + const double gx = sanitizedSettings.originX + + std::round((pivotWorld.x() - sanitizedSettings.originX) / sizeX) * sizeX; + const double gy = sanitizedSettings.originY + + std::round((pivotWorld.y() - sanitizedSettings.originY) / sizeY) * sizeY; + candidates.emplace_back(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; + + candidates.emplace_back(left, top); + candidates.emplace_back(right, top); + candidates.emplace_back(left, bottom); + candidates.emplace_back(right, bottom); + + candidates.emplace_back(midX, top); + candidates.emplace_back(midX, bottom); + candidates.emplace_back(left, midY); + candidates.emplace_back(right, midY); + + candidates.emplace_back(midX, midY); + } + + if (candidates.empty()) { + return pivotWorld; + } const QPointF screenPivot = worldToScreen.map(pivotWorld); - const QPointF screenSnap = worldToScreen.map(snapped); - const double dist = QLineF(screenPivot, screenSnap).length(); + QPointF bestCandidate = pivotWorld; + double bestDistance = std::numeric_limits::infinity(); + + for (const auto& candidate : candidates) { + const QPointF screenCandidate = worldToScreen.map(candidate); + const double candidateDistance = QLineF(screenPivot, screenCandidate).length(); + if (candidateDistance < bestDistance) { + bestDistance = candidateDistance; + bestCandidate = candidate; + } + } + + if (forceSnap) { + return bestCandidate; + } - if (dist <= sanitizedSettings.snapThresholdPx) { - return snapped; + if (bestDistance <= sanitizedSettings.snapThresholdPx) { + return bestCandidate; } return pivotWorld; } diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 3dc80bc29..0b1483fe9 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -93,7 +93,8 @@ class CORE_EXPORT GridController { QPointF maybeSnapPivot(const QPointF& pivotWorld, const QTransform& worldToScreen, bool forceSnap, - bool bypassSnap) const; + bool bypassSnap, + const QRectF* canvasRectWorld = nullptr) const; private: enum class Orientation { diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index bb3d52501..8b825d9b0 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -66,6 +66,7 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mColorAnimator(enve::make_shared()) , mMajorColorAnimator(enve::make_shared()) , mSnapEnabled(true) + , mStoredSnapToCanvas(false) { const QColor defaultMinor = GridSettings::defaults().colorAnimator->getColor(); const QColor defaultMajor = GridSettings::defaults().majorColorAnimator->getColor(); @@ -202,6 +203,7 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mOriginX->setValue(settings.originX); mOriginY->setValue(settings.originY); mSnapThreshold->setValue(settings.snapThresholdPx); + mStoredSnapToCanvas = settings.snapToCanvas; mMajorEveryX->setValue(settings.majorEveryX); mMajorEveryY->setValue(settings.majorEveryY); mStoredShow = settings.show; @@ -244,6 +246,7 @@ GridSettings GridSettingsDialog::settings() const result.originX = mOriginX->value(); result.originY = mOriginY->value(); result.snapThresholdPx = mSnapThreshold->value(); + result.snapToCanvas = mStoredSnapToCanvas; result.majorEveryX = mMajorEveryX->value(); result.majorEveryY = mMajorEveryY->value(); result.show = mStoredShow; diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 10aa94423..e40d40244 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -76,6 +76,7 @@ class GridSettingsDialog : public QDialog bool mSnapEnabled = true; bool mStoredShow = true; bool mStoredDrawOnTop = true; + bool mStoredSnapToCanvas = false; }; #endif // GRIDSETTINGSDIALOG_H From 871547acc0682eb76348b19dcfa06fe92fa82a84 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Wed, 22 Oct 2025 16:19:09 +0200 Subject: [PATCH 22/37] Grid: more Grid Settings dialog tweaking --- src/ui/dialogs/gridsettingsdialog.cpp | 83 ++++++++++++++------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 8b825d9b0..f6ebbba03 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -28,7 +28,7 @@ #include "GUI/global.h" #include "Private/esettings.h" -#include +#include #include #include #include @@ -37,6 +37,7 @@ #include #include #include +#include using Friction::Core::GridSettings; @@ -85,8 +86,22 @@ void GridSettingsDialog::setupUi() setWindowTitle(tr("Grid Settings")); auto* layout = new QVBoxLayout(this); - auto* form = new QFormLayout(); - form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + 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); @@ -98,14 +113,10 @@ void GridSettingsDialog::setupUi() mOriginY->setRange(-kOriginRange, kOriginRange); mOriginY->setSingleStep(1.0); mOriginY->setToolTip(tr("Vertical origin offset")); - auto* originContainer = new QWidget(this); - auto* originLayout = new QHBoxLayout(originContainer); - originLayout->setContentsMargins(0, 0, 0, 0); - originLayout->setSpacing(8); - originLayout->addWidget(mOriginX); - originLayout->addSpacing(12); - originLayout->addWidget(mOriginY); - form->addRow(tr("Origin"), originContainer); + addLabel(formRow, tr("Origin"), mOriginX); + form->addWidget(mOriginX, formRow, 1); + form->addWidget(mOriginY, formRow, 2); + ++formRow; mSizeX = new QDoubleSpinBox(this); mSizeX->setDecimals(0); @@ -117,19 +128,17 @@ void GridSettingsDialog::setupUi() mSizeY->setRange(kMinSpacing, kMaxSpacing); mSizeY->setSingleStep(1.0); mSizeY->setToolTip(tr("Vertical grid spacing")); - auto* spacingContainer = new QWidget(this); - auto* spacingLayout = new QHBoxLayout(spacingContainer); - spacingLayout->setContentsMargins(0, 0, 0, 0); - spacingLayout->setSpacing(8); - spacingLayout->addWidget(mSizeX); - spacingLayout->addSpacing(12); - spacingLayout->addWidget(mSizeY); - form->addRow(tr("Spacing"), spacingContainer); + 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); - form->addRow(tr("Snap radius"), mSnapThreshold); + addLabel(formRow, tr("Snap radius"), mSnapThreshold); + form->addWidget(mSnapThreshold, formRow, 1, 1, 2); + ++formRow; mMajorEveryX = new QSpinBox(this); mMajorEveryX->setRange(1, kMaxMajorEvery); @@ -139,36 +148,28 @@ void GridSettingsDialog::setupUi() mMajorEveryY->setRange(1, kMaxMajorEvery); mMajorEveryY->setSingleStep(1); mMajorEveryY->setToolTip(tr("Vertical major grid line interval")); - auto* majorEveryContainer = new QWidget(this); - auto* majorEveryLayout = new QHBoxLayout(majorEveryContainer); - majorEveryLayout->setContentsMargins(0, 0, 0, 0); - majorEveryLayout->setSpacing(8); - majorEveryLayout->addWidget(mMajorEveryX); - majorEveryLayout->addSpacing(12); - majorEveryLayout->addWidget(mMajorEveryY); - form->addRow(tr("Major line every"), majorEveryContainer); + addLabel(formRow, tr("Major line every"), mMajorEveryX); + form->addWidget(mMajorEveryX, formRow, 1); + form->addWidget(mMajorEveryY, formRow, 2); + ++formRow; mMajorColorButton = new ColorAnimatorButton(mMajorColorAnimator.get(), this); - auto* majorColorContainer = new QWidget(this); - auto* majorColorLayout = new QHBoxLayout(majorColorContainer); - majorColorLayout->setContentsMargins(0, 0, 0, 0); - majorColorLayout->addStretch(); - majorColorLayout->addWidget(mMajorColorButton); - form->addRow(tr("Major line color"), majorColorContainer); + 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); - auto* minorColorContainer = new QWidget(this); - auto* minorColorLayout = new QHBoxLayout(minorColorContainer); - minorColorLayout->setContentsMargins(0, 0, 0, 0); - minorColorLayout->addStretch(); - minorColorLayout->addWidget(mColorButton); - form->addRow(tr("Minor line color"), minorColorContainer); + 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->addRow(mApplyButton); + form->addWidget(mApplyButton, formRow, 0, 1, 3); layout->addLayout(form); eSizesUI::widget.addSpacing(layout); From bf1cf1df30680dcf5ed56c2b77e817154049f80d Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Wed, 22 Oct 2025 16:26:27 +0200 Subject: [PATCH 23/37] Grid: fix originX default as Scene default is 1920px --- src/core/gridcontroller.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 0b1483fe9..17cec3f9a 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -55,7 +55,7 @@ struct CORE_EXPORT GridSettings { double sizeX = 40.0; double sizeY = 40.0; - double originX = 640.0; + double originX = 960.0; double originY = 540.0; int snapThresholdPx = 40; bool enabled = false; From f7fe63699578305e5e2ea7ac04c090a9435dfecc Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 23 Oct 2025 09:39:17 +0200 Subject: [PATCH 24/37] Grid: make dialog to stay on top while allow to use Friction normally --- src/app/GUI/mainwindow.cpp | 27 +++++++++++++++------------ src/ui/dialogs/gridsettingsdialog.cpp | 3 ++- src/ui/dialogs/gridsettingsdialog.h | 4 ++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index 4ea3c8b9b..4f5d34f13 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -221,10 +221,11 @@ BoundingBox *MainWindow::getCurrentBox() void MainWindow::openGridSettingsDialog() { - GridSettingsDialog dialog(this); - dialog.setWindowTitle(tr("Grid Settings")); - dialog.setSettings(mDocument.gridController().settings); - connect(&dialog, &GridSettingsDialog::applyRequested, + 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); @@ -232,14 +233,16 @@ void MainWindow::openGridSettingsDialog() mDocument.saveGridSettingsAsDefault(mDocument.gridController().settings); } }); - if (dialog.exec() == QDialog::Accepted) { - auto settings = dialog.settings(); - settings.enabled = mDocument.gridController().settings.enabled; - mDocument.setGridSettings(settings); - if (dialog.saveAsDefault()) { - 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) diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index f6ebbba03..7c4b7bb22 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -50,7 +50,7 @@ constexpr int kMaxMajorEvery = 100; } GridSettingsDialog::GridSettingsDialog(QWidget* parent) - : QDialog(parent) + : Friction::Ui::Dialog(parent) , mSizeX(nullptr) , mSizeY(nullptr) , mOriginX(nullptr) @@ -69,6 +69,7 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mSnapEnabled(true) , mStoredSnapToCanvas(false) { + setModal(false); const QColor defaultMinor = GridSettings::defaults().colorAnimator->getColor(); const QColor defaultMajor = GridSettings::defaults().majorColorAnimator->getColor(); if (auto* settings = eSettings::sInstance) { diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index e40d40244..77582fda0 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -24,7 +24,7 @@ #ifndef GRIDSETTINGSDIALOG_H #define GRIDSETTINGSDIALOG_H -#include +#include "dialog.h" #include "Animators/coloranimator.h" #include "smartPointers/ememory.h" @@ -41,7 +41,7 @@ struct GridSettings; } } -class GridSettingsDialog : public QDialog +class GridSettingsDialog : public Friction::Ui::Dialog { Q_OBJECT From 9e7c856e6d216785e308075154d6517abda12f3a Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Thu, 23 Oct 2025 18:29:23 +0200 Subject: [PATCH 25/37] Grid: fix for Windows not correctly compiling the new feature --- src/ui/dialogs/gridsettingsdialog.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 77582fda0..4320abb95 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -24,6 +24,7 @@ #ifndef GRIDSETTINGSDIALOG_H #define GRIDSETTINGSDIALOG_H +#include "ui_global.h" #include "dialog.h" #include "Animators/coloranimator.h" #include "smartPointers/ememory.h" @@ -41,7 +42,7 @@ struct GridSettings; } } -class GridSettingsDialog : public Friction::Ui::Dialog +class UI_EXPORT GridSettingsDialog : public Friction::Ui::Dialog { Q_OBJECT From b50cae2a5cc992faba71555f8de9476d3ab8dfd3 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 24 Oct 2025 13:12:42 +0200 Subject: [PATCH 26/37] add the new Grid icon --- src/app/GUI/menu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index 3487e4fd5..55eaa999d 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -567,7 +567,7 @@ void MainWindow::setupMenuBar() cmdAddAction(mResetZoomAction); // TODO: custom icon for Grid menu - mGridMenu = mViewMenu->addMenu(QIcon::fromTheme("rectCreate"), + mGridMenu = mViewMenu->addMenu(QIcon::fromTheme("grid"), tr("Grid && Snapping", "MenuBar_View")); mSnapToCanvasAct = mGridMenu->addAction(tr("Snap to Canvas")); From 12d0cc1b4227ab86fc40f0244e99ee5db63024b2 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 24 Oct 2025 14:21:49 +0200 Subject: [PATCH 27/37] Grid: add snapping support for bounding box corners --- src/core/canvas.h | 2 + src/core/canvasmouseinteractions.cpp | 34 ++++++++- src/core/gridcontroller.cpp | 104 ++++++++++++++++++--------- src/core/gridcontroller.h | 4 +- 4 files changed, 107 insertions(+), 37 deletions(-) diff --git a/src/core/canvas.h b/src/core/canvas.h index ab4035ea2..ed8bfe464 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -47,6 +47,7 @@ #include #include #include +#include #include "gizmos.h" @@ -837,6 +838,7 @@ class CORE_EXPORT Canvas : public CanvasBase bool mHasWorldToScreen = false; qreal mDevicePixelRatio = 1.0; QPointF mGridMoveStartPivot; + std::vector mGridSnapAnchorOffsets; bool mHasCreationPressPos = false; QPointF mCreationPressPos; diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 08e6b38da..71065ac3c 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" @@ -986,6 +987,36 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { if (mStartTransform && !mSelectedBoxes.isEmpty()) { mGridMoveStartPivot = getSelectedBoxesAbsPivotPos(); + + mGridSnapAnchorOffsets.clear(); + 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) { + const QPointF topLeft = combinedRect.topLeft(); + const QPointF topRight = combinedRect.topRight(); + const QPointF bottomLeft = combinedRect.bottomLeft(); + const QPointF bottomRight = combinedRect.bottomRight(); + + mGridSnapAnchorOffsets.emplace_back(topLeft - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(topRight - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(bottomLeft - mGridMoveStartPivot); + mGridSnapAnchorOffsets.emplace_back(bottomRight - mGridMoveStartPivot); + } } auto moveBy = getMoveByValueForEvent(e); @@ -1006,7 +1037,8 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { mWorldToScreen, forceSnap, bypassSnap, - canvasPtr); + canvasPtr, + &mGridSnapAnchorOffsets); if (snapped != targetPivot) { moveBy = snapped - mGridMoveStartPivot; } diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index 04eb79769..51df58bba 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -225,7 +225,8 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, const QTransform& worldToScreen, const bool forceSnap, const bool bypassSnap, - const QRectF* canvasRectWorld) const + const QRectF* canvasRectWorld, + const std::vector* anchorOffsets) const { const GridSettings sanitizedSettings = sanitizeSettings(settings); @@ -250,15 +251,55 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, return pivotWorld; } - std::vector candidates; - candidates.reserve(10); + std::vector fallbackOffsets; + if (!anchorOffsets || anchorOffsets->empty()) { + fallbackOffsets.emplace_back(QPointF(0.0, 0.0)); + } + const std::vector& offsets = (anchorOffsets && !anchorOffsets->empty()) + ? *anchorOffsets + : fallbackOffsets; + + 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)) { - const double gx = sanitizedSettings.originX + - std::round((pivotWorld.x() - sanitizedSettings.originX) / sizeX) * sizeX; - const double gy = sanitizedSettings.originY + - std::round((pivotWorld.y() - sanitizedSettings.originY) / sizeY) * sizeY; - candidates.emplace_back(gx, gy); + 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) { @@ -269,42 +310,35 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, const double midX = (left + right) * 0.5; const double midY = (top + bottom) * 0.5; - candidates.emplace_back(left, top); - candidates.emplace_back(right, top); - candidates.emplace_back(left, bottom); - candidates.emplace_back(right, bottom); - - candidates.emplace_back(midX, top); - candidates.emplace_back(midX, bottom); - candidates.emplace_back(left, midY); - candidates.emplace_back(right, midY); - - candidates.emplace_back(midX, midY); + 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 (candidates.empty()) { + if (!foundCandidate) { return pivotWorld; } - const QPointF screenPivot = worldToScreen.map(pivotWorld); - QPointF bestCandidate = pivotWorld; - double bestDistance = std::numeric_limits::infinity(); - - for (const auto& candidate : candidates) { - const QPointF screenCandidate = worldToScreen.map(candidate); - const double candidateDistance = QLineF(screenPivot, screenCandidate).length(); - if (candidateDistance < bestDistance) { - bestDistance = candidateDistance; - bestCandidate = candidate; - } - } - if (forceSnap) { - return bestCandidate; + return bestPivot; } if (bestDistance <= sanitizedSettings.snapThresholdPx) { - return bestCandidate; + return bestPivot; } return pivotWorld; } diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 17cec3f9a..d4deb5e4e 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -35,6 +35,7 @@ #include #include +#include class QPainter; class SkCanvas; @@ -94,7 +95,8 @@ class CORE_EXPORT GridController { const QTransform& worldToScreen, bool forceSnap, bool bypassSnap, - const QRectF* canvasRectWorld = nullptr) const; + const QRectF* canvasRectWorld = nullptr, + const std::vector* anchorOffsets = nullptr) const; private: enum class Orientation { From cac20fb6564d930027b1101b90be53d5108708e0 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Fri, 24 Oct 2025 15:23:59 +0200 Subject: [PATCH 28/37] Grid: add "snap radius" to rectangle, circle and path creation --- src/core/canvashandlesmartpath.cpp | 9 +++------ src/core/canvasmouseevents.cpp | 10 ++++------ src/core/canvasmouseinteractions.cpp | 6 ++---- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/core/canvashandlesmartpath.cpp b/src/core/canvashandlesmartpath.cpp index 043702667..cee745bb1 100644 --- a/src/core/canvashandlesmartpath.cpp +++ b/src/core/canvashandlesmartpath.cpp @@ -60,8 +60,7 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { mCurrentContainer->addContained(newPath); clearBoxesSelection(); addBoxToSelection(newPath.get()); - const bool forceSnap = mDocument.gridController().settings.enabled; - const QPointF snappedPos = snapEventPos(e, forceSnap); + const QPointF snappedPos = snapEventPos(e, false); const auto relPos = newPath->mapAbsPosToRel(snappedPos); newPath->getBoxTransformAnimator()->setPosition(relPos.x(), relPos.y()); const auto newHandler = newPath->getPathAnimator(); @@ -69,8 +68,7 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { setCurrentSmartEndPoint(node); } else { if(!nodePointUnderMouse) { - const bool forceSnap = mDocument.gridController().settings.enabled; - const QPointF snappedPos = snapEventPos(e, forceSnap); + const QPointF snappedPos = snapEventPos(e, false); const auto newPoint = mLastEndPoint->actionAddPointAbsPos(snappedPos); //newPoint->startTransform(); setCurrentSmartEndPoint(newPoint); @@ -95,8 +93,7 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { void Canvas::handleAddSmartPointMouseMove(const eMouseEvent &e) { if(!mLastEndPoint) return; if(mStartTransform) mLastEndPoint->startTransform(); - const bool forceSnap = mDocument.gridController().settings.enabled; - const QPointF snappedPos = snapEventPos(e, forceSnap); + const QPointF snappedPos = snapEventPos(e, false); if(mLastEndPoint->hasNextNormalPoint() && mLastEndPoint->hasPrevNormalPoint()) { mLastEndPoint->setCtrlsMode(CtrlsMode::corner); diff --git a/src/core/canvasmouseevents.cpp b/src/core/canvasmouseevents.cpp index ef8ee8812..b76a411ce 100644 --- a/src/core/canvasmouseevents.cpp +++ b/src/core/canvasmouseevents.cpp @@ -145,11 +145,10 @@ void Canvas::mouseMoveEvent(const eMouseEvent &e) } else if(mCurrentMode == CanvasMode::pathCreate) { handleAddSmartPointMouseMove(e); } else if(mCurrentMode == CanvasMode::circleCreate) { - const bool forceSnap = mDocument.gridController().settings.enabled; const QPointF anchor = mHasCreationPressPos ? mCreationPressPos - : snapPosToGrid(e.fLastPressPos, e.fModifiers, forceSnap); - const QPointF current = snapEventPos(e, forceSnap); + : snapPosToGrid(e.fLastPressPos, e.fModifiers, false); + const QPointF current = snapEventPos(e, false); const QPointF delta = current - anchor; if(e.shiftMod()) { const qreal lenR = pointToLen(delta); @@ -158,11 +157,10 @@ void Canvas::mouseMoveEvent(const eMouseEvent &e) mCurrentCircle->moveRadiusesByAbs(delta); } } else if(mCurrentMode == CanvasMode::rectCreate) { - const bool forceSnap = mDocument.gridController().settings.enabled; const QPointF anchor = mHasCreationPressPos ? mCreationPressPos - : snapPosToGrid(e.fLastPressPos, e.fModifiers, forceSnap); - const QPointF current = snapEventPos(e, forceSnap); + : snapPosToGrid(e.fLastPressPos, e.fModifiers, false); + const QPointF current = snapEventPos(e, false); const QPointF trans = current - anchor; if(e.shiftMod()) { const qreal valF = qMax(trans.x(), trans.y()); diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 71065ac3c..2d2c39ed7 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -316,8 +316,7 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - const bool gridSnapEnabled = mDocument.gridController().settings.enabled; - const QPointF snappedPos = snapEventPos(e, gridSnapEnabled); + const QPointF snappedPos = snapEventPos(e, false); newPath->setAbsolutePos(snappedPos); clearBoxesSelection(); addBoxToSelection(newPath.get()); @@ -337,8 +336,7 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - const bool gridSnapEnabled = mDocument.gridController().settings.enabled; - const QPointF snappedPos = snapEventPos(e, gridSnapEnabled); + const QPointF snappedPos = snapEventPos(e, false); newPath->setAbsolutePos(snappedPos); clearBoxesSelection(); addBoxToSelection(newPath.get()); From a14e21e8f58ff756579e691f184b30136c33e4c6 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sat, 25 Oct 2025 10:51:34 +0200 Subject: [PATCH 29/37] Grid: added "snap to boxes" and "snap to nodes" --- src/app/GUI/mainwindow.cpp | 15 +++ src/app/GUI/mainwindow.h | 2 + src/app/GUI/menu.cpp | 22 ++++ src/core/Private/document.cpp | 10 ++ src/core/Private/esettings.cpp | 8 ++ src/core/Private/esettings.h | 2 + src/core/canvas.h | 5 + src/core/canvasmouseinteractions.cpp | 157 ++++++++++++++++++++++++-- src/core/gridcontroller.cpp | 32 +++++- src/core/gridcontroller.h | 6 +- src/ui/dialogs/gridsettingsdialog.cpp | 6 + src/ui/dialogs/gridsettingsdialog.h | 2 + 12 files changed, 252 insertions(+), 15 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index 4f5d34f13..4f36c3346 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -121,6 +121,9 @@ MainWindow::MainWindow(Document& document, , mAddKeyAct(nullptr) , mShowGridAct(nullptr) , mSnapToGridAct(nullptr) + , mSnapToCanvasAct(nullptr) + , mSnapToBoxesAct(nullptr) + , mSnapToNodesAct(nullptr) , mGridSettingsAct(nullptr) , mGridDrawOnTopAct(nullptr) , mAddToQueAct(nullptr) @@ -260,6 +263,18 @@ void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& setti 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 (mGridDrawOnTopAct) { QSignalBlocker blocker(mGridDrawOnTopAct); if (mGridDrawOnTopAct->isChecked() != settings.drawOnTop) { diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 90933b14c..20ac959c2 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -262,6 +262,8 @@ class MainWindow : public QMainWindow QAction *mShowGridAct; QAction *mSnapToGridAct; QAction *mSnapToCanvasAct; + QAction *mSnapToBoxesAct; + QAction *mSnapToNodesAct; QAction *mGridSettingsAct; QAction *mGridDrawOnTopAct; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index 55eaa999d..e0d75f1f7 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -589,6 +589,28 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapToGridAct); + 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(); mShowGridAct = mGridMenu->addAction(tr("Show Grid")); diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 7bb04a2f1..9e980084f 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -143,6 +143,8 @@ void Document::loadGridSettingsFromSettings() if (auto* settingsMgr = eSettings::sInstance) { defaults.drawOnTop = settingsMgr->fGridDrawOnTop; defaults.snapToCanvas = settingsMgr->fGridSnapToCanvas; + defaults.snapToBoxes = settingsMgr->fGridSnapToBoxes; + defaults.snapToNodes = settingsMgr->fGridSnapToNodes; } GridSettings loaded = defaults; loaded.sizeX = AppSupport::getSettings("grid", "sizeX", defaults.sizeX).toDouble(); @@ -154,6 +156,8 @@ void Document::loadGridSettingsFromSettings() 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(); auto readMajorEvery = [](const QString& key, int fallback, bool& found) @@ -223,6 +227,8 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const 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", "majorEveryX", settings.majorEveryX); AppSupport::setSettings("grid", "majorEveryY", settings.majorEveryY); AppSupport::setSettings("grid", "majorEvery", settings.majorEveryX); @@ -240,10 +246,14 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) 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->saveKeyToFile("gridColor"); settingsMgr->saveKeyToFile("gridMajorColor"); settingsMgr->saveKeyToFile("gridDrawOnTop"); settingsMgr->saveKeyToFile("gridSnapToCanvas"); + settingsMgr->saveKeyToFile("gridSnapToBoxes"); + settingsMgr->saveKeyToFile("gridSnapToNodes"); } saveGridSettingsToSettings(sanitized); } diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index 3993c6ef7..0a26112c0 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -287,6 +287,14 @@ eSettings::eSettings(const int cpuThreads, fGridSnapToCanvas, "gridSnapToCanvas", false); + gSettings << std::make_shared( + fGridSnapToBoxes, + "gridSnapToBoxes", + false); + gSettings << std::make_shared( + fGridSnapToNodes, + "gridSnapToNodes", + false); gSettings << std::make_shared( fObjectKeyframeColor, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index f055a5dad..647c1695d 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -158,6 +158,8 @@ class CORE_EXPORT eSettings : public QObject QColor fGridMajorColor = Friction::Core::GridSettings::defaults().majorColorAnimator->getColor(); bool fGridDrawOnTop = true; bool fGridSnapToCanvas = false; + bool fGridSnapToBoxes = false; + bool fGridSnapToNodes = false; QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; diff --git a/src/core/canvas.h b/src/core/canvas.h index ed8bfe464..742f1ed85 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -950,6 +950,11 @@ class CORE_EXPORT Canvas : public CanvasBase QPointF getMoveByValueForEvent(const eMouseEvent &e); void cancelCurrentTransform(); void cancelCurrentTransformGimzos(); + + void collectSnapTargets(bool includeBoxes, + bool includeNodes, + std::vector& boxTargets, + std::vector& nodeTargets) const; }; #endif // CANVAS_H diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 2d2c39ed7..2266da2d9 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -52,6 +52,9 @@ #include "MovablePoints/smartnodepoint.h" #include "MovablePoints/pathpivot.h" +#include +#include + #include #include #include @@ -61,6 +64,72 @@ using namespace Friction::Core; +namespace { +bool pointIsFinite(const QPointF& point) +{ + return std::isfinite(point.x()) && std::isfinite(point.y()); +} +} + +void Canvas::collectSnapTargets(bool includeBoxes, + bool includeNodes, + std::vector& boxTargets, + std::vector& nodeTargets) const +{ + boxTargets.clear(); + nodeTargets.clear(); + + if ((!includeBoxes && !includeNodes) || !mCurrentContainer) { + return; + } + + auto addIfValid = [](std::vector& target, const QPointF& pt) { + if (pointIsFinite(pt)) { + target.push_back(pt); + } + }; + + 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 (includeBoxes) { + addIfValid(boxTargets, box->getPivotAbsPos()); + const QRectF rect = box->getAbsBoundingRect().normalized(); + if (!rect.isNull() && rect.isValid()) { + addIfValid(boxTargets, rect.topLeft()); + addIfValid(boxTargets, rect.topRight()); + addIfValid(boxTargets, rect.bottomLeft()); + addIfValid(boxTargets, rect.bottomRight()); + addIfValid(boxTargets, rect.center()); + } + } + 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); +} + QPointF Canvas::snapPosToGrid(const QPointF& pos, Qt::KeyboardModifiers modifiers, bool forceSnap) const @@ -75,7 +144,19 @@ QPointF Canvas::snapPosToGrid(const QPointF& pos, const bool gridEnabled = settings.enabled; const bool canvasSnapEnabled = settings.snapToCanvas; - const bool hasSnapSource = gridEnabled || canvasSnapEnabled; + const bool boxesSnapEnabled = settings.snapToBoxes; + const bool nodesSnapEnabled = settings.snapToNodes; + + std::vector boxTargets; + std::vector nodeTargets; + if (boxesSnapEnabled || nodesSnapEnabled) { + collectSnapTargets(boxesSnapEnabled, nodesSnapEnabled, boxTargets, nodeTargets); + } + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + + const bool hasSnapSource = gridEnabled || canvasSnapEnabled || + hasBoxTargets || hasNodeTargets; const bool shouldForce = (forceSnap && hasSnapSource) || (modifiers & Qt::ControlModifier); @@ -92,7 +173,10 @@ QPointF Canvas::snapPosToGrid(const QPointF& pos, mWorldToScreen, shouldForce, false, - canvasRectPtr); + canvasRectPtr, + nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); } QPointF Canvas::snapEventPos(const eMouseEvent& e, @@ -714,6 +798,20 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { if(mStartTransform) mCurrentNormalSegment.startPassThroughTransform(); mCurrentNormalSegment.makePassThroughAbs(e.fPos, mCurrentNormalSegmentT); } else { + const auto& gridSettings = mDocument.gridController().settings; + const bool boxesSnapEnabled = gridSettings.snapToBoxes; + const bool nodesSnapEnabled = gridSettings.snapToNodes; + std::vector boxTargets; + std::vector nodeTargets; + if (boxesSnapEnabled || nodesSnapEnabled) { + collectSnapTargets(boxesSnapEnabled, nodesSnapEnabled, boxTargets, nodeTargets); + } + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + const bool snapSourcesAvailable = gridSettings.enabled || + gridSettings.snapToCanvas || + hasBoxTargets || hasNodeTargets; + if(mPressedPoint) { addPointToSelection(mPressedPoint); const auto mods = QGuiApplication::queryKeyboardModifiers(); @@ -783,8 +881,6 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; - const auto& gridSettings = mDocument.gridController().settings; - const bool snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas; if(mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { const QPointF targetPos = mGridMoveStartPivot + moveBy; @@ -795,7 +891,14 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { canvasPtr = &canvasRect; } const auto snapped = mDocument.gridController().maybeSnapPivot( - targetPos, mWorldToScreen, forceSnap, bypassSnap, canvasPtr); + targetPos, + mWorldToScreen, + forceSnap, + bypassSnap, + canvasPtr, + nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); if(snapped != targetPos) { moveBy = snapped - mGridMoveStartPivot; } @@ -814,8 +917,6 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; - const auto& gridSettings = mDocument.gridController().settings; - const bool snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas; if(!mSelectedPoints_d.isEmpty() && mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { const QPointF targetPivot = mGridMoveStartPivot + moveBy; @@ -826,7 +927,14 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { canvasPtr = &canvasRect; } const auto snapped = mDocument.gridController().maybeSnapPivot( - targetPivot, mWorldToScreen, forceSnap, bypassSnap, canvasPtr); + targetPivot, + mWorldToScreen, + forceSnap, + bypassSnap, + canvasPtr, + nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); if(snapped != targetPivot) { moveBy = snapped - mGridMoveStartPivot; } @@ -983,6 +1091,8 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { mPressedBox = nullptr; } + const auto& gridSettings = mDocument.gridController().settings; + if (mStartTransform && !mSelectedBoxes.isEmpty()) { mGridMoveStartPivot = getSelectedBoxesAbsPivotPos(); @@ -1015,13 +1125,36 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { mGridSnapAnchorOffsets.emplace_back(bottomLeft - mGridMoveStartPivot); mGridSnapAnchorOffsets.emplace_back(bottomRight - mGridMoveStartPivot); } + + if (gridSettings.snapToNodes) { + 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 auto& gridSettings = mDocument.gridController().settings; - const bool snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas; + const bool boxesSnapEnabled = gridSettings.snapToBoxes; + const bool nodesSnapEnabled = gridSettings.snapToNodes; + std::vector boxTargets; + std::vector nodeTargets; + if (boxesSnapEnabled || nodesSnapEnabled) { + collectSnapTargets(boxesSnapEnabled, nodesSnapEnabled, boxTargets, nodeTargets); + } + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + const bool snapSourcesAvailable = gridSettings.enabled || + gridSettings.snapToCanvas || + hasBoxTargets || hasNodeTargets; if (!mSelectedBoxes.isEmpty() && mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { const QPointF targetPivot = mGridMoveStartPivot + moveBy; @@ -1036,7 +1169,9 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { forceSnap, bypassSnap, canvasPtr, - &mGridSnapAnchorOffsets); + &mGridSnapAnchorOffsets, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); if (snapped != targetPivot) { moveBy = snapped - mGridMoveStartPivot; } diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index 51df58bba..edd9e65b3 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -138,6 +138,8 @@ bool GridSettings::operator==(const GridSettings& other) const show == other.show && drawOnTop == other.drawOnTop && snapToCanvas == other.snapToCanvas && + snapToBoxes == other.snapToBoxes && + snapToNodes == other.snapToNodes && majorEveryX == other.majorEveryX && majorEveryY == other.majorEveryY && thisColor == otherColor && @@ -226,12 +228,20 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, const bool forceSnap, const bool bypassSnap, const QRectF* canvasRectWorld, - const std::vector* anchorOffsets) const + const std::vector* anchorOffsets, + const std::vector* boxTargets, + const std::vector* nodeTargets) const { const GridSettings sanitizedSettings = sanitizeSettings(settings); + const bool hasBoxTargets = sanitizedSettings.snapToBoxes && + boxTargets && !boxTargets->empty(); + const bool hasNodeTargets = sanitizedSettings.snapToNodes && + nodeTargets && !nodeTargets->empty(); + const bool snapSourcesEnabled = sanitizedSettings.enabled || - (sanitizedSettings.snapToCanvas && canvasRectWorld); + (sanitizedSettings.snapToCanvas && canvasRectWorld) || + hasBoxTargets || hasNodeTargets; if ((!snapSourcesEnabled && !forceSnap) || bypassSnap) { return pivotWorld; } @@ -247,7 +257,7 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, } const bool hasCanvasTargets = canUseCanvas && !normalizedCanvas.isEmpty(); - if (!hasGrid && !hasCanvasTargets) { + if (!hasGrid && !hasCanvasTargets && !hasBoxTargets && !hasNodeTargets) { return pivotWorld; } @@ -329,6 +339,22 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, } } + 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; } diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index d4deb5e4e..2c3d60ed9 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -63,6 +63,8 @@ struct CORE_EXPORT GridSettings { bool show = false; bool drawOnTop = false; bool snapToCanvas = false; + bool snapToBoxes = false; + bool snapToNodes = false; int majorEveryX = 8; int majorEveryY = 8; qsptr colorAnimator; @@ -96,7 +98,9 @@ class CORE_EXPORT GridController { bool forceSnap, bool bypassSnap, const QRectF* canvasRectWorld = nullptr, - const std::vector* anchorOffsets = nullptr) const; + const std::vector* anchorOffsets = nullptr, + const std::vector* boxTargets = nullptr, + const std::vector* nodeTargets = nullptr) const; private: enum class Orientation { diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 7c4b7bb22..12b2779c9 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -68,6 +68,8 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mMajorColorAnimator(enve::make_shared()) , mSnapEnabled(true) , mStoredSnapToCanvas(false) + , mStoredSnapToBoxes(false) + , mStoredSnapToNodes(false) { setModal(false); const QColor defaultMinor = GridSettings::defaults().colorAnimator->getColor(); @@ -206,6 +208,8 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mOriginY->setValue(settings.originY); mSnapThreshold->setValue(settings.snapThresholdPx); mStoredSnapToCanvas = settings.snapToCanvas; + mStoredSnapToBoxes = settings.snapToBoxes; + mStoredSnapToNodes = settings.snapToNodes; mMajorEveryX->setValue(settings.majorEveryX); mMajorEveryY->setValue(settings.majorEveryY); mStoredShow = settings.show; @@ -249,6 +253,8 @@ GridSettings GridSettingsDialog::settings() const result.originY = mOriginY->value(); result.snapThresholdPx = mSnapThreshold->value(); result.snapToCanvas = mStoredSnapToCanvas; + result.snapToBoxes = mStoredSnapToBoxes; + result.snapToNodes = mStoredSnapToNodes; result.majorEveryX = mMajorEveryX->value(); result.majorEveryY = mMajorEveryY->value(); result.show = mStoredShow; diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 4320abb95..7b9d650a6 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -78,6 +78,8 @@ class UI_EXPORT GridSettingsDialog : public Friction::Ui::Dialog bool mStoredShow = true; bool mStoredDrawOnTop = true; bool mStoredSnapToCanvas = false; + bool mStoredSnapToBoxes = false; + bool mStoredSnapToNodes = false; }; #endif // GRIDSETTINGSDIALOG_H From 47c8141b6c6699c052d162ca2b8ba11a02cd0efd Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sat, 25 Oct 2025 11:14:42 +0200 Subject: [PATCH 30/37] Grid: "pivot" and "bounding box" points are now optional --- src/app/GUI/mainwindow.cpp | 14 ++++++++++++++ src/app/GUI/mainwindow.h | 2 ++ src/app/GUI/menu.cpp | 22 ++++++++++++++++++++++ src/core/Private/document.cpp | 10 ++++++++++ src/core/Private/esettings.cpp | 8 ++++++++ src/core/Private/esettings.h | 2 ++ src/core/canvasmouseinteractions.cpp | 16 ++++++++++++++-- src/core/gridcontroller.cpp | 13 +++++++++---- src/core/gridcontroller.h | 2 ++ src/ui/dialogs/gridsettingsdialog.cpp | 6 ++++++ src/ui/dialogs/gridsettingsdialog.h | 2 ++ 11 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index 4f36c3346..14c8c61c0 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -124,6 +124,8 @@ MainWindow::MainWindow(Document& document, , mSnapToCanvasAct(nullptr) , mSnapToBoxesAct(nullptr) , mSnapToNodesAct(nullptr) + , mSnapAnchorPivotAct(nullptr) + , mSnapAnchorBoundsAct(nullptr) , mGridSettingsAct(nullptr) , mGridDrawOnTopAct(nullptr) , mAddToQueAct(nullptr) @@ -275,6 +277,18 @@ void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& setti mSnapToNodesAct->setChecked(settings.snapToNodes); } } + 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 (mGridDrawOnTopAct) { QSignalBlocker blocker(mGridDrawOnTopAct); if (mGridDrawOnTopAct->isChecked() != settings.drawOnTop) { diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 20ac959c2..9975297ad 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -264,6 +264,8 @@ class MainWindow : public QMainWindow QAction *mSnapToCanvasAct; QAction *mSnapToBoxesAct; QAction *mSnapToNodesAct; + QAction *mSnapAnchorPivotAct; + QAction *mSnapAnchorBoundsAct; QAction *mGridSettingsAct; QAction *mGridDrawOnTopAct; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index e0d75f1f7..e14b4e777 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -611,6 +611,28 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapToNodesAct); + mSnapAnchorPivotAct = mGridMenu->addAction(tr("Pivot")); + 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("Bounding box")); + 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); + mGridMenu->addSeparator(); mShowGridAct = mGridMenu->addAction(tr("Show Grid")); diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 9e980084f..36bd9588f 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -145,6 +145,8 @@ void Document::loadGridSettingsFromSettings() defaults.snapToCanvas = settingsMgr->fGridSnapToCanvas; defaults.snapToBoxes = settingsMgr->fGridSnapToBoxes; defaults.snapToNodes = settingsMgr->fGridSnapToNodes; + defaults.snapAnchorPivot = settingsMgr->fGridSnapAnchorPivot; + defaults.snapAnchorBounds = settingsMgr->fGridSnapAnchorBounds; } GridSettings loaded = defaults; loaded.sizeX = AppSupport::getSettings("grid", "sizeX", defaults.sizeX).toDouble(); @@ -158,6 +160,8 @@ void Document::loadGridSettingsFromSettings() 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.snapAnchorPivot = AppSupport::getSettings("grid", "snapAnchorPivot", defaults.snapAnchorPivot).toBool(); + loaded.snapAnchorBounds = AppSupport::getSettings("grid", "snapAnchorBounds", defaults.snapAnchorBounds).toBool(); auto readMajorEvery = [](const QString& key, int fallback, bool& found) @@ -229,6 +233,8 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const AppSupport::setSettings("grid", "snapToCanvas", settings.snapToCanvas); AppSupport::setSettings("grid", "snapToBoxes", settings.snapToBoxes); AppSupport::setSettings("grid", "snapToNodes", settings.snapToNodes); + AppSupport::setSettings("grid", "snapAnchorPivot", settings.snapAnchorPivot); + AppSupport::setSettings("grid", "snapAnchorBounds", settings.snapAnchorBounds); AppSupport::setSettings("grid", "majorEveryX", settings.majorEveryX); AppSupport::setSettings("grid", "majorEveryY", settings.majorEveryY); AppSupport::setSettings("grid", "majorEvery", settings.majorEveryX); @@ -248,12 +254,16 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) settingsMgr->fGridSnapToCanvas = sanitized.snapToCanvas; settingsMgr->fGridSnapToBoxes = sanitized.snapToBoxes; settingsMgr->fGridSnapToNodes = sanitized.snapToNodes; + settingsMgr->fGridSnapAnchorPivot = sanitized.snapAnchorPivot; + settingsMgr->fGridSnapAnchorBounds = sanitized.snapAnchorBounds; settingsMgr->saveKeyToFile("gridColor"); settingsMgr->saveKeyToFile("gridMajorColor"); settingsMgr->saveKeyToFile("gridDrawOnTop"); settingsMgr->saveKeyToFile("gridSnapToCanvas"); settingsMgr->saveKeyToFile("gridSnapToBoxes"); settingsMgr->saveKeyToFile("gridSnapToNodes"); + settingsMgr->saveKeyToFile("gridSnapAnchorPivot"); + settingsMgr->saveKeyToFile("gridSnapAnchorBounds"); } saveGridSettingsToSettings(sanitized); } diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index 0a26112c0..72fc59a23 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -295,6 +295,14 @@ eSettings::eSettings(const int cpuThreads, fGridSnapToNodes, "gridSnapToNodes", false); + gSettings << std::make_shared( + fGridSnapAnchorPivot, + "gridSnapAnchorPivot", + true); + gSettings << std::make_shared( + fGridSnapAnchorBounds, + "gridSnapAnchorBounds", + true); gSettings << std::make_shared( fObjectKeyframeColor, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 647c1695d..70d3933cb 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -160,6 +160,8 @@ class CORE_EXPORT eSettings : public QObject bool fGridSnapToCanvas = false; bool fGridSnapToBoxes = false; bool fGridSnapToNodes = false; + bool fGridSnapAnchorPivot = true; + bool fGridSnapAnchorBounds = true; QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 2266da2d9..8021435ac 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -1097,7 +1097,9 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { mGridMoveStartPivot = getSelectedBoxesAbsPivotPos(); mGridSnapAnchorOffsets.clear(); - mGridSnapAnchorOffsets.emplace_back(QPointF(0.0, 0.0)); + if (gridSettings.snapAnchorPivot) { + mGridSnapAnchorOffsets.emplace_back(QPointF(0.0, 0.0)); + } QRectF combinedRect; bool hasRect = false; @@ -1114,16 +1116,26 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { } } - if (hasRect) { + 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.snapToNodes) { diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index edd9e65b3..516880f0c 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -140,6 +140,8 @@ bool GridSettings::operator==(const GridSettings& other) const snapToCanvas == other.snapToCanvas && snapToBoxes == other.snapToBoxes && snapToNodes == other.snapToNodes && + snapAnchorPivot == other.snapAnchorPivot && + snapAnchorBounds == other.snapAnchorBounds && majorEveryX == other.majorEveryX && majorEveryY == other.majorEveryY && thisColor == otherColor && @@ -261,13 +263,16 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, return pivotWorld; } + const std::vector* offsetsPtr = anchorOffsets; std::vector fallbackOffsets; - if (!anchorOffsets || anchorOffsets->empty()) { + if (!offsetsPtr) { fallbackOffsets.emplace_back(QPointF(0.0, 0.0)); + offsetsPtr = &fallbackOffsets; + } + const auto& offsets = *offsetsPtr; + if (offsets.empty()) { + return pivotWorld; } - const std::vector& offsets = (anchorOffsets && !anchorOffsets->empty()) - ? *anchorOffsets - : fallbackOffsets; struct AnchorContext { QPointF offset; diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 2c3d60ed9..871110fa9 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -65,6 +65,8 @@ struct CORE_EXPORT GridSettings { bool snapToCanvas = false; bool snapToBoxes = false; bool snapToNodes = false; + bool snapAnchorPivot = true; + bool snapAnchorBounds = true; int majorEveryX = 8; int majorEveryY = 8; qsptr colorAnimator; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 12b2779c9..7b903058f 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -70,6 +70,8 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mStoredSnapToCanvas(false) , mStoredSnapToBoxes(false) , mStoredSnapToNodes(false) + , mStoredSnapAnchorPivot(true) + , mStoredSnapAnchorBounds(true) { setModal(false); const QColor defaultMinor = GridSettings::defaults().colorAnimator->getColor(); @@ -210,6 +212,8 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mStoredSnapToCanvas = settings.snapToCanvas; mStoredSnapToBoxes = settings.snapToBoxes; mStoredSnapToNodes = settings.snapToNodes; + mStoredSnapAnchorPivot = settings.snapAnchorPivot; + mStoredSnapAnchorBounds = settings.snapAnchorBounds; mMajorEveryX->setValue(settings.majorEveryX); mMajorEveryY->setValue(settings.majorEveryY); mStoredShow = settings.show; @@ -255,6 +259,8 @@ GridSettings GridSettingsDialog::settings() const result.snapToCanvas = mStoredSnapToCanvas; result.snapToBoxes = mStoredSnapToBoxes; result.snapToNodes = mStoredSnapToNodes; + result.snapAnchorPivot = mStoredSnapAnchorPivot; + result.snapAnchorBounds = mStoredSnapAnchorBounds; result.majorEveryX = mMajorEveryX->value(); result.majorEveryY = mMajorEveryY->value(); result.show = mStoredShow; diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 7b9d650a6..702a209fa 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -80,6 +80,8 @@ class UI_EXPORT GridSettingsDialog : public Friction::Ui::Dialog bool mStoredSnapToCanvas = false; bool mStoredSnapToBoxes = false; bool mStoredSnapToNodes = false; + bool mStoredSnapAnchorPivot = true; + bool mStoredSnapAnchorBounds = true; }; #endif // GRIDSETTINGSDIALOG_H From 729c7eded213a86848bb80dc11c0a6881684abb0 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sat, 25 Oct 2025 16:04:22 +0200 Subject: [PATCH 31/37] Grid: anchors now are optional, "pivot" and "bounding box". Added "nodes anchors" --- src/app/GUI/mainwindow.cpp | 7 +++++++ src/app/GUI/mainwindow.h | 1 + src/app/GUI/menu.cpp | 17 +++++++++++++++-- src/core/Private/document.cpp | 5 +++++ src/core/Private/esettings.cpp | 4 ++++ src/core/Private/esettings.h | 1 + src/core/canvasmouseinteractions.cpp | 6 ++++-- src/core/gridcontroller.cpp | 1 + src/core/gridcontroller.h | 1 + src/ui/dialogs/gridsettingsdialog.cpp | 3 +++ src/ui/dialogs/gridsettingsdialog.h | 1 + 11 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index 14c8c61c0..b1f7765b4 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -126,6 +126,7 @@ MainWindow::MainWindow(Document& document, , mSnapToNodesAct(nullptr) , mSnapAnchorPivotAct(nullptr) , mSnapAnchorBoundsAct(nullptr) + , mSnapAnchorNodesAct(nullptr) , mGridSettingsAct(nullptr) , mGridDrawOnTopAct(nullptr) , mAddToQueAct(nullptr) @@ -289,6 +290,12 @@ void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& setti 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) { diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 9975297ad..acbe1f4fa 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -266,6 +266,7 @@ class MainWindow : public QMainWindow QAction *mSnapToNodesAct; QAction *mSnapAnchorPivotAct; QAction *mSnapAnchorBoundsAct; + QAction *mSnapAnchorNodesAct; QAction *mGridSettingsAct; QAction *mGridDrawOnTopAct; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index e14b4e777..72e4721a6 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -611,7 +611,9 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapToNodesAct); - mSnapAnchorPivotAct = mGridMenu->addAction(tr("Pivot")); + 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) { @@ -622,7 +624,7 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapAnchorPivotAct); - mSnapAnchorBoundsAct = mGridMenu->addAction(tr("Bounding box")); + mSnapAnchorBoundsAct = mGridMenu->addAction(tr("Bounding box anchors")); mSnapAnchorBoundsAct->setCheckable(true); mSnapAnchorBoundsAct->setChecked(mDocument.gridController().settings.snapAnchorBounds); connect(mSnapAnchorBoundsAct, &QAction::toggled, this, [this](bool checked) { @@ -633,6 +635,17 @@ void MainWindow::setupMenuBar() }); 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")); diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 36bd9588f..44b112af3 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -147,6 +147,7 @@ void Document::loadGridSettingsFromSettings() defaults.snapToNodes = settingsMgr->fGridSnapToNodes; defaults.snapAnchorPivot = settingsMgr->fGridSnapAnchorPivot; defaults.snapAnchorBounds = settingsMgr->fGridSnapAnchorBounds; + defaults.snapAnchorNodes = settingsMgr->fGridSnapAnchorNodes; } GridSettings loaded = defaults; loaded.sizeX = AppSupport::getSettings("grid", "sizeX", defaults.sizeX).toDouble(); @@ -162,6 +163,7 @@ void Document::loadGridSettingsFromSettings() loaded.snapToNodes = AppSupport::getSettings("grid", "snapToNodes", defaults.snapToNodes).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) @@ -235,6 +237,7 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const AppSupport::setSettings("grid", "snapToNodes", settings.snapToNodes); 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); @@ -256,6 +259,7 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) settingsMgr->fGridSnapToNodes = sanitized.snapToNodes; settingsMgr->fGridSnapAnchorPivot = sanitized.snapAnchorPivot; settingsMgr->fGridSnapAnchorBounds = sanitized.snapAnchorBounds; + settingsMgr->fGridSnapAnchorNodes = sanitized.snapAnchorNodes; settingsMgr->saveKeyToFile("gridColor"); settingsMgr->saveKeyToFile("gridMajorColor"); settingsMgr->saveKeyToFile("gridDrawOnTop"); @@ -264,6 +268,7 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) settingsMgr->saveKeyToFile("gridSnapToNodes"); settingsMgr->saveKeyToFile("gridSnapAnchorPivot"); settingsMgr->saveKeyToFile("gridSnapAnchorBounds"); + settingsMgr->saveKeyToFile("gridSnapAnchorNodes"); } saveGridSettingsToSettings(sanitized); } diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index 72fc59a23..a37f4d926 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -303,6 +303,10 @@ eSettings::eSettings(const int cpuThreads, fGridSnapAnchorBounds, "gridSnapAnchorBounds", true); + gSettings << std::make_shared( + fGridSnapAnchorNodes, + "gridSnapAnchorNodes", + false); gSettings << std::make_shared( fObjectKeyframeColor, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 70d3933cb..22d20473d 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -162,6 +162,7 @@ class CORE_EXPORT eSettings : public QObject bool fGridSnapToNodes = false; bool fGridSnapAnchorPivot = true; bool fGridSnapAnchorBounds = true; + bool fGridSnapAnchorNodes = false; QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 8021435ac..a5fb11774 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -1138,7 +1138,7 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { mGridSnapAnchorOffsets.emplace_back(center - mGridMoveStartPivot); } - if (gridSettings.snapToNodes) { + if (gridSettings.snapAnchorNodes) { const MovablePoint::PtOp gatherOffsets = [&](MovablePoint* point) { if (!point || !point->isSmartNodePoint()) { return; } const auto* node = static_cast(point); @@ -1155,6 +1155,7 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { 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 boxesSnapEnabled = gridSettings.snapToBoxes; const bool nodesSnapEnabled = gridSettings.snapToNodes; std::vector boxTargets; @@ -1166,7 +1167,8 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); const bool snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas || - hasBoxTargets || hasNodeTargets; + hasBoxTargets || hasNodeTargets || + hasAnchorOffsets; if (!mSelectedBoxes.isEmpty() && mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { const QPointF targetPivot = mGridMoveStartPivot + moveBy; diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index 516880f0c..5e862746c 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -142,6 +142,7 @@ bool GridSettings::operator==(const GridSettings& other) const snapToNodes == other.snapToNodes && snapAnchorPivot == other.snapAnchorPivot && snapAnchorBounds == other.snapAnchorBounds && + snapAnchorNodes == other.snapAnchorNodes && majorEveryX == other.majorEveryX && majorEveryY == other.majorEveryY && thisColor == otherColor && diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 871110fa9..7d80368cb 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -67,6 +67,7 @@ struct CORE_EXPORT GridSettings { bool snapToNodes = false; bool snapAnchorPivot = true; bool snapAnchorBounds = true; + bool snapAnchorNodes = false; int majorEveryX = 8; int majorEveryY = 8; qsptr colorAnimator; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 7b903058f..40a01cfae 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -72,6 +72,7 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mStoredSnapToNodes(false) , mStoredSnapAnchorPivot(true) , mStoredSnapAnchorBounds(true) + , mStoredSnapAnchorNodes(false) { setModal(false); const QColor defaultMinor = GridSettings::defaults().colorAnimator->getColor(); @@ -214,6 +215,7 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mStoredSnapToNodes = settings.snapToNodes; mStoredSnapAnchorPivot = settings.snapAnchorPivot; mStoredSnapAnchorBounds = settings.snapAnchorBounds; + mStoredSnapAnchorNodes = settings.snapAnchorNodes; mMajorEveryX->setValue(settings.majorEveryX); mMajorEveryY->setValue(settings.majorEveryY); mStoredShow = settings.show; @@ -261,6 +263,7 @@ GridSettings GridSettingsDialog::settings() const result.snapToNodes = mStoredSnapToNodes; result.snapAnchorPivot = mStoredSnapAnchorPivot; result.snapAnchorBounds = mStoredSnapAnchorBounds; + result.snapAnchorNodes = mStoredSnapAnchorNodes; result.majorEveryX = mMajorEveryX->value(); result.majorEveryY = mMajorEveryY->value(); result.show = mStoredShow; diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 702a209fa..978a765d5 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -82,6 +82,7 @@ class UI_EXPORT GridSettingsDialog : public Friction::Ui::Dialog bool mStoredSnapToNodes = false; bool mStoredSnapAnchorPivot = true; bool mStoredSnapAnchorBounds = true; + bool mStoredSnapAnchorNodes = false; }; #endif // GRIDSETTINGSDIALOG_H From 4e70b79cc4dd93655b2c379dcca41629d6000c5f Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sat, 25 Oct 2025 16:40:09 +0200 Subject: [PATCH 32/37] Grid: add "snap to pivots" option --- src/app/GUI/mainwindow.cpp | 7 ++++ src/app/GUI/mainwindow.h | 1 + src/app/GUI/menu.cpp | 11 +++++++ src/core/Private/document.cpp | 5 +++ src/core/Private/esettings.cpp | 4 +++ src/core/Private/esettings.h | 1 + src/core/canvas.h | 4 ++- src/core/canvasmouseinteractions.cpp | 47 +++++++++++++++++++-------- src/core/gridcontroller.cpp | 17 ++++++++-- src/core/gridcontroller.h | 2 ++ src/ui/dialogs/gridsettingsdialog.cpp | 3 ++ src/ui/dialogs/gridsettingsdialog.h | 1 + 12 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index b1f7765b4..4a9895560 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -124,6 +124,7 @@ MainWindow::MainWindow(Document& document, , mSnapToCanvasAct(nullptr) , mSnapToBoxesAct(nullptr) , mSnapToNodesAct(nullptr) + , mSnapToPivotsAct(nullptr) , mSnapAnchorPivotAct(nullptr) , mSnapAnchorBoundsAct(nullptr) , mSnapAnchorNodesAct(nullptr) @@ -278,6 +279,12 @@ void MainWindow::onGridSettingsChanged(const Friction::Core::GridSettings& setti 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) { diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index acbe1f4fa..24920b558 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -264,6 +264,7 @@ class MainWindow : public QMainWindow QAction *mSnapToCanvasAct; QAction *mSnapToBoxesAct; QAction *mSnapToNodesAct; + QAction *mSnapToPivotsAct; QAction *mSnapAnchorPivotAct; QAction *mSnapAnchorBoundsAct; QAction *mSnapAnchorNodesAct; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index 72e4721a6..bfd2895f8 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -611,6 +611,17 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapToNodesAct); + 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); + mGridMenu->addSeparator(); mSnapAnchorPivotAct = mGridMenu->addAction(tr("Pivot anchor")); diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 44b112af3..3590c4c73 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -145,6 +145,7 @@ void Document::loadGridSettingsFromSettings() 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; @@ -161,6 +162,7 @@ void Document::loadGridSettingsFromSettings() 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(); @@ -235,6 +237,7 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const 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); @@ -257,6 +260,7 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) 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; @@ -266,6 +270,7 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) settingsMgr->saveKeyToFile("gridSnapToCanvas"); settingsMgr->saveKeyToFile("gridSnapToBoxes"); settingsMgr->saveKeyToFile("gridSnapToNodes"); + settingsMgr->saveKeyToFile("gridSnapToPivots"); settingsMgr->saveKeyToFile("gridSnapAnchorPivot"); settingsMgr->saveKeyToFile("gridSnapAnchorBounds"); settingsMgr->saveKeyToFile("gridSnapAnchorNodes"); diff --git a/src/core/Private/esettings.cpp b/src/core/Private/esettings.cpp index a37f4d926..93a6e77a7 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -295,6 +295,10 @@ eSettings::eSettings(const int cpuThreads, fGridSnapToNodes, "gridSnapToNodes", false); + gSettings << std::make_shared( + fGridSnapToPivots, + "gridSnapToPivots", + false); gSettings << std::make_shared( fGridSnapAnchorPivot, "gridSnapAnchorPivot", diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 22d20473d..e575e6d9f 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -160,6 +160,7 @@ class CORE_EXPORT eSettings : public QObject bool fGridSnapToCanvas = false; bool fGridSnapToBoxes = false; bool fGridSnapToNodes = false; + bool fGridSnapToPivots = false; bool fGridSnapAnchorPivot = true; bool fGridSnapAnchorBounds = true; bool fGridSnapAnchorNodes = false; diff --git a/src/core/canvas.h b/src/core/canvas.h index 742f1ed85..d693dad25 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -951,8 +951,10 @@ class CORE_EXPORT Canvas : public CanvasBase void cancelCurrentTransform(); void cancelCurrentTransformGimzos(); - void collectSnapTargets(bool includeBoxes, + void collectSnapTargets(bool includePivots, + bool includeBounds, bool includeNodes, + std::vector& pivotTargets, std::vector& boxTargets, std::vector& nodeTargets) const; }; diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index a5fb11774..7ff4ae4d8 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -71,15 +71,18 @@ bool pointIsFinite(const QPointF& point) } } -void Canvas::collectSnapTargets(bool includeBoxes, +void Canvas::collectSnapTargets(bool includePivots, + bool includeBounds, bool includeNodes, + std::vector& pivotTargets, std::vector& boxTargets, std::vector& nodeTargets) const { + pivotTargets.clear(); boxTargets.clear(); nodeTargets.clear(); - if ((!includeBoxes && !includeNodes) || !mCurrentContainer) { + if ((!includePivots && !includeBounds && !includeNodes) || !mCurrentContainer) { return; } @@ -99,8 +102,10 @@ void Canvas::collectSnapTargets(bool includeBoxes, const bool visible = box->isVisible(); if (!selectedBranch && visible) { - if (includeBoxes) { - addIfValid(boxTargets, box->getPivotAbsPos()); + if (includePivots) { + addIfValid(pivotTargets, box->getPivotAbsPos()); + } + if (includeBounds) { const QRectF rect = box->getAbsBoundingRect().normalized(); if (!rect.isNull() && rect.isValid()) { addIfValid(boxTargets, rect.topLeft()); @@ -144,19 +149,23 @@ QPointF Canvas::snapPosToGrid(const QPointF& 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 (boxesSnapEnabled || nodesSnapEnabled) { - collectSnapTargets(boxesSnapEnabled, nodesSnapEnabled, boxTargets, 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 || - hasBoxTargets || hasNodeTargets; + hasPivotTargets || hasBoxTargets || hasNodeTargets; const bool shouldForce = (forceSnap && hasSnapSource) || (modifiers & Qt::ControlModifier); @@ -175,6 +184,7 @@ QPointF Canvas::snapPosToGrid(const QPointF& pos, false, canvasRectPtr, nullptr, + hasPivotTargets ? &pivotTargets : nullptr, hasBoxTargets ? &boxTargets : nullptr, hasNodeTargets ? &nodeTargets : nullptr); } @@ -799,18 +809,22 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { mCurrentNormalSegment.makePassThroughAbs(e.fPos, mCurrentNormalSegmentT); } else { const auto& gridSettings = mDocument.gridController().settings; + const bool pivotsSnapEnabled = gridSettings.snapToPivots; const bool boxesSnapEnabled = gridSettings.snapToBoxes; const bool nodesSnapEnabled = gridSettings.snapToNodes; + std::vector pivotTargets; std::vector boxTargets; std::vector nodeTargets; - if (boxesSnapEnabled || nodesSnapEnabled) { - collectSnapTargets(boxesSnapEnabled, nodesSnapEnabled, boxTargets, 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 snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas || - hasBoxTargets || hasNodeTargets; + hasPivotTargets || hasBoxTargets || hasNodeTargets; if(mPressedPoint) { addPointToSelection(mPressedPoint); @@ -897,6 +911,7 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { bypassSnap, canvasPtr, nullptr, + hasPivotTargets ? &pivotTargets : nullptr, hasBoxTargets ? &boxTargets : nullptr, hasNodeTargets ? &nodeTargets : nullptr); if(snapped != targetPos) { @@ -933,6 +948,7 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { bypassSnap, canvasPtr, nullptr, + hasPivotTargets ? &pivotTargets : nullptr, hasBoxTargets ? &boxTargets : nullptr, hasNodeTargets ? &nodeTargets : nullptr); if(snapped != targetPivot) { @@ -1156,18 +1172,22 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; const bool hasAnchorOffsets = !mGridSnapAnchorOffsets.empty(); + const bool pivotsSnapEnabled = gridSettings.snapToPivots; const bool boxesSnapEnabled = gridSettings.snapToBoxes; const bool nodesSnapEnabled = gridSettings.snapToNodes; + std::vector pivotTargets; std::vector boxTargets; std::vector nodeTargets; - if (boxesSnapEnabled || nodesSnapEnabled) { - collectSnapTargets(boxesSnapEnabled, nodesSnapEnabled, boxTargets, 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 snapSourcesAvailable = gridSettings.enabled || gridSettings.snapToCanvas || - hasBoxTargets || hasNodeTargets || + hasPivotTargets || hasBoxTargets || hasNodeTargets || hasAnchorOffsets; if (!mSelectedBoxes.isEmpty() && mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { @@ -1184,6 +1204,7 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { bypassSnap, canvasPtr, &mGridSnapAnchorOffsets, + hasPivotTargets ? &pivotTargets : nullptr, hasBoxTargets ? &boxTargets : nullptr, hasNodeTargets ? &nodeTargets : nullptr); if (snapped != targetPivot) { diff --git a/src/core/gridcontroller.cpp b/src/core/gridcontroller.cpp index 5e862746c..a731a9522 100644 --- a/src/core/gridcontroller.cpp +++ b/src/core/gridcontroller.cpp @@ -140,6 +140,7 @@ bool GridSettings::operator==(const GridSettings& other) const snapToCanvas == other.snapToCanvas && snapToBoxes == other.snapToBoxes && snapToNodes == other.snapToNodes && + snapToPivots == other.snapToPivots && snapAnchorPivot == other.snapAnchorPivot && snapAnchorBounds == other.snapAnchorBounds && snapAnchorNodes == other.snapAnchorNodes && @@ -232,11 +233,14 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, 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 && @@ -244,7 +248,7 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, const bool snapSourcesEnabled = sanitizedSettings.enabled || (sanitizedSettings.snapToCanvas && canvasRectWorld) || - hasBoxTargets || hasNodeTargets; + hasPivotTargets || hasBoxTargets || hasNodeTargets; if ((!snapSourcesEnabled && !forceSnap) || bypassSnap) { return pivotWorld; } @@ -260,7 +264,7 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, } const bool hasCanvasTargets = canUseCanvas && !normalizedCanvas.isEmpty(); - if (!hasGrid && !hasCanvasTargets && !hasBoxTargets && !hasNodeTargets) { + if (!hasGrid && !hasCanvasTargets && !hasPivotTargets && !hasBoxTargets && !hasNodeTargets) { return pivotWorld; } @@ -345,6 +349,15 @@ QPointF GridController::maybeSnapPivot(const QPointF& pivotWorld, } } + + 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) { diff --git a/src/core/gridcontroller.h b/src/core/gridcontroller.h index 7d80368cb..704212cd4 100644 --- a/src/core/gridcontroller.h +++ b/src/core/gridcontroller.h @@ -65,6 +65,7 @@ struct CORE_EXPORT GridSettings { bool snapToCanvas = false; bool snapToBoxes = false; bool snapToNodes = false; + bool snapToPivots = false; bool snapAnchorPivot = true; bool snapAnchorBounds = true; bool snapAnchorNodes = false; @@ -102,6 +103,7 @@ class CORE_EXPORT GridController { 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; diff --git a/src/ui/dialogs/gridsettingsdialog.cpp b/src/ui/dialogs/gridsettingsdialog.cpp index 40a01cfae..73d6fa929 100644 --- a/src/ui/dialogs/gridsettingsdialog.cpp +++ b/src/ui/dialogs/gridsettingsdialog.cpp @@ -70,6 +70,7 @@ GridSettingsDialog::GridSettingsDialog(QWidget* parent) , mStoredSnapToCanvas(false) , mStoredSnapToBoxes(false) , mStoredSnapToNodes(false) + , mStoredSnapToPivots(false) , mStoredSnapAnchorPivot(true) , mStoredSnapAnchorBounds(true) , mStoredSnapAnchorNodes(false) @@ -213,6 +214,7 @@ void GridSettingsDialog::setSettings(const GridSettings& settings) mStoredSnapToCanvas = settings.snapToCanvas; mStoredSnapToBoxes = settings.snapToBoxes; mStoredSnapToNodes = settings.snapToNodes; + mStoredSnapToPivots = settings.snapToPivots; mStoredSnapAnchorPivot = settings.snapAnchorPivot; mStoredSnapAnchorBounds = settings.snapAnchorBounds; mStoredSnapAnchorNodes = settings.snapAnchorNodes; @@ -261,6 +263,7 @@ GridSettings GridSettingsDialog::settings() const result.snapToCanvas = mStoredSnapToCanvas; result.snapToBoxes = mStoredSnapToBoxes; result.snapToNodes = mStoredSnapToNodes; + result.snapToPivots = mStoredSnapToPivots; result.snapAnchorPivot = mStoredSnapAnchorPivot; result.snapAnchorBounds = mStoredSnapAnchorBounds; result.snapAnchorNodes = mStoredSnapAnchorNodes; diff --git a/src/ui/dialogs/gridsettingsdialog.h b/src/ui/dialogs/gridsettingsdialog.h index 978a765d5..d391c9c48 100644 --- a/src/ui/dialogs/gridsettingsdialog.h +++ b/src/ui/dialogs/gridsettingsdialog.h @@ -80,6 +80,7 @@ class UI_EXPORT GridSettingsDialog : public Friction::Ui::Dialog bool mStoredSnapToCanvas = false; bool mStoredSnapToBoxes = false; bool mStoredSnapToNodes = false; + bool mStoredSnapToPivots = false; bool mStoredSnapAnchorPivot = true; bool mStoredSnapAnchorBounds = true; bool mStoredSnapAnchorNodes = false; From 0931d018d7d34e1b3082aeb30132807a63b0e706 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sat, 25 Oct 2025 16:53:46 +0200 Subject: [PATCH 33/37] Grid: added bounding box midpoints to "snap to boxes" --- src/core/canvasmouseinteractions.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 7ff4ae4d8..a22c26844 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -112,7 +112,15 @@ void Canvas::collectSnapTargets(bool includePivots, addIfValid(boxTargets, rect.topRight()); addIfValid(boxTargets, rect.bottomLeft()); addIfValid(boxTargets, rect.bottomRight()); + const QPointF topCenter((rect.left() + rect.right()) * 0.5, rect.top()); + const QPointF bottomCenter((rect.left() + rect.right()) * 0.5, rect.bottom()); + const QPointF leftCenter(rect.left(), (rect.top() + rect.bottom()) * 0.5); + const QPointF rightCenter(rect.right(), (rect.top() + rect.bottom()) * 0.5); addIfValid(boxTargets, rect.center()); + addIfValid(boxTargets, topCenter); + addIfValid(boxTargets, bottomCenter); + addIfValid(boxTargets, leftCenter); + addIfValid(boxTargets, rightCenter); } } if (includeNodes) { From 6c93fedddd2684fc261a61d85e08ed3ef3c56a54 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sat, 25 Oct 2025 16:55:14 +0200 Subject: [PATCH 34/37] Grid: simple menu items tweaks --- src/app/GUI/menu.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index bfd2895f8..7eccc9274 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -589,6 +589,17 @@ void MainWindow::setupMenuBar() }); 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); @@ -611,17 +622,6 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapToNodesAct); - 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); - mGridMenu->addSeparator(); mSnapAnchorPivotAct = mGridMenu->addAction(tr("Pivot anchor")); @@ -635,7 +635,7 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mSnapAnchorPivotAct); - mSnapAnchorBoundsAct = mGridMenu->addAction(tr("Bounding box anchors")); + mSnapAnchorBoundsAct = mGridMenu->addAction(tr("Boxes anchors")); mSnapAnchorBoundsAct->setCheckable(true); mSnapAnchorBoundsAct->setChecked(mDocument.gridController().settings.snapAnchorBounds); connect(mSnapAnchorBoundsAct, &QAction::toggled, this, [this](bool checked) { From a3f83185dd8913732ad3c2e483fc37a18dcad07d Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sat, 25 Oct 2025 18:11:46 +0200 Subject: [PATCH 35/37] Grid: add a global "Snapping" switch --- src/app/GUI/mainwindow.cpp | 12 ++++++++++ src/app/GUI/mainwindow.h | 2 ++ src/app/GUI/menu.cpp | 11 ++++++++- src/core/Private/document.cpp | 28 ++++++++++++++++++++++ src/core/Private/document.h | 5 ++++ src/core/Private/esettings.cpp | 4 ++++ src/core/Private/esettings.h | 1 + src/core/canvasmouseinteractions.cpp | 36 ++++++++++++++++------------ 8 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index 4a9895560..bcb50ec78 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -120,6 +120,7 @@ MainWindow::MainWindow(Document& document, , mClearSelAct(nullptr) , mAddKeyAct(nullptr) , mShowGridAct(nullptr) + , mSnappingAct(nullptr) , mSnapToGridAct(nullptr) , mSnapToCanvasAct(nullptr) , mSnapToBoxesAct(nullptr) @@ -177,6 +178,8 @@ MainWindow::MainWindow(Document& document, 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); @@ -320,6 +323,15 @@ void MainWindow::onGridSnapEnabledChanged(bool 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 24920b558..c0c2b77f3 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -202,6 +202,7 @@ class MainWindow : public QMainWindow 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); @@ -260,6 +261,7 @@ class MainWindow : public QMainWindow QAction *mZoomOutAction; QAction *mFitViewAction; QAction *mShowGridAct; + QAction *mSnappingAct; QAction *mSnapToGridAct; QAction *mSnapToCanvasAct; QAction *mSnapToBoxesAct; diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index 7eccc9274..0b47d8570 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -566,10 +566,19 @@ void MainWindow::setupMenuBar() }); cmdAddAction(mResetZoomAction); - // TODO: custom icon for Grid menu 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); diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 3590c4c73..68e03ee73 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -122,6 +122,23 @@ void Document::setGridSnapEnabled(const bool 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; @@ -222,6 +239,14 @@ void Document::loadGridSettingsFromSettings() 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 @@ -244,6 +269,7 @@ void Document::saveGridSettingsToSettings(const GridSettings& settings) const 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); @@ -264,6 +290,7 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) settingsMgr->fGridSnapAnchorPivot = sanitized.snapAnchorPivot; settingsMgr->fGridSnapAnchorBounds = sanitized.snapAnchorBounds; settingsMgr->fGridSnapAnchorNodes = sanitized.snapAnchorNodes; + settingsMgr->fGridSnappingActive = mSnappingActive; settingsMgr->saveKeyToFile("gridColor"); settingsMgr->saveKeyToFile("gridMajorColor"); settingsMgr->saveKeyToFile("gridDrawOnTop"); @@ -274,6 +301,7 @@ void Document::saveGridSettingsAsDefault(const GridSettings& settings) settingsMgr->saveKeyToFile("gridSnapAnchorPivot"); settingsMgr->saveKeyToFile("gridSnapAnchorBounds"); settingsMgr->saveKeyToFile("gridSnapAnchorNodes"); + settingsMgr->saveKeyToFile("gridSnappingActive"); } saveGridSettingsToSettings(sanitized); } diff --git a/src/core/Private/document.h b/src/core/Private/document.h index 95986c7bd..24761fce7 100644 --- a/src/core/Private/document.h +++ b/src/core/Private/document.h @@ -76,6 +76,8 @@ class CORE_EXPORT Document : public SingleWidgetTarget { 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); @@ -222,6 +224,7 @@ class CORE_EXPORT Document : public SingleWidgetTarget { 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, @@ -258,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/esettings.cpp b/src/core/Private/esettings.cpp index 93a6e77a7..0c403c4c2 100644 --- a/src/core/Private/esettings.cpp +++ b/src/core/Private/esettings.cpp @@ -311,6 +311,10 @@ eSettings::eSettings(const int cpuThreads, 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 e575e6d9f..d8516e28a 100644 --- a/src/core/Private/esettings.h +++ b/src/core/Private/esettings.h @@ -164,6 +164,7 @@ class CORE_EXPORT eSettings : public QObject bool fGridSnapAnchorPivot = true; bool fGridSnapAnchorBounds = true; bool fGridSnapAnchorNodes = false; + bool fGridSnappingActive = false; QColor fObjectKeyframeColor; QColor fPropertyGroupKeyframeColor; diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index a22c26844..80e15fe5d 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -149,6 +149,10 @@ QPointF Canvas::snapPosToGrid(const QPointF& pos, { if (!mHasWorldToScreen) { return pos; } + if (!mDocument.isSnappingActive()) { + return pos; + } + const auto& gridController = mDocument.gridController(); const auto& settings = gridController.settings; @@ -817,22 +821,23 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { mCurrentNormalSegment.makePassThroughAbs(e.fPos, mCurrentNormalSegmentT); } else { const auto& gridSettings = mDocument.gridController().settings; - const bool pivotsSnapEnabled = gridSettings.snapToPivots; - const bool boxesSnapEnabled = gridSettings.snapToBoxes; - const bool nodesSnapEnabled = gridSettings.snapToNodes; + 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 (pivotsSnapEnabled || boxesSnapEnabled || nodesSnapEnabled) { + 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 = gridSettings.enabled || + const bool snapSourcesAvailable = snappingActive && (gridSettings.enabled || gridSettings.snapToCanvas || - hasPivotTargets || hasBoxTargets || hasNodeTargets; + hasPivotTargets || hasBoxTargets || hasNodeTargets); if(mPressedPoint) { addPointToSelection(mPressedPoint); @@ -903,7 +908,7 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; - if(mHasWorldToScreen && + if(snappingActive && mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { const QPointF targetPos = mGridMoveStartPivot + moveBy; QRectF canvasRect; @@ -940,7 +945,7 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; - if(!mSelectedPoints_d.isEmpty() && mHasWorldToScreen && + if(!mSelectedPoints_d.isEmpty() && snappingActive && mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { const QPointF targetPivot = mGridMoveStartPivot + moveBy; QRectF canvasRect; @@ -1180,24 +1185,25 @@ void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { const bool bypassSnap = e.fModifiers & Qt::AltModifier; const bool forceSnap = e.fModifiers & Qt::ControlModifier; const bool hasAnchorOffsets = !mGridSnapAnchorOffsets.empty(); - const bool pivotsSnapEnabled = gridSettings.snapToPivots; - const bool boxesSnapEnabled = gridSettings.snapToBoxes; - const bool nodesSnapEnabled = gridSettings.snapToNodes; + 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 (pivotsSnapEnabled || boxesSnapEnabled || nodesSnapEnabled) { + 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 = gridSettings.enabled || + const bool snapSourcesAvailable = snappingActive && (gridSettings.enabled || gridSettings.snapToCanvas || hasPivotTargets || hasBoxTargets || hasNodeTargets || - hasAnchorOffsets; - if (!mSelectedBoxes.isEmpty() && mHasWorldToScreen && + hasAnchorOffsets); + if (!mSelectedBoxes.isEmpty() && snappingActive && mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { const QPointF targetPivot = mGridMoveStartPivot + moveBy; QRectF canvasRect; From cce503670191cb028da54c1c38bb15af25ce0644 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sun, 26 Oct 2025 10:52:06 +0100 Subject: [PATCH 36/37] Grid: add bounding box points of selected object as snapping points for self pivot translation --- src/core/canvas.h | 3 +- src/core/canvasmouseinteractions.cpp | 56 +++++++++++++++++++--------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/core/canvas.h b/src/core/canvas.h index d693dad25..b01383b97 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -956,7 +956,8 @@ class CORE_EXPORT Canvas : public CanvasBase bool includeNodes, std::vector& pivotTargets, std::vector& boxTargets, - std::vector& nodeTargets) const; + std::vector& nodeTargets, + bool includeSelectedBounds = false) const; }; #endif // CANVAS_H diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index 80e15fe5d..a94c56636 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -76,7 +76,8 @@ void Canvas::collectSnapTargets(bool includePivots, bool includeNodes, std::vector& pivotTargets, std::vector& boxTargets, - std::vector& nodeTargets) const + std::vector& nodeTargets, + bool includeSelectedBounds) const { pivotTargets.clear(); boxTargets.clear(); @@ -92,6 +93,30 @@ void Canvas::collectSnapTargets(bool includePivots, } }; + 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; } @@ -106,22 +131,7 @@ void Canvas::collectSnapTargets(bool includePivots, addIfValid(pivotTargets, box->getPivotAbsPos()); } if (includeBounds) { - const QRectF rect = box->getAbsBoundingRect().normalized(); - if (!rect.isNull() && rect.isValid()) { - addIfValid(boxTargets, rect.topLeft()); - addIfValid(boxTargets, rect.topRight()); - addIfValid(boxTargets, rect.bottomLeft()); - addIfValid(boxTargets, rect.bottomRight()); - const QPointF topCenter((rect.left() + rect.right()) * 0.5, rect.top()); - const QPointF bottomCenter((rect.left() + rect.right()) * 0.5, rect.bottom()); - const QPointF leftCenter(rect.left(), (rect.top() + rect.bottom()) * 0.5); - const QPointF rightCenter(rect.right(), (rect.top() + rect.bottom()) * 0.5); - addIfValid(boxTargets, rect.center()); - addIfValid(boxTargets, topCenter); - addIfValid(boxTargets, bottomCenter); - addIfValid(boxTargets, leftCenter); - addIfValid(boxTargets, rightCenter); - } + appendBoundsTargets(box->getAbsBoundingRect()); } if (includeNodes) { auto* mutableBox = const_cast(box); @@ -141,6 +151,13 @@ void Canvas::collectSnapTargets(bool includePivots, }; 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, @@ -828,9 +845,12 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { 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); + pivotTargets, boxTargets, nodeTargets, + includeSelectedBounds); } const bool hasPivotTargets = pivotsSnapEnabled && !pivotTargets.empty(); const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); From f5e5fcc5bae633604183031f9ceed55016ceaa70 Mon Sep 17 00:00:00 2001 From: Pablo Gil Date: Sun, 26 Oct 2025 12:01:57 +0100 Subject: [PATCH 37/37] Grid: some fixes from comments --- src/core/Private/document.cpp | 19 ++++++++++--------- src/core/Private/documentrw.cpp | 13 +++---------- src/core/canvasmouseinteractions.cpp | 9 +-------- src/core/simplemath.cpp | 5 +++++ src/core/simplemath.h | 3 +++ 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 68e03ee73..51b284a48 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -157,15 +157,16 @@ void Document::setGridSettings(const GridSettings& settings) void Document::loadGridSettingsFromSettings() { GridSettings defaults; - if (auto* settingsMgr = eSettings::sInstance) { - 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; + 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(); diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 31817ffde..2773f2f52 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -112,17 +112,12 @@ void Document::readGridSettings(eReadStream &src) bool show = mGridController.settings.show; src >> enabled; src >> show; - const int fileVersion = src.evFileVersion(); - if (fileVersion >= EvFormat::grids) { - src >> settings.majorEveryX; - src >> settings.majorEveryY; - } + src >> settings.majorEveryX; + src >> settings.majorEveryY; QColor color; src >> color; QColor majorColor = color; - if (fileVersion >= EvFormat::grids) { - src >> majorColor; - } + src >> majorColor; settings.enabled = enabled; settings.show = show; if (!settings.colorAnimator) { @@ -264,8 +259,6 @@ void Document::writeDoxumentXEV(QDomDocument& doc) const { gridSettings.setAttribute("show", grid.show ? "true" : "false"); gridSettings.setAttribute("majorEveryX", QString::number(grid.majorEveryX)); gridSettings.setAttribute("majorEveryY", QString::number(grid.majorEveryY)); - // Legacy attribute for older consumers expecting a unified value. - gridSettings.setAttribute("majorEvery", QString::number(grid.majorEveryX)); 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)); diff --git a/src/core/canvasmouseinteractions.cpp b/src/core/canvasmouseinteractions.cpp index a94c56636..8fc4dea8a 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -64,13 +64,6 @@ using namespace Friction::Core; -namespace { -bool pointIsFinite(const QPointF& point) -{ - return std::isfinite(point.x()) && std::isfinite(point.y()); -} -} - void Canvas::collectSnapTargets(bool includePivots, bool includeBounds, bool includeNodes, @@ -88,7 +81,7 @@ void Canvas::collectSnapTargets(bool includePivots, } auto addIfValid = [](std::vector& target, const QPointF& pt) { - if (pointIsFinite(pt)) { + if (isPointFinite(pt)) { target.push_back(pt); } }; 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