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/extraactions.cpp b/src/app/GUI/extraactions.cpp index 84e7f9352..e66ac8de5 100644 --- a/src/app/GUI/extraactions.cpp +++ b/src/app/GUI/extraactions.cpp @@ -647,48 +647,6 @@ void MainWindow::setupMenuExtras() } } -void MainWindow::setupMenuGizmo(QMenu *menu, - const Core::Gizmos::Interact &ti) -{ - if (!menu) { return; } - - const bool visible = mDocument.getGizmoVisibility(ti); - QString title; - - switch (ti) { - case Core::Gizmos::Interact::Position: - title = tr("Position"); - break; - case Core::Gizmos::Interact::Rotate: - title = tr("Rotate"); - break; - case Core::Gizmos::Interact::Scale: - title = tr("Scale"); - break; - case Core::Gizmos::Interact::Shear: - title = tr("Shear"); - break; - default: return; - } - - const auto act = menu->addAction(title); - act->setCheckable(true); - act->setChecked(visible); - - connect(act, &QAction::toggled, - this, [this, ti]() { - mDocument.setGizmoVisibility(ti, !mDocument.getGizmoVisibility(ti)); - }); - connect(&mDocument, &Document::gizmoVisibilityChanged, - this, [ti, act](Core::Gizmos::Interact i, - bool enabled) { - if (ti != i) { return; } - act->blockSignals(true); - act->setChecked(enabled); - act->blockSignals(false); - }); -} - void MainWindow::setupPropertiesActions() { mViewMenu->addSeparator(); diff --git a/src/app/GUI/mainwindow.cpp b/src/app/GUI/mainwindow.cpp index ef419bac7..5c4f282ca 100644 --- a/src/app/GUI/mainwindow.cpp +++ b/src/app/GUI/mainwindow.cpp @@ -464,6 +464,7 @@ void MainWindow::updateSettingsForCurrentCanvas(Canvas* const scene) void MainWindow::setupToolBar() { + mToolBox = new Ui::ToolBox(mActions, mDocument, this); mToolbar = new Ui::ToolBar(tr("Toolbar"), "MainToolBar", this); @@ -474,7 +475,6 @@ void MainWindow::setupToolBar() this, [this](const QString &msg){ statusBar()->showMessage(msg, 500); }); addToolBar(mColorToolBar); - mToolBox = new Ui::ToolBox(mActions, mDocument, this); { const auto toolbar = mToolBox->getToolBar(Ui::ToolBox::Main); if (toolbar) { addToolBar(Qt::LeftToolBarArea, toolbar); } @@ -496,6 +496,11 @@ void MainWindow::setupToolBar() statusBar()->addPermanentWidget(mCanvasToolBar); + { + const auto toolbar = mToolBox->getToolBar(Ui::ToolBox::Interact); + if (toolbar) { statusBar()->addPermanentWidget(toolbar); } + } + connect(&mAudioHandler, &AudioHandler::deviceChanged, this, [this]() { statusBar()->showMessage(tr("Changed audio output: %1").arg(mAudioHandler.getDeviceName()), diff --git a/src/app/GUI/mainwindow.h b/src/app/GUI/mainwindow.h index 37ba7eaf7..cc5b50fe5 100644 --- a/src/app/GUI/mainwindow.h +++ b/src/app/GUI/mainwindow.h @@ -50,7 +50,6 @@ #include "ekeyfilter.h" #include "window.h" #include "GUI/RenderWidgets/renderwidget.h" -#include "gizmos.h" #include "widgets/fontswidget.h" #include "widgets/toolbar.h" @@ -314,8 +313,6 @@ class MainWindow : public QMainWindow void setupMenuScene(); void setupMenuEffects(); void setupMenuExtras(); - void setupMenuGizmo(QMenu *menu, - const Friction::Core::Gizmos::Interact &ti); void setupPropertiesActions(); BoundingBox* getCurrentBox(); diff --git a/src/app/GUI/menu.cpp b/src/app/GUI/menu.cpp index 52173aa75..eb7c6efd2 100644 --- a/src/app/GUI/menu.cpp +++ b/src/app/GUI/menu.cpp @@ -638,26 +638,6 @@ void MainWindow::setupMenuBar() mDynamicQuality->setCheckable(true); mDynamicQuality->setChecked(eFilterSettings::sSmartDisplat()); - const auto menuGizmo = mViewMenu->addMenu(QIcon::fromTheme("gizmos"), - tr("Transform Interacts")); - setupMenuGizmo(menuGizmo, Core::Gizmos::Interact::Position); - setupMenuGizmo(menuGizmo, Core::Gizmos::Interact::Rotate); - setupMenuGizmo(menuGizmo, Core::Gizmos::Interact::Scale); - setupMenuGizmo(menuGizmo, Core::Gizmos::Interact::Shear); - { - const auto controls = enve_cast(mToolBox->getToolBar(Ui::ToolBox::Controls)); - if (controls) { - const auto act = menuGizmo->addAction(tr("Show in Controls")); - act->setCheckable(true); - act->setChecked(controls->getTransformInteractVisibility()); - connect(act, &QAction::triggered, - this, [controls](bool checked) { - if (!controls) { return; } - controls->setTransformInteractVisibility(checked); - }); - } - } - mClipViewToCanvas = mViewMenu->addAction( tr("Clip to Scene", "MenuBar_View")); mClipViewToCanvas->setCheckable(true); diff --git a/src/app/friction.qss b/src/app/friction.qss index 3a844aaa1..3207dfd3e 100644 --- a/src/app/friction.qss +++ b/src/app/friction.qss @@ -668,15 +668,33 @@ ExpressionEditor { background: %6; } -#ToolBoxButton, -#ToolBoxGizmo { +#ToolBoxButton { margin-top: .05em; margin-bottom: .05em; margin-left: .1em; margin-right: .1em; } +#ToolBoxGizmo { + margin-left: .1em; + margin-right: .1em; +} + #ToolBoxButton:disabled, #ToolBoxGizmo:!checked { background: transparent; } + +QToolButton#ToolBoxGizmo[popupMode="1"] { + padding-right: %10px; +} + +QToolButton#ToolBoxGizmo[popupMode="1"]::menu-button { + width: %10px; +} + +QToolButton#ToolBoxGizmo[popupMode="1"]::menu-arrow { + image: url(:/icons/hicolor/%11x%11/actions/go-down.png); + width: %10px; + height: %10px; +} diff --git a/src/app/icons b/src/app/icons index c19e05952..867bf6457 160000 --- a/src/app/icons +++ b/src/app/icons @@ -1 +1 @@ -Subproject commit c19e059527a5ca96b6a6f831df55e88ed948d677 +Subproject commit 867bf64575aa2eb31fd837c6541be54d00e68c24 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fb668b8b0..098671322 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -54,6 +54,7 @@ include_directories( set( SOURCES appsupport.cpp + grid.cpp Boxes/nullobject.cpp Expressions/expression.cpp Expressions/expressionpresets.cpp @@ -255,6 +256,7 @@ set( canvasmouseevents.cpp canvasmouseinteractions.cpp canvasgizmos.cpp + canvasgrid.cpp canvasselectedboxesactions.cpp canvasselectedpointsactions.cpp clipboardcontainer.cpp @@ -359,6 +361,7 @@ set( set( HEADERS appsupport.h + grid.h Boxes/nullobject.h Expressions/expression.h Expressions/expressionpresets.h diff --git a/src/core/GUI/coloranimatorbutton.cpp b/src/core/GUI/coloranimatorbutton.cpp index 53400b06d..3d79e1e9f 100644 --- a/src/core/GUI/coloranimatorbutton.cpp +++ b/src/core/GUI/coloranimatorbutton.cpp @@ -112,8 +112,7 @@ void ColorAnimatorButton::openColorSettingsDialog() mColorTarget); } else { const auto colOp = [this](const ColorSetting& setting) { - mColor = setting.getColor(); - update(); + setColor(setting.getColor()); }; colorWidget = eWidgets::sColorWidget(dialog, mColor, @@ -130,6 +129,8 @@ void ColorAnimatorButton::setColor(const QColor &color) { mColor = color; update(); + + emit colorChanged(color); } QColor ColorAnimatorButton::color() const diff --git a/src/core/GUI/coloranimatorbutton.h b/src/core/GUI/coloranimatorbutton.h index 5009092a2..db5e1bdc4 100644 --- a/src/core/GUI/coloranimatorbutton.h +++ b/src/core/GUI/coloranimatorbutton.h @@ -28,10 +28,14 @@ #include "boxeslistactionbutton.h" #include "smartPointers/selfref.h" #include "conncontextptr.h" + class ColorAnimator; -class CORE_EXPORT ColorAnimatorButton : public BoxesListActionButton { +class CORE_EXPORT ColorAnimatorButton : public BoxesListActionButton +{ + Q_OBJECT ColorAnimatorButton(QWidget * const parent = nullptr); + public: ColorAnimatorButton(ColorAnimator * const colorTarget, QWidget * const parent = nullptr); @@ -43,8 +47,13 @@ class CORE_EXPORT ColorAnimatorButton : public BoxesListActionButton { void setColor(const QColor& color); QColor color() const; + +signals: + void colorChanged(const QColor &color); + protected: void paintEvent(QPaintEvent *); + private: QColor mColor; ConnContextQPtr mColorTarget; diff --git a/src/core/Private/document.cpp b/src/core/Private/document.cpp index 28085eaf8..017ffa47e 100644 --- a/src/core/Private/document.cpp +++ b/src/core/Private/document.cpp @@ -24,10 +24,12 @@ // Fork of enve - Copyright (C) 2016-2020 Maurycy Liebner #include "Private/document.h" -#include "FileCacheHandlers/filecachehandler.h" #include "canvas.h" #include "simpletask.h" +#include +#include + Document* Document::sInstance = nullptr; using namespace Friction::Core; @@ -36,20 +38,37 @@ Document::Document(TaskScheduler& taskScheduler) { Q_ASSERT(!sInstance); sInstance = this; + + mGrid = new Grid(this); + mGrid->setSettings(eSettings::instance().fGrid); + connect(mGrid, &Grid::changed, + this, [this](const Grid::Settings &settings) { + qDebug() << "Grid Changed"; + Grid::debugSettings(settings); + updateScenes(); + }); + connect(&taskScheduler, &TaskScheduler::finishedAllQuedTasks, this, &Document::updateScenes); } -void Document::updateScenes() { +Grid* Document::getGrid() +{ + return mGrid; +} + +void Document::updateScenes() +{ SimpleTask::sProcessAll(); TaskScheduler::instance()->queTasks(); - for(const auto& scene : fVisibleScenes) { + for (const auto& scene : fVisibleScenes) { emit scene.first->requestUpdate(); } } -void Document::actionFinished() { +void Document::actionFinished() +{ updateScenes(); for (const auto& scene : fVisibleScenes) { const auto newUndoRedo = scene.first->newUndoRedoSet(); @@ -60,49 +79,59 @@ void Document::actionFinished() { } } -void Document::replaceClipboard(const stdsptr &container) { +void Document::replaceClipboard(const stdsptr &container) +{ fClipboardContainer = container; } -Clipboard *Document::getClipboard(const ClipboardType type) const { - if(!fClipboardContainer) return nullptr; - if(type == fClipboardContainer->getType()) +Clipboard *Document::getClipboard(const ClipboardType type) const +{ + if (!fClipboardContainer) { return nullptr; } + if (type == fClipboardContainer->getType()) { return fClipboardContainer.get(); + } return nullptr; } -DynamicPropsClipboard* Document::getDynamicPropsClipboard() const { +DynamicPropsClipboard* Document::getDynamicPropsClipboard() const +{ auto contT = getClipboard(ClipboardType::dynamicProperties); return static_cast(contT); } -PropertyClipboard* Document::getPropertyClipboard() const { +PropertyClipboard* Document::getPropertyClipboard() const +{ auto contT = getClipboard(ClipboardType::property); return static_cast(contT); } -KeysClipboard* Document::getKeysClipboard() const { +KeysClipboard* Document::getKeysClipboard() const +{ auto contT = getClipboard(ClipboardType::keys); return static_cast(contT); } -BoxesClipboard* Document::getBoxesClipboard() const { +BoxesClipboard* Document::getBoxesClipboard() const +{ auto contT = getClipboard(ClipboardType::boxes); return static_cast(contT); } -SmartPathClipboard* Document::getSmartPathClipboard() const { +SmartPathClipboard* Document::getSmartPathClipboard() const +{ auto contT = getClipboard(ClipboardType::smartPath); return static_cast(contT); } -void Document::setPath(const QString &path) { +void Document::setPath(const QString &path) +{ fEvFile = path; emit evFilePathChanged(fEvFile); } -QString Document::projectDirectory() const { - if(fEvFile.isEmpty()) { +QString Document::projectDirectory() const +{ + if (fEvFile.isEmpty()) { return QDir::homePath(); } else { QFileInfo fileInfo(fEvFile); @@ -110,7 +139,8 @@ QString Document::projectDirectory() const { } } -void Document::setCanvasMode(const CanvasMode mode) { +void Document::setCanvasMode(const CanvasMode mode) +{ fCanvasMode = mode; emit canvasModeSet(mode); actionFinished(); @@ -142,6 +172,11 @@ void Document::setGizmoVisibility(const Gizmos::Interact &ti, key = "Shear"; fGizmoShearVisibility = visibility; break; + case Gizmos::Interact::All: + if (fGizmoAllVisibility == visibility) { return; } + key = "All"; + fGizmoAllVisibility = visibility; + break; default: return; } @@ -158,16 +193,14 @@ bool Document::getGizmoVisibility(const Gizmos::Interact &ti) switch (ti) { case Gizmos::Interact::Position: return fGizmoPositionVisibility; - break; case Gizmos::Interact::Rotate: return fGizmoRotateVisibility; - break; case Gizmos::Interact::Scale: return fGizmoScaleVisibility; - break; case Gizmos::Interact::Shear: return fGizmoShearVisibility; - break; + case Gizmos::Interact::All: + return fGizmoAllVisibility; default:; } return false; @@ -187,6 +220,8 @@ Canvas *Document::createNewScene(const bool emitCreated) fGizmoScaleVisibility); newScene->setGizmoVisibility(Gizmos::Interact::Shear, fGizmoShearVisibility); + newScene->setGizmoVisibility(Gizmos::Interact::All, + fGizmoAllVisibility); if (emitCreated) { emit sceneCreated(newScene.get()); @@ -194,13 +229,15 @@ Canvas *Document::createNewScene(const bool emitCreated) return newScene.get(); } -bool Document::removeScene(const qsptr& scene) { +bool Document::removeScene(const qsptr& scene) +{ const int id = fScenes.indexOf(scene); return removeScene(id); } -bool Document::removeScene(const int id) { - if(id < 0 || id >= fScenes.count()) return false; +bool Document::removeScene(const int id) +{ + if (id < 0 || id >= fScenes.count()) { return false; } const auto scene = fScenes.takeAt(id); SWT_removeChild(scene.data()); emit sceneRemoved(scene.data()); @@ -208,23 +245,26 @@ bool Document::removeScene(const int id) { return true; } -void Document::addVisibleScene(Canvas * const scene) { +void Document::addVisibleScene(Canvas * const scene) +{ fVisibleScenes[scene]++; updateScenes(); } -bool Document::removeVisibleScene(Canvas * const scene) { +bool Document::removeVisibleScene(Canvas * const scene) +{ const auto it = fVisibleScenes.find(scene); - if(it == fVisibleScenes.end()) return false; - if(it->second == 1) fVisibleScenes.erase(it); - else it->second--; + if (it == fVisibleScenes.end()) { return false; } + if (it->second == 1) { fVisibleScenes.erase(it); } + else { it->second--; } return true; } -void Document::setActiveScene(Canvas * const scene) { - if(scene == fActiveScene) return; +void Document::setActiveScene(Canvas * const scene) +{ + if (scene == fActiveScene) { return; } auto& conn = fActiveScene.assign(scene); - if(fActiveScene) { + if (fActiveScene) { conn << connect(fActiveScene, &Canvas::currentBoxChanged, this, &Document::currentBoxChanged); conn << connect(fActiveScene, &Canvas::selectedPaintSettingsChanged, @@ -253,43 +293,52 @@ void Document::setActiveScene(Canvas * const scene) { emit activeSceneSet(scene); } -void Document::clearActiveScene() { +void Document::clearActiveScene() +{ setActiveScene(nullptr); } -int Document::getActiveSceneFrame() const { - if(!fActiveScene) return 0; +int Document::getActiveSceneFrame() const +{ + if (!fActiveScene) { return 0; } return fActiveScene->anim_getCurrentAbsFrame(); } -void Document::setActiveSceneFrame(const int frame) { - if(!fActiveScene) return; - if(fActiveScene->anim_getCurrentRelFrame() == frame) return; +void Document::setActiveSceneFrame(const int frame) +{ + if (!fActiveScene) { return; } + if (fActiveScene->anim_getCurrentRelFrame() == frame) { return; } fActiveScene->anim_setAbsFrame(frame); emit activeSceneFrameSet(frame); } -void Document::incActiveSceneFrame() { +void Document::incActiveSceneFrame() +{ setActiveSceneFrame(getActiveSceneFrame() + 1); } -void Document::decActiveSceneFrame() { +void Document::decActiveSceneFrame() +{ setActiveSceneFrame(getActiveSceneFrame() - 1); } -void Document::addBookmarkBrush(SimpleBrushWrapper * const brush) { - if(!brush) return; +void Document::addBookmarkBrush(SimpleBrushWrapper * const brush) +{ + if (!brush) { return; } removeBookmarkBrush(brush); fBrushes << brush; emit bookmarkBrushAdded(brush); } -void Document::removeBookmarkBrush(SimpleBrushWrapper * const brush) { - if(fBrushes.removeOne(brush)) +void Document::removeBookmarkBrush(SimpleBrushWrapper * const brush) +{ + if (fBrushes.removeOne(brush)) { emit bookmarkBrushRemoved(brush); + } } -void Document::addBookmarkColor(const QColor &color) { +void Document::addBookmarkColor(const QColor &color) +{ removeBookmarkColor(color); fColors << color; emit bookmarkColorAdded(color); @@ -307,9 +356,10 @@ void Document::removeBookmarkColor(const QColor &color) } } -void Document::setBrush(BrushContexedWrapper * const brush) { +void Document::setBrush(BrushContexedWrapper * const brush) +{ fBrush = brush->getSimpleBrush(); - if(fBrush) { + if (fBrush) { fBrush->setColor(fBrushColor); switch(fPaintMode) { case PaintMode::normal: fBrush->setNormalMode(); break; @@ -324,33 +374,38 @@ void Document::setBrush(BrushContexedWrapper * const brush) { emit brushColorChanged(fBrush ? fBrush->getColor() : Qt::white); } -void Document::setBrushColor(const QColor &color) { +void Document::setBrushColor(const QColor &color) +{ fBrushColor = color; - if(fBrush) fBrush->setColor(fBrushColor); + if (fBrush) { fBrush->setColor(fBrushColor); } emit brushColorChanged(color); } -void Document::incBrushRadius() { - if(!fBrush) return; +void Document::incBrushRadius() +{ + if (!fBrush) { return; } fBrush->incPaintBrushSize(0.3); emit brushSizeChanged(fBrush->getBrushSize()); } -void Document::decBrushRadius() { - if(!fBrush) return; +void Document::decBrushRadius() +{ + if (!fBrush) { return; } fBrush->decPaintBrushSize(0.3); emit brushSizeChanged(fBrush->getBrushSize()); } -void Document::setOnionDisabled(const bool disabled) { +void Document::setOnionDisabled(const bool disabled) +{ fOnionVisible = !disabled; actionFinished(); } -void Document::setPaintMode(const PaintMode mode) { - if(mode == fPaintMode) return; +void Document::setPaintMode(const PaintMode mode) +{ + if (mode == fPaintMode) { return; } fPaintMode = mode; - if(fBrush) { + if (fBrush) { switch(fPaintMode) { case PaintMode::normal: fBrush->setNormalMode(); break; case PaintMode::erase: fBrush->startEraseMode(); break; @@ -362,10 +417,11 @@ void Document::setPaintMode(const PaintMode mode) { emit paintModeChanged(mode); } -void Document::clear() { +void Document::clear() +{ setPath(""); const int nScenes = fScenes.count(); - for(int i = 0; i < nScenes; i++) removeScene(0); + for (int i = 0; i < nScenes; i++) { removeScene(0); } replaceClipboard(nullptr); const auto iBrushes = fBrushes; for (const auto brush : iBrushes) { @@ -377,12 +433,15 @@ void Document::clear() { removeBookmarkColor(color); } fColors.clear(); + + mGrid->setSettings(eSettings::instance().fGrid); } void Document::SWT_setupAbstraction(SWT_Abstraction * const abstraction, const UpdateFuncs &updateFuncs, - const int visiblePartWidgetId) { - for(const auto& scene : fScenes) { + const int visiblePartWidgetId) +{ + for (const auto& scene : fScenes) { auto abs = scene->SWT_abstractionForWidget(updateFuncs, visiblePartWidgetId); abstraction->addChildAbstraction(abs->ref()); diff --git a/src/core/Private/document.h b/src/core/Private/document.h index 729f88fe2..20a0f1c0e 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 "grid.h" class SceneBoundGradient; class FileDataCacheHandler; @@ -70,6 +71,8 @@ class CORE_EXPORT Document : public SingleWidgetTarget { static Document* sInstance; + Friction::Core::Grid* getGrid(); + stdsptr fClipboardContainer; QString fEvFile; @@ -78,10 +81,21 @@ class CORE_EXPORT Document : public SingleWidgetTarget { bool fLocalPivot = true; - bool fGizmoPositionVisibility = AppSupport::getSettings("gizmos", "Position", true).toBool(); - bool fGizmoRotateVisibility = AppSupport::getSettings("gizmos", "Rotate", true).toBool(); - bool fGizmoScaleVisibility = AppSupport::getSettings("gizmos", "Scale", false).toBool(); - bool fGizmoShearVisibility = AppSupport::getSettings("gizmos", "Shear", false).toBool(); + bool fGizmoPositionVisibility = AppSupport::getSettings("gizmos", + "Position", + true).toBool(); + bool fGizmoRotateVisibility = AppSupport::getSettings("gizmos", + "Rotate", + true).toBool(); + bool fGizmoScaleVisibility = AppSupport::getSettings("gizmos", + "Scale", + false).toBool(); + bool fGizmoShearVisibility = AppSupport::getSettings("gizmos", + "Shear", + false).toBool(); + bool fGizmoAllVisibility = AppSupport::getSettings("gizmos", + "All", + true).toBool(); CanvasMode fCanvasMode; @@ -109,6 +123,8 @@ class CORE_EXPORT Document : public SingleWidgetTarget { bool fOnionVisible = false; PaintMode fPaintMode = PaintMode::normal; + Friction::Core::Grid *mGrid; + QList> fScenes; std::map fVisibleScenes; ConnContextPtr fActiveScene; @@ -200,11 +216,14 @@ class CORE_EXPORT Document : public SingleWidgetTarget { void readBookmarked(eReadStream &src); void readGradients(eReadStream& src); + signals: + void canvasModeSet(CanvasMode); void gizmoVisibilityChanged(const Friction::Core::Gizmos::Interact &ti, const bool &visible); + void gridChanged(const Friction::Core::Grid::Settings &settings); void sceneCreated(Canvas*); void sceneRemoved(Canvas*); diff --git a/src/core/Private/documentrw.cpp b/src/core/Private/documentrw.cpp index 9fa04b075..feaee6f3e 100644 --- a/src/core/Private/documentrw.cpp +++ b/src/core/Private/documentrw.cpp @@ -32,68 +32,75 @@ #include "Paint/brushescontext.h" #include "simpletask.h" #include "canvas.h" -#include "appsupport.h" +#include "grid.h" -void Document::writeBookmarked(eWriteStream &dst) const { +void Document::writeBookmarked(eWriteStream &dst) const +{ dst << fColors.count(); - for(const auto &col : fColors) { - dst << col; - } - + for (const auto &col : fColors) { dst << col; } dst << fBrushes.count(); - for(const auto &brush : fBrushes) { - dst << brush; - } + for(const auto &brush : fBrushes) { dst << brush; } } -void Document::writeScenes(eWriteStream& dst) const { +void Document::writeScenes(eWriteStream& dst) const +{ writeBookmarked(dst); dst.writeCheckpoint(); + mGrid->writeDocument(dst); + dst.writeCheckpoint(); + const int nScenes = fScenes.count(); dst.write(&nScenes, sizeof(int)); - for(const auto &scene : fScenes) { + for (const auto &scene : fScenes) { scene->writeBoundingBox(dst); dst.writeCheckpoint(); } } -void Document::readBookmarked(eReadStream &src) { +void Document::readBookmarked(eReadStream &src) +{ int nCol; src >> nCol; - for(int i = 0; i < nCol; i++) { + for (int i = 0; i < nCol; i++) { QColor col; src >> col; addBookmarkColor(col); } int nBrush; src >> nBrush; - for(int i = 0; i < nBrush; i++) { + for (int i = 0; i < nBrush; i++) { SimpleBrushWrapper* brush; src >> brush; - if(brush) addBookmarkBrush(brush); + if (brush) { addBookmarkBrush(brush); } } } -void Document::readGradients(eReadStream& src) { +void Document::readGradients(eReadStream& src) +{ int nGrads; src >> nGrads; - for(int i = 0; i < nGrads; i++) { + for (int i = 0; i < nGrads; i++) { enve::make_shared(nullptr)->read(src); } } -void Document::readScenes(eReadStream& src) { - if(src.evFileVersion() > 1) { +void Document::readScenes(eReadStream& src) +{ + if (src.evFileVersion() > 1) { readBookmarked(src); src.readCheckpoint("Error reading bookmarks"); } - if(src.evFileVersion() <= 5) { + if (src.evFileVersion() <= 5) { readGradients(src); src.readCheckpoint("Error reading gradients"); } + if (src.evFileVersion() >= EvFormat::grid) { + mGrid->readDocument(src); + src.readCheckpoint("Error reading grid"); + } int nScenes; src.read(&nScenes, sizeof(int)); - for(int i = 0; i < nScenes; i++) { + for (int i = 0; i < nScenes; i++) { Canvas* scene; - if(src.evFileVersion() < EvFormat::betterSWTAbsReadWrite) { + if (src.evFileVersion() < EvFormat::betterSWTAbsReadWrite) { scene = createNewScene(); } else { scene = fScenes.at(fScenes.count() - nScenes + i).get(); @@ -106,12 +113,13 @@ void Document::readScenes(eReadStream& src) { SimpleTask::sProcessAll(); } -void Document::writeDoxumentXEV(QDomDocument& doc) const { +void Document::writeDoxumentXEV(QDomDocument& doc) const +{ auto document = doc.createElement("Document"); document.setAttribute("format-version", XevFormat::version); auto bColors = doc.createElement("ColorBookmarks"); - for(const auto &col : fColors) { + for (const auto &col : fColors) { auto color = doc.createElement("Color"); color.setAttribute("name", col.name()); bColors.appendChild(color); @@ -119,14 +127,16 @@ void Document::writeDoxumentXEV(QDomDocument& doc) const { document.appendChild(bColors); auto bBrushes = doc.createElement("BrushBookmarks"); - for(const auto &b : fBrushes) { + for (const auto &b : fBrushes) { const auto brush = XevExportHelpers::brushToElement(b, doc); bBrushes.appendChild(brush); } document.appendChild(bBrushes); + mGrid->writeXML(doc); + auto scenes = doc.createElement("Scenes"); - for(const auto &s : fScenes) { + for (const auto &s : fScenes) { auto scene = doc.createElement("Scene"); const qreal resolution = s->getResolution(); scene.setAttribute("resolution", QString::number(resolution)); @@ -148,16 +158,18 @@ void Document::writeDoxumentXEV(QDomDocument& doc) const { } void Document::writeScenesXEV(const std::shared_ptr& xevFileSaver, - const RuntimeIdToWriteId& objListIdConv) const { + const RuntimeIdToWriteId& objListIdConv) const +{ int id = 0; - for(const auto &s : fScenes) { + for (const auto &s : fScenes) { const QString path = "scenes/" + QString::number(id++) + "/"; s->writeBoxOrSoundXEV(xevFileSaver, objListIdConv, path); } } void Document::writeXEV(const std::shared_ptr& xevFileSaver, - const RuntimeIdToWriteId& objListIdConv) const { + const RuntimeIdToWriteId& objListIdConv) const +{ auto& fileSaver = xevFileSaver->fileSaver(); fileSaver.processText("document.xml", [&](QTextStream& stream) { QDomDocument document; @@ -168,7 +180,8 @@ void Document::writeXEV(const std::shared_ptr& xevFileSaver, } void Document::readDocumentXEV(ZipFileLoader& fileLoader, - QList& scenes) { + QList& scenes) +{ fileLoader.process("document.xml", [&](QIODevice* const src) { QDomDocument document; document.setContent(src); @@ -177,41 +190,47 @@ void Document::readDocumentXEV(ZipFileLoader& fileLoader, } void Document::readDocumentXEV(const QDomDocument& doc, - QList& scenes) { + QList& scenes) +{ const auto document = doc.firstChildElement("Document"); const QString versionStr = document.attribute("format-version", ""); - if(versionStr.isEmpty()) RuntimeThrow("No format version specified"); + if (versionStr.isEmpty()) { RuntimeThrow("No format version specified"); } // const int version = XmlExportHelpers::stringToInt(versionStr); auto bColors = document.firstChildElement("ColorBookmarks"); const auto colors = bColors.elementsByTagName("Color"); const int nColors = colors.count(); - for(int i = 0; i < nColors; i++) { + for (int i = 0; i < nColors; i++) { const auto color = colors.at(i); - if(!color.isElement()) continue; + if (!color.isElement()) { continue; } const auto colorEle = color.toElement(); const QString name = colorEle.attribute("name"); - if(name.isEmpty()) continue; + if (name.isEmpty()) { continue; } addBookmarkColor(QColor(name)); } auto bBrushes = document.firstChildElement("BrushBookmarks"); const auto brushes = bBrushes.elementsByTagName("Brush"); const int nBrushes = brushes.count(); - for(int i = 0; i < nBrushes; i++) { + for (int i = 0; i < nBrushes; i++) { const auto brush = brushes.at(i); - if(!brush.isElement()) continue; + if (!brush.isElement()) { continue; } const auto brushEle = brush.toElement(); const auto brushPtr = XevExportHelpers::brushFromElement(brushEle); - if(brushPtr) addBookmarkBrush(brushPtr); + if (brushPtr) { addBookmarkBrush(brushPtr); } + } + + const auto gridElement = document.firstChildElement("Grid"); + if (!gridElement.isNull()) { + mGrid->readXML(gridElement); } auto scenesE = document.firstChildElement("Scenes"); const auto sceneEles = scenesE.elementsByTagName("Scene"); const int nScenes = sceneEles.count(); - for(int i = 0; i < nScenes; i++) { + for (int i = 0; i < nScenes; i++) { const auto sceneNode = sceneEles.at(i); - if(!sceneNode.isElement()) continue; + if (!sceneNode.isElement()) { continue; } const auto sceneEle = sceneNode.toElement(); const auto resStr = sceneEle.attribute("resolution"); @@ -223,7 +242,7 @@ void Document::readDocumentXEV(const QDomDocument& doc, const bool clip = sceneEle.attribute("clip") == "true"; const auto rangeStr = sceneEle.attribute("frameRange", "0 200"); const auto rangeStrs = rangeStr.split(' ', Qt::SkipEmptyParts); - if(rangeStrs.count() != 2) RuntimeThrow("Invalid frame range " + rangeStr); + if (rangeStrs.count() != 2) { RuntimeThrow("Invalid frame range " + rangeStr); } const int rangeMin = XmlExportHelpers::stringToInt(rangeStrs[0]); const int rangeMax = XmlExportHelpers::stringToInt(rangeStrs[1]); @@ -243,9 +262,10 @@ void Document::readDocumentXEV(const QDomDocument& doc, void Document::readScenesXEV(XevReadBoxesHandler& boxReadHandler, ZipFileLoader& fileLoader, const QList& scenes, - const RuntimeIdToWriteId& objListIdConv) { + const RuntimeIdToWriteId& objListIdConv) +{ int id = 0; - for(const auto& scene : scenes) { + for (const auto& scene : scenes) { const auto block = scene->blockUndoRedo(); const QString path = "scenes/" + QString::number(id++) + "/"; scene->readBoxOrSoundXEV(boxReadHandler, fileLoader, diff --git a/src/core/Private/esettings.h b/src/core/Private/esettings.h index 7c4d92118..b9de55126 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 "grid.h" #include "themesupport.h" #include "Expressions/expressionpresets.h" @@ -168,6 +169,9 @@ class CORE_EXPORT eSettings : public QObject // expressions presets Friction::Core::ExpressionPresets fExpressions; + // grid settings + Friction::Core::Grid::Settings fGrid = Friction::Core::Grid::loadSettings(); + // last used stroke color QColor fLastUsedStrokeColor = AppSupport::getSettings("FillStroke", "LastStrokeColor", diff --git a/src/core/ReadWrite/evformat.h b/src/core/ReadWrite/evformat.h index 5d45c54ff..c9011d813 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, + grid = 34, nextVersion }; diff --git a/src/core/canvas.cpp b/src/core/canvas.cpp index a2a5ae8cf..f6e587c51 100644 --- a/src/core/canvas.cpp +++ b/src/core/canvas.cpp @@ -101,6 +101,17 @@ 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(); @@ -239,7 +250,8 @@ void drawTransparencyMesh(SkCanvas* const canvas, void Canvas::renderSk(SkCanvas* const canvas, const QRect& drawRect, const QMatrix& viewTrans, - const bool mouseGrabbing) { + const bool mouseGrabbing) +{ mDrawnSinceQue = true; SkPaint paint; paint.setStyle(SkPaint::kFill_Style); @@ -255,13 +267,35 @@ void Canvas::renderSk(SkCanvas* const canvas, eSizesUI::widget*0.25f*invZoom}; const auto dashPathEffect = SkDashPathEffect::Make(intervals, 2, 0); + QTransform worldToScreenTransform = mWorldToScreen; + QTransform screenToWorldTransform = mScreenToWorld; + bool haveWorldTransform = mHasWorldToScreen; + if (!haveWorldTransform) { + bool invertible = false; + worldToScreenTransform = QTransform(viewTrans.m11(), viewTrans.m12(), 0.0, + viewTrans.m21(), viewTrans.m22(), 0.0, + viewTrans.dx(), viewTrans.dy(), 1.0); + screenToWorldTransform = worldToScreenTransform.inverted(&invertible); + haveWorldTransform = invertible; + } + const QRectF worldViewport = haveWorldTransform ? screenToWorldTransform.mapRect(QRectF(drawRect)) : QRectF(QPointF(0.0, 0.0), + QSizeF(drawRect.width(), + drawRect.height())); + QRectF gridViewport = worldViewport.normalized(); + const qreal gridPixelRatio = haveWorldTransform ? mDevicePixelRatio : pixelRatio; + const auto& gridSettings = mDocument.getGrid()->getSettings(); + const bool gridVisible = gridSettings.show && (!haveWorldTransform || !gridViewport.isEmpty()); + const bool gridOnTop = gridSettings.drawOnTop; + const bool drawCanvas = mSceneFrame && mSceneFrame->fBoxState == mStateId; + canvas->concat(skViewTrans); - if(isPreviewingOrRendering()) { - if(mSceneFrame) { + if (isPreviewingOrRendering()) { + if (mSceneFrame) { canvas->clear(SK_ColorBLACK); canvas->save(); - if(bgColor.alpha() != 255) + if (bgColor.alpha() != 255) { drawTransparencyMesh(canvas, canvasRect); + } const float reversedRes = toSkScalar(1/mSceneFrame->fResolution); canvas->scale(reversedRes, reversedRes); mSceneFrame->drawImage(canvas, filter); @@ -269,10 +303,11 @@ void Canvas::renderSk(SkCanvas* const canvas, } return; } + canvas->save(); - if(mClipToCanvasSize) { + + if (mClipToCanvasSize) { canvas->clear(SK_ColorBLACK); - canvas->clipRect(canvasRect); } else { canvas->clear(ThemeSupport::getThemeBaseSkColor()); paint.setColor(SK_ColorGRAY); @@ -280,23 +315,38 @@ 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))) { + if (!mClipToCanvasSize || !drawCanvas) { + if (bgColor.alpha() == 255 && skViewTrans.mapRect(canvasRect).contains(toSkRect(drawRect))) { canvas->clear(toSkColor(bgColor)); } else { - paint.setStyle(SkPaint::kFill_Style); - paint.setColor(toSkColor(bgColor)); - canvas->drawRect(canvasRect, paint); + SkPaint bgPaint; + bgPaint.setStyle(SkPaint::kFill_Style); + bgPaint.setColor(toSkColor(bgColor)); + canvas->drawRect(canvasRect, bgPaint); } + } + if (gridVisible && !gridOnTop) { + mDocument.getGrid()->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) { + } else if (drawCanvas) { canvas->save(); const float reversedRes = toSkScalar(1/mSceneFrame->fResolution); canvas->scale(reversedRes, reversedRes); @@ -305,21 +355,30 @@ void Canvas::renderSk(SkCanvas* const canvas, } canvas->restore(); + canvas->restore(); + + if (gridVisible && gridOnTop) { + mDocument.getGrid()->drawGrid(canvas, + gridViewport, + worldToScreenTransform, + gridPixelRatio); + } if (!enve_cast(mCurrentContainer)) { mCurrentContainer->drawBoundingRect(canvas, invZoom); } + //if(!mPaintTarget.isValid()) { const auto mods = QApplication::queryKeyboardModifiers(); const bool ctrlPressed = mods & Qt::CTRL && mods & Qt::SHIFT; - for(int i = mSelectedBoxes.count() - 1; i >= 0; i--) { + for (int i = mSelectedBoxes.count() - 1; i >= 0; i--) { const auto& iBox = mSelectedBoxes.at(i); canvas->save(); iBox->drawBoundingRect(canvas, invZoom); iBox->drawAllCanvasControls(canvas, mCurrentMode, invZoom, ctrlPressed); canvas->restore(); } - for(const auto obj : mNullObjects) { + for (const auto obj : mNullObjects) { canvas->save(); obj->drawNullObject(canvas, mCurrentMode, invZoom, ctrlPressed); canvas->restore(); @@ -328,17 +387,17 @@ void Canvas::renderSk(SkCanvas* const canvas, renderGizmos(canvas, qInvZoom, invZoom); - if(mCurrentMode == CanvasMode::boxTransform || + if (mCurrentMode == CanvasMode::boxTransform || mCurrentMode == CanvasMode::pointTransform) { - if(mTransMode == TransformMode::rotate || + if (mTransMode == TransformMode::rotate || mTransMode == TransformMode::scale || mTransMode == TransformMode::shear) { mRotPivot->drawTransforming(canvas, mCurrentMode, invZoom, eSizesUI::widget*0.25f*invZoom); - } else if(!mouseGrabbing || mRotPivot->isSelected()) { + } else if (!mouseGrabbing || mRotPivot->isSelected()) { mRotPivot->drawSk(canvas, mCurrentMode, invZoom, false, false); } - } else if(mCurrentMode == CanvasMode::drawPath) { + } else if (mCurrentMode == CanvasMode::drawPath) { const SkScalar nodeSize = 0.15f*eSizesUI::widget*invZoom; SkPaint paint; paint.setStyle(SkPaint::kFill_Style); @@ -351,40 +410,47 @@ void Canvas::renderSk(SkCanvas* const canvas, drawColor.green(), drawColor.blue()); const SkScalar ptSize = 0.25*nodeSize; - for(const auto& pt : pts) { + for (const auto& pt : pts) { canvas->drawCircle(pt.x(), pt.y(), ptSize, paint); } const bool drawFitted = mDocument.fDrawPathManual && mManualDrawPathState == ManualDrawPathState::drawn; - if(drawFitted) { + if (drawFitted) { paint.setARGB(255, 255, 0, 0); const auto& highlightPts = mDrawPath.forceSplits(); - for(const int ptId : highlightPts) { + for (const int ptId : highlightPts) { const auto& pt = pts.at(ptId); canvas->drawCircle(pt.x(), pt.y(), nodeSize, paint); } const auto& fitted = mDrawPath.getFitted(); paint.setARGB(255, 255, 0, 0); - for(const auto& seg : fitted) { + for (const auto& seg : fitted) { const auto path = seg.toSkPath(); - SkiaHelpers::drawOutlineOverlay(canvas, path, invZoom, SK_ColorWHITE); + SkiaHelpers::drawOutlineOverlay(canvas, + path, + invZoom, + SK_ColorWHITE); const auto& p0 = seg.p0(); canvas->drawCircle(p0.x(), p0.y(), nodeSize, paint); } - if(!mDrawPathTmp.isEmpty()) { - SkiaHelpers::drawOutlineOverlay(canvas, mDrawPathTmp, - invZoom, SK_ColorWHITE); + if (!mDrawPathTmp.isEmpty()) { + SkiaHelpers::drawOutlineOverlay(canvas, + mDrawPathTmp, + invZoom, + SK_ColorWHITE); } } paint.setARGB(255, 0, 75, 155); - if(mHoveredPoint_d && mHoveredPoint_d->isSmartNodePoint()) { + + if (mHoveredPoint_d && mHoveredPoint_d->isSmartNodePoint()) { const QPointF pos = mHoveredPoint_d->getAbsolutePos(); const qreal r = 0.5*qInvZoom*mHoveredPoint_d->getRadius(); canvas->drawCircle(pos.x(), pos.y(), r, paint); } - if(mDrawPathFirst) { + + if (mDrawPathFirst) { const QPointF pos = mDrawPathFirst->getAbsolutePos(); const qreal r = 0.5*qInvZoom*mDrawPathFirst->getRadius(); canvas->drawCircle(pos.x(), pos.y(), r, paint); @@ -403,7 +469,7 @@ void Canvas::renderSk(SkCanvas* const canvas, paint.setPathEffect(nullptr); canvas->restore(); } else {*/ - if(mSelecting) { + if (mSelecting) { paint.setStyle(SkPaint::kStroke_Style); paint.setPathEffect(dashPathEffect); paint.setStrokeWidth(2*invZoom); @@ -415,12 +481,12 @@ void Canvas::renderSk(SkCanvas* const canvas, //paint.setPathEffect(nullptr); } - if(mHoveredPoint_d) { + if (mHoveredPoint_d) { mHoveredPoint_d->drawHovered(canvas, invZoom); - } else if(mHoveredNormalSegment.isValid()) { + } else if (mHoveredNormalSegment.isValid()) { mHoveredNormalSegment.drawHoveredSk(canvas, invZoom); - } else if(mHoveredBox) { - if(!mCurrentNormalSegment.isValid()) { + } else if (mHoveredBox) { + if (!mCurrentNormalSegment.isValid()) { mHoveredBox->drawHoveredSk(canvas, invZoom); } } @@ -434,8 +500,9 @@ void Canvas::renderSk(SkCanvas* const canvas, canvas->resetMatrix(); - if(mTransMode != TransformMode::none || mValueInput.inputEnabled()) + if (mTransMode != TransformMode::none || mValueInput.inputEnabled()) { mValueInput.draw(canvas, drawRect.height() - eSizesUI::widget); + } } void Canvas::setCanvasSize(const int width, diff --git a/src/core/canvas.h b/src/core/canvas.h index 0a61c9bb2..6bdeb5c6c 100644 --- a/src/core/canvas.h +++ b/src/core/canvas.h @@ -46,6 +46,8 @@ #include #include #include +#include +#include #include "gizmos.h" @@ -187,6 +189,8 @@ 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(); @@ -811,6 +815,19 @@ 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 collectAnchorOffsets(const Friction::Core::Grid::Settings &settings); + const QPair moveBySnapTargets(const Qt::KeyboardModifiers &modifiers, + const QPointF &moveBy, + const Friction::Core::Grid::Settings &settings, + const bool &includeSelectedBounds = false, + const bool &useAnchorOffsets = true, + const bool &mustHaveSelected = true); + void drawPathClear(); void drawPathFinish(const qreal invScale); @@ -827,6 +844,16 @@ class CORE_EXPORT Canvas : public CanvasBase protected: Document& mDocument; + + QTransform mWorldToScreen; + QTransform mScreenToWorld; + bool mHasWorldToScreen = false; + qreal mDevicePixelRatio = 1.0; + QPointF mGridMoveStartPivot; + std::vector mGridSnapAnchorOffsets; + bool mHasCreationPressPos = false; + QPointF mCreationPressPos; + bool mDrawnSinceQue = true; qsptr mUndoRedoStack; @@ -935,6 +962,14 @@ class CORE_EXPORT Canvas : public CanvasBase QPointF getMoveByValueForEvent(const eMouseEvent &e); void cancelCurrentTransform(); void cancelCurrentTransformGimzos(); + + void collectSnapTargets(bool includePivots, + bool includeBounds, + bool includeNodes, + std::vector& pivotTargets, + std::vector& boxTargets, + std::vector& nodeTargets, + bool includeSelectedBounds = false) const; }; #endif // CANVAS_H diff --git a/src/core/canvasgizmos.cpp b/src/core/canvasgizmos.cpp index c986d9114..76015b618 100644 --- a/src/core/canvasgizmos.cpp +++ b/src/core/canvasgizmos.cpp @@ -32,6 +32,7 @@ void Canvas::renderGizmos(SkCanvas * const canvas, const qreal qInvZoom, const float invZoom) { + if (!mGizmos.fState.visible) { return; } updateRotateHandleGeometry(qInvZoom); auto drawAxisLine = [&](const Gizmos::LineGeometry &geom, const QColor &baseColor, @@ -399,6 +400,10 @@ void Canvas::setGizmoVisibility(const Gizmos::Interact &ti, setShearGizmoHover(Gizmos::ShearHandle::Y, false); } break; + case Gizmos::Interact::All: + if (mGizmos.fState.visible == visibility) { return; } + mGizmos.fState.visible = visibility; + break; default: return; } @@ -411,16 +416,14 @@ bool Canvas::getGizmoVisibility(const Gizmos::Interact &ti) switch (ti) { case Gizmos::Interact::Position: return mGizmos.fState.showPosition; - break; case Gizmos::Interact::Rotate: return mGizmos.fState.showRotate; - break; case Gizmos::Interact::Scale: return mGizmos.fState.showScale; - break; case Gizmos::Interact::Shear: return mGizmos.fState.showShear; - break; + case Gizmos::Interact::All: + return mGizmos.fState.visible; default:; } return false; diff --git a/src/core/canvasgrid.cpp b/src/core/canvasgrid.cpp new file mode 100644 index 000000000..8e9c92f63 --- /dev/null +++ b/src/core/canvasgrid.cpp @@ -0,0 +1,304 @@ +/* +# +# 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, version 3. +# +# 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 "canvas.h" + +#include "Private/document.h" +#include "eevent.h" +#include "MovablePoints/smartnodepoint.h" + +QPointF Canvas::snapPosToGrid(const QPointF& pos, + Qt::KeyboardModifiers modifiers, + bool forceSnap) const +{ + if (!mHasWorldToScreen) { return pos; } + + const auto& settings = mDocument.getGrid()->getSettings(); + + if (!settings.snapEnabled) { return pos; } + + const bool bypassSnap = modifiers & Qt::ShiftModifier; + if (bypassSnap) { return pos; } + + const bool gridEnabled = settings.snapToGrid && settings.show; + const bool canvasSnapEnabled = settings.snapToCanvas; + const bool pivotsSnapEnabled = settings.snapToPivots; + const bool boxesSnapEnabled = settings.snapToBoxes; + const bool nodesSnapEnabled = settings.snapToNodes; + + std::vector pivotTargets; + std::vector boxTargets; + std::vector nodeTargets; + if (pivotsSnapEnabled || boxesSnapEnabled || nodesSnapEnabled) { + collectSnapTargets(pivotsSnapEnabled, + boxesSnapEnabled, + nodesSnapEnabled, + pivotTargets, + boxTargets, + nodeTargets); + } + const bool hasPivotTargets = pivotsSnapEnabled && !pivotTargets.empty(); + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + + const bool hasSnapSource = gridEnabled || canvasSnapEnabled || + hasPivotTargets || hasBoxTargets || hasNodeTargets; + const bool shouldForce = (forceSnap && hasSnapSource) || + (modifiers & Qt::ControlModifier); + + if (!hasSnapSource && !shouldForce) { return pos; } + + QRectF canvasRect; + const QRectF* canvasRectPtr = nullptr; + if (canvasSnapEnabled) { + canvasRect = QRectF(QPointF(0.0, 0.0), + QSizeF(mWidth, mHeight)); + canvasRectPtr = &canvasRect; + } + + return mDocument.getGrid()->maybeSnapPivot(pos, + mWorldToScreen, + shouldForce, + false, + canvasRectPtr, + nullptr, + hasPivotTargets ? &pivotTargets : nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); +} + +QPointF Canvas::snapEventPos(const eMouseEvent& e, + bool forceSnap) const +{ + return snapPosToGrid(e.fPos, + e.fModifiers, + forceSnap); +} + +void Canvas::collectAnchorOffsets(const Friction::Core::Grid::Settings &settings) +{ + mGridMoveStartPivot = getSelectedBoxesAbsPivotPos(); + + mGridSnapAnchorOffsets.clear(); + if (settings.snapAnchorPivot) { + mGridSnapAnchorOffsets.emplace_back(QPointF(0.0, 0.0)); + } + + QRectF combinedRect; + bool hasRect = false; + for (const auto& box : mSelectedBoxes) { + const QRectF rect = box->getAbsBoundingRect(); + if (rect.width() < 0.0 || rect.height() < 0.0) { continue; } + if (!hasRect) { + combinedRect = rect; + hasRect = true; + } else { + combinedRect = combinedRect.united(rect); + } + } + + if (hasRect && settings.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 (settings.snapAnchorNodes) { + const MovablePoint::PtOp gatherOffsets = [&](MovablePoint* point) { + if (!point || !point->isSmartNodePoint()) { return; } + const auto* node = static_cast(point); + if (node) { + mGridSnapAnchorOffsets.emplace_back(node->getAbsolutePos() - mGridMoveStartPivot); + } + }; + for (const auto& box : mSelectedBoxes) { + if (!box) { continue; } + box->selectAllCanvasPts(gatherOffsets, CanvasMode::pointTransform); + } + } +} + +const QPair Canvas::moveBySnapTargets(const Qt::KeyboardModifiers &modifiers, + const QPointF &moveBy, + const Friction::Core::Grid::Settings &settings, + const bool &includeSelectedBounds, + const bool &useAnchorOffsets, + const bool &mustHaveSelected) +{ + QPair result; + result.first = false; + + const bool snappingActive = settings.snapEnabled; + if (!snappingActive) { return result; } + + const bool bypassSnap = modifiers & Qt::ShiftModifier; + const bool forceSnap = modifiers & Qt::ControlModifier; + const bool hasAnchorOffsets = useAnchorOffsets && !mGridSnapAnchorOffsets.empty(); + const bool pivotsSnapEnabled = snappingActive && settings.snapToPivots; + const bool boxesSnapEnabled = snappingActive && settings.snapToBoxes; + const bool nodesSnapEnabled = snappingActive && settings.snapToNodes; + + std::vector pivotTargets; + std::vector boxTargets; + std::vector nodeTargets; + + if (pivotsSnapEnabled || boxesSnapEnabled || nodesSnapEnabled) { + collectSnapTargets(pivotsSnapEnabled, + boxesSnapEnabled, + nodesSnapEnabled, + pivotTargets, + boxTargets, + nodeTargets, + includeSelectedBounds); + } + const bool hasPivotTargets = pivotsSnapEnabled && !pivotTargets.empty(); + const bool hasBoxTargets = boxesSnapEnabled && !boxTargets.empty(); + const bool hasNodeTargets = nodesSnapEnabled && !nodeTargets.empty(); + const bool snapSourcesAvailable = snappingActive && (settings.snapToGrid || + settings.snapToCanvas || + hasPivotTargets || + hasBoxTargets || + hasNodeTargets || + hasAnchorOffsets); + + if (mustHaveSelected && mSelectedBoxes.isEmpty()) { return result; } + + if (mHasWorldToScreen && (snapSourcesAvailable || forceSnap)) { + const QPointF targetPivot = mGridMoveStartPivot + moveBy; + QRectF canvasRect; + const QRectF* canvasPtr = nullptr; + if (settings.snapToCanvas) { + canvasRect = QRectF(QPointF(0.0, 0.0), QSizeF(mWidth, mHeight)); + canvasPtr = &canvasRect; + } + const auto snapped = mDocument.getGrid()->maybeSnapPivot(targetPivot, + mWorldToScreen, + forceSnap, + bypassSnap, + canvasPtr, + useAnchorOffsets ? &mGridSnapAnchorOffsets : nullptr, + hasPivotTargets ? &pivotTargets : nullptr, + hasBoxTargets ? &boxTargets : nullptr, + hasNodeTargets ? &nodeTargets : nullptr); + if (snapped != targetPivot) { result = {true, snapped - mGridMoveStartPivot}; } + } + + return result; +} + +void Canvas::collectSnapTargets(bool includePivots, + bool includeBounds, + bool includeNodes, + std::vector& pivotTargets, + std::vector& boxTargets, + std::vector& nodeTargets, + bool includeSelectedBounds) const +{ + pivotTargets.clear(); + boxTargets.clear(); + nodeTargets.clear(); + + if ((!includePivots && !includeBounds && !includeNodes) || !mCurrentContainer) { + return; + } + + auto addIfValid = [](std::vector& target, const QPointF& pt) { + if (isPointFinite(pt)) { target.push_back(pt); } + }; + + auto appendBoundsTargets = [&](const QRectF& rect) { + const QRectF normalized = rect.normalized(); + if (normalized.isNull() || !normalized.isValid()) { return; } + + const QPointF topLeft = normalized.topLeft(); + const QPointF topRight = normalized.topRight(); + const QPointF bottomLeft = normalized.bottomLeft(); + const QPointF bottomRight = normalized.bottomRight(); + const QPointF topCenter((normalized.left() + normalized.right()) * 0.5, normalized.top()); + const QPointF bottomCenter((normalized.left() + normalized.right()) * 0.5, normalized.bottom()); + const QPointF leftCenter(normalized.left(), (normalized.top() + normalized.bottom()) * 0.5); + const QPointF rightCenter(normalized.right(), (normalized.top() + normalized.bottom()) * 0.5); + + addIfValid(boxTargets, normalized.center()); + addIfValid(boxTargets, topLeft); + addIfValid(boxTargets, topRight); + addIfValid(boxTargets, bottomLeft); + addIfValid(boxTargets, bottomRight); + addIfValid(boxTargets, topCenter); + addIfValid(boxTargets, bottomCenter); + addIfValid(boxTargets, leftCenter); + addIfValid(boxTargets, rightCenter); + }; + + const std::function recurse = [&](const ContainerBox* container, + bool ancestorSelected) { + if (!container) { return; } + const auto& boxes = container->getContainedBoxes(); + for (auto* box : boxes) { + if (!box) { continue; } + const bool selectedBranch = ancestorSelected || box->isSelected(); + const bool visible = box->isVisible(); + + if (!selectedBranch && visible) { + if (includePivots) { addIfValid(pivotTargets, box->getPivotAbsPos()); } + if (includeBounds) { appendBoundsTargets(box->getAbsBoundingRect()); } + if (includeNodes) { + const MovablePoint::PtOp gather = [&](MovablePoint* point) { + if (!point || !point->isSmartNodePoint()) { return; } + const auto* node = static_cast(point); + if (node) { addIfValid(nodeTargets, node->getAbsolutePos()); } + }; + box->selectAllCanvasPts(gather, CanvasMode::pointTransform); + } + } + + if (const auto* childContainer = enve_cast(box)) { + recurse(childContainer, selectedBranch); + } + } + }; + + recurse(mCurrentContainer, false); + + if (includeBounds && includeSelectedBounds) { + for (const auto& selectedBox : mSelectedBoxes) { + if (!selectedBox || !selectedBox->isVisible()) { continue; } + appendBoundsTargets(selectedBox->getAbsBoundingRect()); + } + } +} diff --git a/src/core/canvashandlesmartpath.cpp b/src/core/canvashandlesmartpath.cpp index edc15603f..cee745bb1 100644 --- a/src/core/canvashandlesmartpath.cpp +++ b/src/core/canvashandlesmartpath.cpp @@ -27,6 +27,7 @@ #include "MovablePoints/smartnodepoint.h" #include "Boxes/smartvectorpath.h" #include "eevent.h" +#include "Private/document.h" void Canvas::clearCurrentSmartEndPoint() { setCurrentSmartEndPoint(nullptr); @@ -59,14 +60,16 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { mCurrentContainer->addContained(newPath); clearBoxesSelection(); addBoxToSelection(newPath.get()); - const auto relPos = newPath->mapAbsPosToRel(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + const auto relPos = newPath->mapAbsPosToRel(snappedPos); newPath->getBoxTransformAnimator()->setPosition(relPos.x(), relPos.y()); const auto newHandler = newPath->getPathAnimator(); const auto node = newHandler->createNewSubPathAtRelPos({0, 0}); setCurrentSmartEndPoint(node); } else { if(!nodePointUnderMouse) { - const auto newPoint = mLastEndPoint->actionAddPointAbsPos(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + const auto newPoint = mLastEndPoint->actionAddPointAbsPos(snappedPos); //newPoint->startTransform(); setCurrentSmartEndPoint(newPoint); } else if(!mLastEndPoint) { @@ -90,11 +93,12 @@ void Canvas::handleAddSmartPointMousePress(const eMouseEvent &e) { void Canvas::handleAddSmartPointMouseMove(const eMouseEvent &e) { if(!mLastEndPoint) return; if(mStartTransform) mLastEndPoint->startTransform(); + const QPointF snappedPos = snapEventPos(e, false); if(mLastEndPoint->hasNextNormalPoint() && mLastEndPoint->hasPrevNormalPoint()) { mLastEndPoint->setCtrlsMode(CtrlsMode::corner); mLastEndPoint->setC0Enabled(true); - mLastEndPoint->moveC0ToAbsPos(e.fPos); + mLastEndPoint->moveC0ToAbsPos(snappedPos); } else { if(!mLastEndPoint->hasNextNormalPoint() && !mLastEndPoint->hasPrevNormalPoint()) { @@ -104,9 +108,9 @@ void Canvas::handleAddSmartPointMouseMove(const eMouseEvent &e) { mLastEndPoint->setCtrlsMode(CtrlsMode::symmetric); } if(mLastEndPoint->hasNextNormalPoint()) { - mLastEndPoint->moveC0ToAbsPos(e.fPos); + mLastEndPoint->moveC0ToAbsPos(snappedPos); } else { - mLastEndPoint->moveC2ToAbsPos(e.fPos); + mLastEndPoint->moveC2ToAbsPos(snappedPos); } } } diff --git a/src/core/canvasmouseevents.cpp b/src/core/canvasmouseevents.cpp index c7e3bc9ee..fe7b58c7f 100644 --- a/src/core/canvasmouseevents.cpp +++ b/src/core/canvasmouseevents.cpp @@ -108,8 +108,9 @@ void Canvas::mouseMoveEvent(const eMouseEvent &e) mPaintTarget.cropMove(e.fPos); } return; - } else*/ if(leftPressed || e.fMouseGrabbing) { - if(mMovesToSkip > 0) { + } else*/ + if (leftPressed || e.fMouseGrabbing) { + if (mMovesToSkip > 0) { mMovesToSkip--; return; } @@ -119,52 +120,62 @@ void Canvas::mouseMoveEvent(const eMouseEvent &e) !mGizmos.fState.axisHandleActive && !mGizmos.fState.scaleHandleActive && !mGizmos.fState.shearHandleActive) { - if((mCurrentMode == CanvasMode::pointTransform && + if ((mCurrentMode == CanvasMode::pointTransform && !mPressedPoint && !mCurrentNormalSegment.isValid()) || (mCurrentMode == CanvasMode::boxTransform && !mPressedBox && !mPressedPoint)) { startSelectionAtPoint(e.fPos); } } - if(mSelecting) { + if (mSelecting) { moveSecondSelectionPoint(e.fPos); - } else if(mCurrentMode == CanvasMode::pointTransform) { + } else if (mCurrentMode == CanvasMode::pointTransform) { handleMovePointMouseMove(e); - } else if(mCurrentMode == CanvasMode::boxTransform) { - if(mPressedPoint) { + } else if (mCurrentMode == CanvasMode::boxTransform) { + if (mPressedPoint) { handleMovePointMouseMove(e); } else { handleMovePathMouseMove(e); } - } else if(mCurrentMode == CanvasMode::drawPath) { + } else if (mCurrentMode == CanvasMode::drawPath) { const bool manual = mDocument.fDrawPathManual; const bool drawing = mManualDrawPathState == ManualDrawPathState::none; - if(!manual || drawing) mDrawPath.lineTo(e.fPos); + if (!manual || drawing) { mDrawPath.lineTo(e.fPos); } mDrawPath.smooth(mDocument.fDrawPathSmooth); updateHoveredPoint(e); - } else if(mCurrentMode == CanvasMode::pathCreate) { + } else if (mCurrentMode == CanvasMode::pathCreate) { handleAddSmartPointMouseMove(e); - } else if(mCurrentMode == CanvasMode::circleCreate) { - if(e.shiftMod()) { - const qreal lenR = pointToLen(e.fPos - e.fLastPressPos); + } else if (mCurrentMode == CanvasMode::circleCreate) { + const QPointF anchor = mHasCreationPressPos ? mCreationPressPos : snapPosToGrid(e.fLastPressPos, + e.fModifiers, + false); + const QPointF current = snapEventPos(e, false); + const QPointF delta = current - anchor; + if (e.shiftMod()) { + const qreal lenR = pointToLen(delta); mCurrentCircle->moveRadiusesByAbs({lenR, lenR}); } else { - mCurrentCircle->moveRadiusesByAbs(e.fPos - e.fLastPressPos); + mCurrentCircle->moveRadiusesByAbs(delta); } - } else if(mCurrentMode == CanvasMode::rectCreate) { - if(e.shiftMod()) { - const QPointF trans = e.fPos - e.fLastPressPos; + } else if (mCurrentMode == CanvasMode::rectCreate) { + const QPointF anchor = mHasCreationPressPos ? mCreationPressPos : snapPosToGrid(e.fLastPressPos, + e.fModifiers, + false); + const QPointF current = snapEventPos(e, false); + const QPointF trans = current - anchor; + if (e.shiftMod()) { const qreal valF = qMax(trans.x(), trans.y()); mCurrentRectangle->moveSizePointByAbs({valF, valF}); } else { - mCurrentRectangle->moveSizePointByAbs(e.fPos - e.fLastPressPos); + mCurrentRectangle->moveSizePointByAbs(trans); } } } mStartTransform = false; - if(!mSelecting && !e.fMouseGrabbing && leftPressed) + if (!mSelecting && !e.fMouseGrabbing && leftPressed) { e.fGrabMouse(); + } } void Canvas::mouseReleaseEvent(const eMouseEvent &e) @@ -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 205219c22..4f5d89e84 100644 --- a/src/core/canvasmouseinteractions.cpp +++ b/src/core/canvasmouseinteractions.cpp @@ -30,12 +30,15 @@ #include "Private/document.h" #include "GUI/dialogsinterface.h" +#include "Boxes/boundingbox.h" #include "Boxes/circle.h" #include "Boxes/rectangle.h" #include "Boxes/imagebox.h" #include "Boxes/textbox.h" #include "Boxes/internallinkbox.h" #include "Boxes/containerbox.h" +#include "MovablePoints/smartctrlpoint.h" +#include "MovablePoints/pathpointshandler.h" #include "Boxes/smartvectorpath.h" //#include "Boxes/paintbox.h" #include "Boxes/nullobject.h" @@ -60,10 +63,11 @@ using namespace Friction::Core; -void Canvas::handleMovePathMousePressEvent(const eMouseEvent& e) { +void Canvas::handleMovePathMousePressEvent(const eMouseEvent& e) +{ mPressedBox = mCurrentContainer->getBoxAt(e.fPos); - if(e.shiftMod()) return; - if(mPressedBox ? !mPressedBox->isSelected() : true) { + if (e.shiftMod()) { return; } + if (mPressedBox ? !mPressedBox->isSelected() : true) { clearBoxesSelection(); } } @@ -123,40 +127,35 @@ void Canvas::addActionsToMenu(QMenu *const menu) }); } -void Canvas::handleRightButtonMouseRelease(const eMouseEvent& e) { - if(e.fMouseGrabbing) { +void Canvas::handleRightButtonMouseRelease(const eMouseEvent& e) +{ + if (e.fMouseGrabbing) { cancelCurrentTransform(); e.fReleaseMouse(); mValueInput.clearAndDisableInput(); } else { mPressedBox = mHoveredBox; mPressedPoint = mHoveredPoint_d; - if(mPressedPoint) { + if (mPressedPoint) { QMenu qMenu; PointTypeMenu menu(&qMenu, this, e.fWidget); - if(mPressedPoint->selectionEnabled()) { - if(!mPressedPoint->isSelected()) { - if(!e.shiftMod()) clearPointsSelection(); + if (mPressedPoint->selectionEnabled()) { + if (!mPressedPoint->isSelected()) { + if (!e.shiftMod()) { clearPointsSelection(); } addPointToSelection(mPressedPoint); } - for(const auto& pt : mSelectedPoints_d) { - pt->canvasContextMenu(&menu); - } - } else { - mPressedPoint->canvasContextMenu(&menu); - } + for (const auto& pt : mSelectedPoints_d) { pt->canvasContextMenu(&menu); } + } else { mPressedPoint->canvasContextMenu(&menu); } qMenu.exec(e.fGlobalPos); - } else if(mPressedBox) { - if(!mPressedBox->isSelected()) { - if(!e.shiftMod()) clearBoxesSelection(); + } else if (mPressedBox) { + if (!mPressedBox->isSelected()) { + if (!e.shiftMod()) { clearBoxesSelection(); } addBoxToSelection(mPressedBox); } QMenu qMenu(e.fWidget); PropertyMenu menu(&qMenu, this, e.fWidget); - for(const auto& box : mSelectedBoxes) { - box->setupCanvasMenu(&menu); - } + for (const auto& box : mSelectedBoxes) { box->setupCanvasMenu(&menu); } qMenu.exec(e.fGlobalPos); } else { clearPointsSelection(); @@ -169,13 +168,15 @@ void Canvas::handleRightButtonMouseRelease(const eMouseEvent& e) { mDocument.actionFinished(); } -void Canvas::clearHoveredEdge() { +void Canvas::clearHoveredEdge() +{ mHoveredNormalSegment.reset(); } -void Canvas::handleMovePointMousePressEvent(const eMouseEvent& e) { - if(mHoveredNormalSegment.isValid()) { - if(e.ctrlMod()) { +void Canvas::handleMovePointMousePressEvent(const eMouseEvent& e) +{ + if (mHoveredNormalSegment.isValid()) { + if (e.ctrlMod()) { clearPointsSelection(); mPressedPoint = mHoveredNormalSegment.divideAtAbsPos(e.fPos); } else { @@ -186,20 +187,21 @@ void Canvas::handleMovePointMousePressEvent(const eMouseEvent& e) { clearLastPressedPoint(); } clearHovered(); - } else if(mPressedPoint) { - if(mPressedPoint->isSelected()) return; - if(!e.shiftMod() && mPressedPoint->selectionEnabled()) { + } else if (mPressedPoint) { + if (mPressedPoint->isSelected()) { return; } + if (!e.shiftMod() && mPressedPoint->selectionEnabled()) { clearPointsSelection(); } - if(!mPressedPoint->selectionEnabled()) { + if (!mPressedPoint->selectionEnabled()) { addPointToSelection(mPressedPoint); } } } -void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { - if(e.fMouseGrabbing) { +void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) +{ + if (e.fMouseGrabbing) { //handleMouseRelease(event->pos()); //releaseMouseAndDontTrack(); return; @@ -208,6 +210,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; @@ -231,37 +234,34 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { mPressedPoint = getPointAtAbsPos(e.fPos, mCurrentMode, invScale); - if(mRotPivot->isPointAtAbsPos(e.fPos, mCurrentMode, invScale)) { + if (mRotPivot->isPointAtAbsPos(e.fPos, mCurrentMode, invScale)) { return mRotPivot->setSelected(true); } - if(mCurrentMode == CanvasMode::boxTransform) { - if(mHoveredPoint_d) { - handleMovePointMousePressEvent(e); - } else { - handleMovePathMousePressEvent(e); - } - } else if(mCurrentMode == CanvasMode::pathCreate) { + if (mCurrentMode == CanvasMode::boxTransform) { + if (mHoveredPoint_d) { handleMovePointMousePressEvent(e); } + else { handleMovePathMousePressEvent(e); } + } else if (mCurrentMode == CanvasMode::pathCreate) { handleAddSmartPointMousePress(e); - } else if(mCurrentMode == CanvasMode::pointTransform) { + } else if (mCurrentMode == CanvasMode::pointTransform) { handleMovePointMousePressEvent(e); - } else if(mCurrentMode == CanvasMode::drawPath) { + } else if (mCurrentMode == CanvasMode::drawPath) { const bool manual = mDocument.fDrawPathManual; bool start; - if(manual) { + if (manual) { start = mManualDrawPathState == ManualDrawPathState::none; - if(mManualDrawPathState == ManualDrawPathState::drawn) { + if (mManualDrawPathState == ManualDrawPathState::drawn) { qreal dist; const int forceSplit = mDrawPath.nearestForceSplit(e.fPos, &dist); const int maxDist = 10; - if(dist < maxDist) mDrawPath.removeForceSplit(forceSplit); + if (dist < maxDist) { mDrawPath.removeForceSplit(forceSplit); } else { const int smoothPt = mDrawPath.nearestSmoothPt(e.fPos, &dist); - if(dist < maxDist) mDrawPath.addForceSplit(smoothPt); + if (dist < maxDist) { mDrawPath.addForceSplit(smoothPt); } } mDrawPath.fit(DBL_MAX/5, false); } - } else start = true; - if(start) { + } else { start = true; } + if (start) { mDrawPathFirst = getPointAtAbsPos(e.fPos, mCurrentMode, invScale); mDrawPathFit = 0; drawPathClear(); @@ -270,32 +270,35 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { } else if (mCurrentMode == CanvasMode::pickFillStroke || mCurrentMode == CanvasMode::pickFillStrokeEvent) { //mPressedBox = getBoxAtFromAllDescendents(e.fPos); - } else if(mCurrentMode == CanvasMode::circleCreate) { + } else if (mCurrentMode == CanvasMode::circleCreate) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - newPath->setAbsolutePos(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + newPath->setAbsolutePos(snappedPos); clearBoxesSelection(); addBoxToSelection(newPath.get()); - mCurrentCircle = newPath.get(); - - } else if(mCurrentMode == CanvasMode::nullCreate) { + mCreationPressPos = snappedPos; + mHasCreationPressPos = true; + } else if (mCurrentMode == CanvasMode::nullCreate) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); newPath->setAbsolutePos(e.fPos); clearBoxesSelection(); addBoxToSelection(newPath.get()); - } else if(mCurrentMode == CanvasMode::rectCreate) { + } else if (mCurrentMode == CanvasMode::rectCreate) { const auto newPath = enve::make_shared(); newPath->planCenterPivotPosition(); mCurrentContainer->addContained(newPath); - newPath->setAbsolutePos(e.fPos); + const QPointF snappedPos = snapEventPos(e, false); + newPath->setAbsolutePos(snappedPos); clearBoxesSelection(); addBoxToSelection(newPath.get()); - mCurrentRectangle = newPath.get(); + mCreationPressPos = snappedPos; + mHasCreationPressPos = true; } else if (mCurrentMode == CanvasMode::textCreate) { if (enve_cast(mHoveredBox)) { setCurrentBox(mHoveredBox); @@ -308,9 +311,7 @@ void Canvas::handleLeftButtonMousePress(const eMouseEvent& e) { newPath->setFontSize(mDocument.fFontSize); mCurrentContainer->addContained(newPath); newPath->setAbsolutePos(e.fPos); - mCurrentTextBox = newPath.get(); - clearBoxesSelection(); addBoxToSelection(newPath.get()); } @@ -321,20 +322,15 @@ void Canvas::cancelCurrentTransform() { mGizmos.fState.rotatingFromHandle = false; - if(mCurrentMode == CanvasMode::pointTransform) { - if(mCurrentNormalSegment.isValid()) { + if (mCurrentMode == CanvasMode::pointTransform) { + if (mCurrentNormalSegment.isValid()) { mCurrentNormalSegment.cancelPassThroughTransform(); - } else { - cancelSelectedPointsTransform(); - } - } else if(mCurrentMode == CanvasMode::boxTransform) { - if(mRotPivot->isSelected()) { - mRotPivot->cancelTransform(); - } else { - cancelSelectedBoxesTransform(); - } - } else if(mCurrentMode == CanvasMode::pathCreate) { - + } else { cancelSelectedPointsTransform(); } + } else if (mCurrentMode == CanvasMode::boxTransform) { + if (mRotPivot->isSelected()) { mRotPivot->cancelTransform(); } + else { cancelSelectedBoxesTransform(); } + } else if (mCurrentMode == CanvasMode::pathCreate) { + // } else if (mCurrentMode == CanvasMode::pickFillStroke || mCurrentMode == CanvasMode::pickFillStrokeEvent) { //mCanvasWindow->setCanvasMode(MOVE_PATH); @@ -344,40 +340,33 @@ void Canvas::cancelCurrentTransform() cancelCurrentTransformGimzos(); } -void Canvas::handleMovePointMouseRelease(const eMouseEvent &e) { - if(mRotPivot->isSelected()) { +void Canvas::handleMovePointMouseRelease(const eMouseEvent &e) +{ + if (mRotPivot->isSelected()) { mRotPivot->setSelected(false); - } else if(mTransMode == TransformMode::rotate || - mTransMode == TransformMode::scale || - mTransMode == TransformMode::shear) { + } else if (mTransMode == TransformMode::rotate || + mTransMode == TransformMode::scale || + mTransMode == TransformMode::shear) { finishSelectedPointsTransform(); mTransMode = TransformMode::none; - } else if(mSelecting) { + } else if (mSelecting) { mSelecting = false; - if(!e.shiftMod()) clearPointsSelection(); + if (!e.shiftMod()) { clearPointsSelection(); } moveSecondSelectionPoint(e.fPos); selectAndAddContainedPointsToSelection(mSelectionRect); - } else if(mStartTransform) { - if(mPressedPoint) { - if(mPressedPoint->isCtrlPoint()) { - removePointFromSelection(mPressedPoint); - } else if(e.shiftMod()) { - if(mPressedPoint->isSelected()) { - removePointFromSelection(mPressedPoint); - } else { - addPointToSelection(mPressedPoint); - } - } else { - selectOnlyLastPressedPoint(); - } + } else if (mStartTransform) { + if (mPressedPoint) { + if (mPressedPoint->isCtrlPoint()) { removePointFromSelection(mPressedPoint); } + else if (e.shiftMod()) { + if (mPressedPoint->isSelected()) { removePointFromSelection(mPressedPoint); } + else { addPointToSelection(mPressedPoint); } + } else { selectOnlyLastPressedPoint(); } } else { mPressedBox = mCurrentContainer->getBoxAt(e.fPos); - if(mPressedBox ? !!enve_cast(mPressedBox) : true) { + if (mPressedBox ? !!enve_cast(mPressedBox) : true) { const auto pressedBox = getBoxAtFromAllDescendents(e.fPos); - if(!pressedBox) { - if(!e.shiftMod()) { - clearPointsSelectionOrDeselect(); - } + if (!pressedBox) { + if (!e.shiftMod()) { clearPointsSelectionOrDeselect(); } } else { clearPointsSelection(); clearCurrentSmartEndPoint(); @@ -387,13 +376,10 @@ void Canvas::handleMovePointMouseRelease(const eMouseEvent &e) { mPressedBox = pressedBox; } } - if(mPressedBox) { - if(e.shiftMod()) { - if(mPressedBox->isSelected()) { - removeBoxFromSelection(mPressedBox); - } else { - addBoxToSelection(mPressedBox); - } + if (mPressedBox) { + if (e.shiftMod()) { + if (mPressedBox->isSelected()) { removeBoxFromSelection(mPressedBox); } + else { addBoxToSelection(mPressedBox); } } else { clearPointsSelection(); clearCurrentSmartEndPoint(); @@ -404,39 +390,33 @@ void Canvas::handleMovePointMouseRelease(const eMouseEvent &e) { } } else { finishSelectedPointsTransform(); - if(mPressedPoint) { - if(!mPressedPoint->selectionEnabled()) { - removePointFromSelection(mPressedPoint); - } + if (mPressedPoint) { + if (!mPressedPoint->selectionEnabled()) { removePointFromSelection(mPressedPoint); } } } } -void Canvas::handleMovePathMouseRelease(const eMouseEvent &e) { - if(mRotPivot->isSelected()) { - if(!mStartTransform) mRotPivot->finishTransform(); +void Canvas::handleMovePathMouseRelease(const eMouseEvent &e) +{ + if (mRotPivot->isSelected()) { + if (!mStartTransform) { mRotPivot->finishTransform(); } mRotPivot->setSelected(false); - } else if(mTransMode == TransformMode::rotate) { + } else if (mTransMode == TransformMode::rotate) { pushUndoRedoName("Rotate Objects"); finishSelectedBoxesTransform(); - } else if(mTransMode == TransformMode::scale) { + } else if (mTransMode == TransformMode::scale) { pushUndoRedoName("Scale Objects"); finishSelectedBoxesTransform(); - } else if(mTransMode == TransformMode::shear) { + } else if (mTransMode == TransformMode::shear) { pushUndoRedoName("Shear Objects"); finishSelectedBoxesTransform(); - } else if(mStartTransform) { + } else if (mStartTransform) { mSelecting = false; - if(e.shiftMod() && mPressedBox) { - if(mPressedBox->isSelected()) { - removeBoxFromSelection(mPressedBox); - } else { - addBoxToSelection(mPressedBox); - } - } else { - selectOnlyLastPressedBox(); - } - } else if(mSelecting) { + if (e.shiftMod() && mPressedBox) { + if (mPressedBox->isSelected()) { removeBoxFromSelection(mPressedBox); } + else { addBoxToSelection(mPressedBox); } + } else { selectOnlyLastPressedBox(); } + } else if (mSelecting) { moveSecondSelectionPoint(e.fPos); mCurrentContainer->addContainedBoxesToSelection(mSelectionRect); mSelecting = false; @@ -447,8 +427,9 @@ void Canvas::handleMovePathMouseRelease(const eMouseEvent &e) { } SmartNodePoint* drawPathAppend(const QList& fitted, - SmartNodePoint* endPoint) { - for(int i = 0; i < fitted.count(); i++) { + SmartNodePoint* endPoint) +{ + for (int i = 0; i < fitted.count(); i++) { const auto& seg = fitted.at(i); endPoint->moveC2ToAbsPos(seg.c1()); endPoint = endPoint->actionAddPointAbsPos(seg.p3()); @@ -457,12 +438,13 @@ SmartNodePoint* drawPathAppend(const QList& fitted, return endPoint; } -qsptr drawPathNew(QList& fitted) { +qsptr drawPathNew(QList& fitted) +{ const QPointF& begin = fitted.first().p0(); const QPointF& end = fitted.last().p3(); const qreal beginEndDist = pointToLen(end - begin); const bool close = beginEndDist < 7 && fitted.count() > 1; - if(close) fitted.last().setP3(begin); + if (close) { fitted.last().setP3(begin); } const auto newPath = enve::make_shared(); CubicList fittedList(fitted); newPath->loadSkPath(fittedList.toSkPath()); @@ -470,14 +452,16 @@ qsptr drawPathNew(QList& fitted) { return newPath; } -void Canvas::drawPathClear() { +void Canvas::drawPathClear() +{ mManualDrawPathState = ManualDrawPathState::none; mDrawPathFirst.clear(); mDrawPath.clear(); mDrawPathTmp.reset(); } -void Canvas::drawPathFinish(const qreal invScale) { +void Canvas::drawPathFinish(const qreal invScale) +{ mDrawPath.smooth(mDocument.fDrawPathSmooth); const bool manual = mDocument.fDrawPathManual; const qreal error = manual ? DBL_MAX/5 : @@ -485,7 +469,7 @@ void Canvas::drawPathFinish(const qreal invScale) { mDrawPath.fit(error, !manual); auto& fitted = mDrawPath.getFitted(); - if(!fitted.isEmpty()) { + if (!fitted.isEmpty()) { const QPointF& begin = fitted.first().p0(); const QPointF& end = fitted.last().p3(); const auto beginHover = getPointAtAbsPos(begin, mCurrentMode, invScale); @@ -496,12 +480,12 @@ void Canvas::drawPathFinish(const qreal invScale) { const bool endEndPoint = endNode ? endNode->isEndPoint() : false; bool createNew = false; - if(beginNode && endNode && beginNode != endNode) { + if (beginNode && endNode && beginNode != endNode) { const auto beginParent = beginNode->getTargetAnimator(); const auto endParent = endNode->getTargetAnimator(); const bool sampeParent = beginParent == endParent; - if(sampeParent) { + if (sampeParent) { const auto transform = beginNode->getTransform(); const auto matrix = transform->getTotalTransform(); const auto invMatrix = matrix.inverted(); @@ -512,13 +496,13 @@ void Canvas::drawPathFinish(const qreal invScale) { const int beginId = beginNode->getNodeId(); const int endId = endNode->getNodeId(); beginParent->actionReplaceSegments(beginId, endId, fitted); - } else if(beginEndPoint && endEndPoint) { + } else if (beginEndPoint && endEndPoint) { const bool reverse = endNode->hasNextPoint(); const auto orderedBegin = reverse ? endNode : beginNode; const auto orderedEnd = reverse ? beginNode : endNode; - if(orderedEnd->hasNextPoint() || !endNode->hasNextPoint()) { + if (orderedEnd->hasNextPoint() || !endNode->hasNextPoint()) { std::reverse(fitted.begin(), fitted.end()); std::for_each(fitted.begin(), fitted.end(), [](qCubicSegment2D& seg) { seg.reverse(); }); @@ -530,13 +514,13 @@ void Canvas::drawPathFinish(const qreal invScale) { last->moveC2ToAbsPos(lastSeg.c1()); orderedBegin->moveC0ToAbsPos(lastSeg.c2()); last->actionConnectToNormalPoint(orderedBegin); - } else createNew = true; - } else if(beginNode && beginEndPoint) { + } else { createNew = true; } + } else if (beginNode && beginEndPoint) { drawPathAppend(fitted, beginNode); - } else if(endNode && endEndPoint) { + } else if (endNode && endEndPoint) { drawPathAppend(fitted, endNode); - } else createNew = true; - if(createNew) { + } else { createNew = true; } + if (createNew) { const auto matrix = mCurrentContainer->getTotalTransform(); const auto invMatrix = matrix.inverted(); std::for_each(fitted.begin(), fitted.end(), @@ -605,32 +589,28 @@ void Canvas::handleLeftMouseRelease(const eMouseEvent &e) handleLeftMouseGizmos(); - if(mCurrentNormalSegment.isValid()) { - if(!mStartTransform) mCurrentNormalSegment.finishPassThroughTransform(); + if (mCurrentNormalSegment.isValid()) { + if (!mStartTransform) { mCurrentNormalSegment.finishPassThroughTransform(); } mHoveredNormalSegment = mCurrentNormalSegment; mHoveredNormalSegment.generateSkPath(); mCurrentNormalSegment.reset(); return; } - if(mDoubleClick) return; - if(mCurrentMode == CanvasMode::pointTransform) { + if (mDoubleClick) { return; } + if (mCurrentMode == CanvasMode::pointTransform) { handleMovePointMouseRelease(e); - } else if(mCurrentMode == CanvasMode::boxTransform) { - if(!mPressedPoint) { - handleMovePathMouseRelease(e); - } else { + } else if (mCurrentMode == CanvasMode::boxTransform) { + if (!mPressedPoint) { handleMovePathMouseRelease(e); } + else { handleMovePointMouseRelease(e); clearPointsSelection(); } - } else if(mCurrentMode == CanvasMode::pathCreate) { + } else if (mCurrentMode == CanvasMode::pathCreate) { handleAddSmartPointMouseRelease(e); - } else if(mCurrentMode == CanvasMode::drawPath) { + } else if (mCurrentMode == CanvasMode::drawPath) { const bool manual = mDocument.fDrawPathManual; - if(manual) { - mManualDrawPathState = ManualDrawPathState::drawn; - } else { - drawPathFinish(1/e.fScale); - } + if (manual) { mManualDrawPathState = ManualDrawPathState::drawn; } + else { drawPathFinish(1/e.fScale); } } else if (mCurrentMode == CanvasMode::pickFillStrokeEvent) { emit currentPickedColor(pickPixelColor(e.fGlobalPos)); } @@ -638,73 +618,77 @@ void Canvas::handleLeftMouseRelease(const eMouseEvent &e) mTransMode = TransformMode::none; } -QPointF Canvas::getMoveByValueForEvent(const eMouseEvent &e) { - if(mValueInput.inputEnabled()) +QPointF Canvas::getMoveByValueForEvent(const eMouseEvent &e) +{ + if (mValueInput.inputEnabled()) { return mValueInput.getPtValue(); + } const QPointF moveByPoint = e.fPos - e.fLastPressPos; mValueInput.setDisplayedValue(moveByPoint); - if(mValueInput.yOnlyMode()) return {0, moveByPoint.y()}; - else if(mValueInput.xOnlyMode()) return {moveByPoint.x(), 0}; + if (mValueInput.yOnlyMode()) { return {0, moveByPoint.y()}; } + else if (mValueInput.xOnlyMode()) { return {moveByPoint.x(), 0}; } return moveByPoint; } -#include -#include "MovablePoints/smartctrlpoint.h" -#include "MovablePoints/pathpointshandler.h" -#include "Boxes/smartvectorpath.h" -void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { - if(mRotPivot->isSelected()) { - if(mStartTransform) mRotPivot->startTransform(); +void Canvas::handleMovePointMouseMove(const eMouseEvent &e) +{ + if (mRotPivot->isSelected()) { + if (mStartTransform) { mRotPivot->startTransform(); } mRotPivot->moveByAbs(getMoveByValueForEvent(e)); - } else if(mTransMode == TransformMode::scale) { + } else if (mTransMode == TransformMode::scale) { scaleSelected(e); - } else if(mTransMode == TransformMode::shear) { + } else if (mTransMode == TransformMode::shear) { shearSelected(e); - } else if(mTransMode == TransformMode::rotate) { + } else if (mTransMode == TransformMode::rotate) { rotateSelected(e); - } else if(mCurrentNormalSegment.isValid()) { - if(mStartTransform) mCurrentNormalSegment.startPassThroughTransform(); + } else if (mCurrentNormalSegment.isValid()) { + if (mStartTransform) { mCurrentNormalSegment.startPassThroughTransform(); } mCurrentNormalSegment.makePassThroughAbs(e.fPos, mCurrentNormalSegmentT); } else { - if(mPressedPoint) { + const auto& gridSettings = mDocument.getGrid()->getSettings(); + const bool snappingActive = gridSettings.snapEnabled; + const bool boxesSnapEnabled = snappingActive && gridSettings.snapToBoxes; + const bool includeSelectedBounds = boxesSnapEnabled && mPressedPoint && mPressedPoint->isPivotPoint(); + + if (mPressedPoint) { addPointToSelection(mPressedPoint); const auto mods = QGuiApplication::queryKeyboardModifiers(); - if(mPressedPoint->isSmartNodePoint()) { - if(mods & Qt::ControlModifier) { + if (mPressedPoint->isSmartNodePoint()) { + if (mods & Qt::ControlModifier) { const auto nodePt = static_cast(mPressedPoint.data()); - if(nodePt->isDissolved()) { + if (nodePt->isDissolved()) { const int selId = nodePt->moveToClosestSegment(e.fPos); const auto handler = nodePt->getHandler(); const auto dissPt = handler->getPointWithId(selId); - if(nodePt->getNodeId() != selId) { + if (nodePt->getNodeId() != selId) { removePointFromSelection(nodePt); addPointToSelection(dissPt); } mPressedPoint = dissPt; return; } - } else if(mods & Qt::ShiftModifier) { + } else if (mods & Qt::ShiftModifier) { const auto nodePt = static_cast(mPressedPoint.data()); const auto nodePtAnim = nodePt->getTargetAnimator(); - if(nodePt->isNormal()) { + if (nodePt->isNormal()) { SmartNodePoint* closestNode = nullptr; qreal minDist = 10/e.fScale; - for(const auto& sBox : mSelectedBoxes) { - if(!enve_cast(sBox)) continue; + for (const auto& sBox : mSelectedBoxes) { + if (!enve_cast(sBox)) { continue; } const auto sPatBox = static_cast(sBox); const auto sAnim = sPatBox->getPathAnimator(); - for(int i = 0; i < sAnim->ca_getNumberOfChildren(); i++) { + for (int i = 0; i < sAnim->ca_getNumberOfChildren(); i++) { const auto sPath = sAnim->getChild(i); - if(sPath == nodePtAnim) continue; + if (sPath == nodePtAnim) { continue; } const auto sHandler = static_cast(sPath->getPointsHandler()); const auto node = sHandler->getClosestNode(e.fPos, minDist); - if(node) { + if (node) { closestNode = node; minDist = pointToLen(closestNode->getAbsolutePos() - e.fPos); } } } - if(closestNode) { + if (closestNode) { const bool reverse = mods & Qt::ALT; const auto sC0 = reverse ? closestNode->getC2Pt() : closestNode->getC0Pt(); @@ -717,7 +701,7 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { nodePt->getC0Pt()->setAbsolutePos(sC0->getAbsolutePos()); nodePt->getC2Pt()->setAbsolutePos(sC2->getAbsolutePos()); } else { - if(mStartTransform) mPressedPoint->startTransform(); + if (mStartTransform) { mPressedPoint->startTransform(); } mPressedPoint->moveByAbs(getMoveByValueForEvent(e)); } return; @@ -725,33 +709,64 @@ void Canvas::handleMovePointMouseMove(const eMouseEvent &e) { } } - if(!mPressedPoint->selectionEnabled()) { - if(mStartTransform) mPressedPoint->startTransform(); - mPressedPoint->moveByAbs(getMoveByValueForEvent(e)); + if (!mPressedPoint->selectionEnabled()) { + if (mStartTransform) { + mPressedPoint->startTransform(); + mGridMoveStartPivot = mPressedPoint->getAbsolutePos(); + } + + auto moveBy = getMoveByValueForEvent(e); + if (snappingActive) { + const auto snapped = moveBySnapTargets(e.fModifiers, + moveBy, + gridSettings, + includeSelectedBounds, + false, + false); + if (snapped.first) { moveBy = snapped.second; } + } + + mPressedPoint->moveByAbs(moveBy); return; } } - moveSelectedPointsByAbs(getMoveByValueForEvent(e), - mStartTransform); + + if (mStartTransform && !mSelectedPoints_d.isEmpty()) { + mGridMoveStartPivot = getSelectedPointsAbsPivotPos(); + } + + auto moveBy = getMoveByValueForEvent(e); + if (snappingActive && !mSelectedPoints_d.isEmpty()) { + const auto snapped = moveBySnapTargets(e.fModifiers, + moveBy, + gridSettings, + includeSelectedBounds, + false, + false); + if (snapped.first) { moveBy = snapped.second; } + } + + moveSelectedPointsByAbs(moveBy, mStartTransform); } } -void Canvas::scaleSelected(const eMouseEvent& e) { +void Canvas::scaleSelected(const eMouseEvent& e) +{ const QPointF absPos = mRotPivot->getAbsolutePos(); const QPointF distMoved = e.fPos - e.fLastPressPos; qreal scaleBy; - if(mValueInput.inputEnabled()) { - scaleBy = mValueInput.getValue(); - } else { + if (mValueInput.inputEnabled()) { scaleBy = mValueInput.getValue(); } + else { scaleBy = 1 + distSign({distMoved.x(), -distMoved.y()})*0.003; } + qreal scaleX; qreal scaleY; - if(mValueInput.xOnlyMode()) { + if (mValueInput.xOnlyMode()) { scaleX = scaleBy; scaleY = 1; - } else if(mValueInput.yOnlyMode()) { + } else if (mValueInput.yOnlyMode()) { scaleX = 1; scaleY = scaleBy; } else { @@ -759,14 +774,21 @@ void Canvas::scaleSelected(const eMouseEvent& e) { scaleY = scaleBy; } - if(mCurrentMode == CanvasMode::boxTransform) { - scaleSelectedBy(scaleX, scaleY, absPos, mStartTransform); + if (mCurrentMode == CanvasMode::boxTransform) { + scaleSelectedBy(scaleX, + scaleY, + absPos, + mStartTransform); } else { - scaleSelectedPointsBy(scaleX, scaleY, absPos, mStartTransform); + scaleSelectedPointsBy(scaleX, + scaleY, + absPos, + mStartTransform); } - if(!mValueInput.inputEnabled()) + if (!mValueInput.inputEnabled()) { mValueInput.setDisplayedValue({scaleX, scaleY}); + } mRotPivot->setMousePos(e.fPos); } @@ -780,28 +802,30 @@ void Canvas::shearSelected(const eMouseEvent& e) shearBy = mValueInput.getValue(); } else { qreal axisDelta; - if (mValueInput.xOnlyMode()) { - axisDelta = -distMoved.x(); - } else { - axisDelta = distMoved.y(); - } + if (mValueInput.xOnlyMode()) { axisDelta = -distMoved.x(); } + else { axisDelta = distMoved.y(); } shearBy = axisDelta * 0.01; } + qreal shearX = 0; qreal shearY = 0; - if (mValueInput.xOnlyMode()) { - shearX = shearBy; - } else if (mValueInput.yOnlyMode()) { - shearY = shearBy; - } else { + if (mValueInput.xOnlyMode()) { shearX = shearBy; } + else if (mValueInput.yOnlyMode()) { shearY = shearBy; } + else { shearX = shearBy; shearY = shearBy; } if (mCurrentMode == CanvasMode::boxTransform) { - shearSelectedBy(shearX, shearY, absPos, mStartTransform); + shearSelectedBy(shearX, + shearY, + absPos, + mStartTransform); } else { - shearSelectedPointsBy(shearX, shearY, absPos, mStartTransform); + shearSelectedPointsBy(shearX, + shearY, + absPos, + mStartTransform); } if (!mValueInput.inputEnabled()) { @@ -810,34 +834,32 @@ void Canvas::shearSelected(const eMouseEvent& e) mRotPivot->setMousePos(e.fPos); } -void Canvas::rotateSelected(const eMouseEvent& e) { +void Canvas::rotateSelected(const eMouseEvent& e) +{ const QPointF absPos = mRotPivot->getAbsolutePos(); qreal rot; - if(mValueInput.inputEnabled()) { + if (mValueInput.inputEnabled()) { rot = mValueInput.getValue(); } else { const QLineF dest_line(absPos, e.fPos); const QLineF prev_line(absPos, e.fLastPressPos); qreal d_rot = dest_line.angleTo(prev_line); - if(d_rot > 180) d_rot -= 360; - - if(mLastDRot - d_rot > 90) { - mRotHalfCycles += 2; - } else if(mLastDRot - d_rot < -90) { - mRotHalfCycles -= 2; - } + if (d_rot > 180) { d_rot -= 360; } + if (mLastDRot - d_rot > 90) { mRotHalfCycles += 2; } + else if (mLastDRot - d_rot < -90) { mRotHalfCycles -= 2; } mLastDRot = d_rot; rot = d_rot + mRotHalfCycles*180; } - if(mCurrentMode == CanvasMode::boxTransform) { + if (mCurrentMode == CanvasMode::boxTransform) { rotateSelectedBy(rot, absPos, mStartTransform); } else { rotateSelectedPointsBy(rot, absPos, mStartTransform); } - if(!mValueInput.inputEnabled()) + if (!mValueInput.inputEnabled()) { mValueInput.setDisplayedValue(rot); + } mRotPivot->setMousePos(e.fPos); } @@ -867,39 +889,54 @@ bool Canvas::prepareRotation(const QPointF &startPos, return true; } -void Canvas::handleMovePathMouseMove(const eMouseEvent& e) { - if(mRotPivot->isSelected()) { - if(mStartTransform) mRotPivot->startTransform(); +void Canvas::handleMovePathMouseMove(const eMouseEvent& e) +{ + if (mRotPivot->isSelected()) { + if (mStartTransform) { mRotPivot->startTransform(); } mRotPivot->moveByAbs(getMoveByValueForEvent(e)); - } else if(mTransMode == TransformMode::scale) { + } else if (mTransMode == TransformMode::scale) { scaleSelected(e); - } else if(mTransMode == TransformMode::shear) { + } else if (mTransMode == TransformMode::shear) { shearSelected(e); - } else if(mTransMode == TransformMode::rotate) { + } else if (mTransMode == TransformMode::rotate) { rotateSelected(e); } else { - if(mPressedBox) { + if (mPressedBox) { addBoxToSelection(mPressedBox); mPressedBox = nullptr; } - const auto moveBy = getMoveByValueForEvent(e); + const auto& gridSettings = mDocument.getGrid()->getSettings(); + + if (mStartTransform && !mSelectedBoxes.isEmpty()) { + collectAnchorOffsets(gridSettings); + } + + auto moveBy = getMoveByValueForEvent(e); + if (gridSettings.snapEnabled && !mSelectedBoxes.isEmpty()) { + const auto snapped = moveBySnapTargets(e.fModifiers, + moveBy, + gridSettings, + false, + true, + true); + if (snapped.first) { moveBy = snapped.second; } + } + moveSelectedBoxesByAbs(moveBy, mStartTransform); } } -void Canvas::updateTransformation(const eKeyEvent &e) { - if(mSelecting) { +void Canvas::updateTransformation(const eKeyEvent &e) +{ + if (mSelecting) { moveSecondSelectionPoint(e.fPos); - } else if(mCurrentMode == CanvasMode::pointTransform) { + } else if (mCurrentMode == CanvasMode::pointTransform) { handleMovePointMouseMove(e); - } else if(mCurrentMode == CanvasMode::boxTransform) { - if(!mPressedPoint) { - handleMovePathMouseMove(e); - } else { - handleMovePointMouseMove(e); - } - } else if(mCurrentMode == CanvasMode::pathCreate) { + } else if (mCurrentMode == CanvasMode::boxTransform) { + if (!mPressedPoint) { handleMovePathMouseMove(e); } + else { handleMovePointMouseMove(e); } + } else if (mCurrentMode == CanvasMode::pathCreate) { handleAddSmartPointMouseMove(e); } } diff --git a/src/core/gizmos.h b/src/core/gizmos.h index 2b23abc24..44800da6f 100644 --- a/src/core/gizmos.h +++ b/src/core/gizmos.h @@ -43,7 +43,8 @@ namespace Friction Position, Rotate, Scale, - Shear + Shear, + All }; enum class AxisConstraint { @@ -187,6 +188,7 @@ namespace Friction bool showScale = false; bool showShear = false; bool rotatingFromHandle = false; + bool visible = true; }; Config fConfig; diff --git a/src/core/grid.cpp b/src/core/grid.cpp new file mode 100644 index 000000000..3d6b3afc6 --- /dev/null +++ b/src/core/grid.cpp @@ -0,0 +1,901 @@ +/* +# +# 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, version 3. +# +# 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 "grid.h" + +#include "simplemath.h" +#include "skia/skqtconversions.h" + +#include "include/core/SkCanvas.h" +#include "include/core/SkPaint.h" +#include "include/core/SkPoint.h" + +#include "Private/esettings.h" +#include "appsupport.h" + +#include +#include +#include + +#include + +#include +#include +#include + +using namespace Friction::Core; + +Grid::Grid(QObject *parent) + : QObject(parent) +{ + +} + +void Grid::drawGrid(QPainter* painter, + const QRectF& worldViewport, + const QTransform& worldToScreen, + const qreal devicePixelRatio) const +{ + if (!painter || !mSettings.show) { return; } + + auto drawLine = [&](const QPointF& a, + const QPointF& b, + const bool major, + const Orientation orientation, + const double alphaFactor) + { + QPen pen(major ? mSettings.colorMajor : mSettings.color); + 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 Grid::drawGrid(SkCanvas* canvas, + const QRectF& worldViewport, + const QTransform& worldToScreen, + const qreal devicePixelRatio) const +{ + if (!canvas || !mSettings.show) { return; } + + 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 ? mSettings.colorMajor : mSettings.color; + 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 Grid::maybeSnapPivot(const QPointF& pivotWorld, + const QTransform& worldToScreen, + const bool forceSnap, + const bool bypassSnap, + const QRectF* canvasRectWorld, + const std::vector* anchorOffsets, + const std::vector* pivotTargets, + const std::vector* boxTargets, + const std::vector* nodeTargets) const +{ + const bool hasPivotTargets = mSettings.snapToPivots && + pivotTargets && !pivotTargets->empty(); + const bool hasBoxTargets = mSettings.snapToBoxes && + boxTargets && !boxTargets->empty(); + const bool hasNodeTargets = mSettings.snapToNodes && + nodeTargets && !nodeTargets->empty(); + + const bool snapSourcesEnabled = (mSettings.snapToGrid && mSettings.show) || + (mSettings.snapToCanvas && canvasRectWorld) || + hasPivotTargets || hasBoxTargets || hasNodeTargets; + + if ((!snapSourcesEnabled && !forceSnap) || bypassSnap) { return pivotWorld; } + + const double sizeX = mSettings.sizeX; + const double sizeY = mSettings.sizeY; + const bool hasGrid = sizeX > 0.0 && sizeY > 0.0 && mSettings.show; + + const bool canUseCanvas = mSettings.snapToCanvas && canvasRectWorld; + QRectF normalizedCanvas; + if (canUseCanvas) { normalizedCanvas = canvasRectWorld->normalized(); } + const bool hasCanvasTargets = canUseCanvas && !normalizedCanvas.isEmpty(); + + if (!hasGrid && !hasCanvasTargets && + !hasPivotTargets && !hasBoxTargets && + !hasNodeTargets) { return pivotWorld; } + + const std::vector* offsetsPtr = anchorOffsets; + std::vector fallbackOffsets; + if (!offsetsPtr) { + fallbackOffsets.emplace_back(QPointF(0.0, 0.0)); + offsetsPtr = &fallbackOffsets; + } + + const auto& offsets = *offsetsPtr; + if (offsets.empty()) { return pivotWorld; } + + struct AnchorContext { + QPointF offset; + QPointF world; + QPointF screen; + }; + + std::vector anchors; + anchors.reserve(offsets.size()); + for (const auto& offset : offsets) { + const QPointF worldPoint = pivotWorld + offset; + anchors.push_back({offset, + worldPoint, + worldToScreen.map(worldPoint)}); + } + + if (anchors.empty()) { return pivotWorld; } + + QPointF bestPivot = pivotWorld; + double bestDistance = std::numeric_limits::infinity(); + bool foundCandidate = false; + + auto considerCandidate = [&](const AnchorContext& anchor, + const QPointF& candidateAnchorWorld) + { + const QPointF candidatePivot = candidateAnchorWorld - anchor.offset; + const QPointF screenCandidate = worldToScreen.map(candidateAnchorWorld); + const double candidateDistance = QLineF(anchor.screen, screenCandidate).length(); + if (candidateDistance < bestDistance) { + bestDistance = candidateDistance; + bestPivot = candidatePivot; + foundCandidate = true; + } + }; + + if (hasGrid && (mSettings.snapToGrid || forceSnap)) { + for (const auto& anchor : anchors) { + const double gx = mSettings.originX + std::round((anchor.world.x() - mSettings.originX) / sizeX) * sizeX; + const double gy = mSettings.originY + std::round((anchor.world.y() - mSettings.originY) / sizeY) * sizeY; + considerCandidate(anchor, QPointF(gx, gy)); + } + } + + if (hasCanvasTargets) { + const double left = normalizedCanvas.left(); + const double right = normalizedCanvas.right(); + const double top = normalizedCanvas.top(); + const double bottom = normalizedCanvas.bottom(); + const double midX = (left + right) * 0.5; + const double midY = (top + bottom) * 0.5; + + const QPointF canvasTargets[] = { + QPointF(left, top), + QPointF(right, top), + QPointF(left, bottom), + QPointF(right, bottom), + QPointF(midX, top), + QPointF(midX, bottom), + QPointF(left, midY), + QPointF(right, midY), + QPointF(midX, midY) + }; + + for (const auto& anchor : anchors) { + for (const auto& target : canvasTargets) { considerCandidate(anchor, target); } + } + } + + if (hasPivotTargets) { + for (const auto& anchor : anchors) { + for (const auto& target : *pivotTargets) { considerCandidate(anchor, target); } + } + } + + if (hasBoxTargets) { + for (const auto& anchor : anchors) { + for (const auto& target : *boxTargets) { considerCandidate(anchor, target); } + } + } + + if (hasNodeTargets) { + for (const auto& anchor : anchors) { + for (const auto& target : *nodeTargets) { considerCandidate(anchor, target); } + } + } + + if (!foundCandidate) { return pivotWorld; } + if (forceSnap) { return bestPivot; } + + if (bestDistance <= mSettings.snapThresholdPx) { return bestPivot; } + return pivotWorld; +} + +QColor Grid::scaledAlpha(const QColor &base, + double factor) +{ + QColor c = base; + factor = clamp(factor, 0.0, 1.0); + c.setAlphaF(c.alphaF() * factor); + return c; +} + +double Grid::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 Grid::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 Grid::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); +} + +void Grid::setSettings(const Settings &settings, + const bool global) +{ + if (!differSettings(mSettings, settings)) { return; } + mSettings = settings; + if (global) { eSettings::sInstance->fGrid = settings; } + emit changed(settings); +} + +Grid::Settings Grid::getSettings() +{ + return mSettings; +} + +const Grid::Settings Grid::loadSettings() +{ + Settings settings; + { + const QVariant var = AppSupport::getSettings("grid", "sizeX"); + if (var.isValid()) { settings.sizeX = var.toDouble(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "sizeY"); + if (var.isValid()) { settings.sizeY = var.toDouble(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "originX"); + if (var.isValid()) { settings.originX = var.toDouble(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "originY"); + if (var.isValid()) { settings.originY = var.toDouble(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapThresholdPx"); + if (var.isValid()) { settings.snapThresholdPx = var.toInt(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "show"); + if (var.isValid()) { settings.show = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "drawOnTop"); + if (var.isValid()) { settings.drawOnTop = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapEnabled"); + if (var.isValid()) { settings.snapEnabled = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapToCanvas"); + if (var.isValid()) { settings.snapToCanvas = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapToBoxes"); + if (var.isValid()) { settings.snapToBoxes = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapToNodes"); + if (var.isValid()) { settings.snapToNodes = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapToPivots"); + if (var.isValid()) { settings.snapToPivots = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapToGrid"); + if (var.isValid()) { settings.snapToGrid = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapAnchorPivot"); + if (var.isValid()) { settings.snapAnchorPivot = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapAnchorBounds"); + if (var.isValid()) { settings.snapAnchorBounds = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "snapAnchorNodes"); + if (var.isValid()) { settings.snapAnchorNodes = var.toBool(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "majorEveryX"); + if (var.isValid()) { settings.majorEveryX = var.toInt(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "majorEveryY"); + if (var.isValid()) { settings.majorEveryY = var.toInt(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "color"); + if (var.isValid()) { settings.color = var.value(); } + } + { + const QVariant var = AppSupport::getSettings("grid", "colorMajor"); + if (var.isValid()) { settings.colorMajor = var.value(); } + } + qDebug() << "Load Grid Settings"; + debugSettings(settings); + return settings; +} + +void Grid::saveSettings(const Settings &settings) +{ + qDebug() << "Save Grid Settings"; + debugSettings(settings); + 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", "show", settings.show); + AppSupport::setSettings("grid", "drawOnTop", settings.drawOnTop); + AppSupport::setSettings("grid", "snapEnabled", settings.snapEnabled); + 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", "snapToGrid", settings.snapToGrid); + 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", "color", settings.color); + AppSupport::setSettings("grid", "colorMajor", settings.colorMajor); +} + +void Grid::debugSettings(const Settings &settings) +{ + qDebug() << "Grid Settings:" + << "sizeX" << settings.sizeX + << "sizeY" << settings.sizeY + << "originX" << settings.originX + << "originY" << settings.originY + << "snapThresholdPx" << settings.snapThresholdPx + << "show" << settings.show + << "drawOnTop" << settings.drawOnTop + << "snapEnabled" << settings.snapEnabled + << "snapToCanvas" << settings.snapToCanvas + << "snapToBoxes" << settings.snapToBoxes + << "snapToNodes" << settings.snapToNodes + << "snapToPivots" << settings.snapToPivots + << "snapToGrid" << settings.snapToGrid + << "snapAnchorPivot" << settings.snapAnchorPivot + << "snapAnchorBounds" << settings.snapAnchorBounds + << "snapAnchorNodes" << settings.snapAnchorNodes + << "majorEveryX" << settings.majorEveryX + << "majorEveryY" << settings.majorEveryY + << "color" << settings.color + << "colorMajor" << settings.colorMajor; +} + +bool Grid::differSettings(const Settings &orig, + const Settings &diff) +{ + if (orig.sizeX != diff.sizeX || + orig.sizeY != diff.sizeY || + orig.originX != diff.originX || + orig.originY != diff.originY || + orig.snapThresholdPx != diff.snapThresholdPx || + orig.show != diff.show || + orig.drawOnTop != diff.drawOnTop || + orig.snapEnabled != diff.snapEnabled || + orig.snapToCanvas != diff.snapToCanvas || + orig.snapToBoxes != diff.snapToBoxes || + orig.snapToNodes != diff.snapToNodes || + orig.snapToPivots != diff.snapToPivots || + orig.snapToGrid != diff.snapToGrid || + orig.snapAnchorPivot != diff.snapAnchorPivot || + orig.snapAnchorBounds != diff.snapAnchorBounds || + orig.snapAnchorNodes != diff.snapAnchorNodes || + orig.majorEveryX != diff.majorEveryX || + orig.majorEveryY != diff.majorEveryY || + orig.color != diff.color || + orig.colorMajor != diff.colorMajor) { return true; } + return false; +} + +void Grid::setOption(const Option &option, + const QVariant &value, + const bool global) +{ + qDebug() << "Grid::setOption" << (int)option << value << global; + QString key; + switch(option){ + case Option::SizeX: + if (mSettings.sizeX == value.toInt()) { return; } + mSettings.sizeX = value.toInt(); + if (global) { + eSettings::sInstance->fGrid.sizeX = value.toInt(); + key = "sizeX"; + } + break; + case Option::SizeY: + if (mSettings.sizeY == value.toInt()) { return; } + mSettings.sizeY = value.toInt(); + if (global) { + eSettings::sInstance->fGrid.sizeY = value.toInt(); + key = "sizeY"; + } + break; + case Option::OriginX: + if (mSettings.originX == value.toInt()) { return; } + mSettings.originX = value.toInt(); + if (global) { + eSettings::sInstance->fGrid.originX = value.toInt(); + key = "originX"; + } + break; + case Option::OriginY: + if (mSettings.originY == value.toInt()) { return; } + mSettings.originY = value.toInt(); + if (global) { + eSettings::sInstance->fGrid.originY = value.toInt(); + key = "originY"; + } + break; + case Option::SnapThresholdPx: + if (mSettings.snapThresholdPx == value.toInt()) { return; } + mSettings.snapThresholdPx = value.toInt(); + if (global) { + eSettings::sInstance->fGrid.snapThresholdPx = value.toInt(); + key = "snapThresholdPx"; + } + break; + case Option::Show: + if (mSettings.show == value.toBool()) { return; } + mSettings.show = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.show = value.toBool(); + key = "show"; + } + break; + case Option::DrawOnTop: + if (mSettings.drawOnTop == value.toBool()) { return; } + mSettings.drawOnTop = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.drawOnTop = value.toBool(); + key = "drawOnTop"; + } + break; + case Option::SnapEnabled: + if (mSettings.snapEnabled == value.toBool()) { return; } + mSettings.snapEnabled = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapEnabled = value.toBool(); + key = "snapEnabled"; + } + break; + case Option::SnapToCanvas: + if (mSettings.snapToCanvas == value.toBool()) { return; } + mSettings.snapToCanvas = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapToCanvas = value.toBool(); + key = "snapToCanvas"; + } + break; + case Option::SnapToBoxes: + if (mSettings.snapToBoxes == value.toBool()) { return; } + mSettings.snapToBoxes = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapToBoxes = value.toBool(); + key = "snapToBoxes"; + } + break; + case Option::SnapToNodes: + if (mSettings.snapToNodes == value.toBool()) { return; } + mSettings.snapToNodes = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapToNodes = value.toBool(); + key = "snapToNodes"; + } + break; + case Option::SnapToPivots: + if (mSettings.snapToPivots == value.toBool()) { return; } + mSettings.snapToPivots = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapToPivots = value.toBool(); + key = "snapToPivots"; + } + break; + case Option::SnapToGrid: + if (mSettings.snapToGrid == value.toBool()) { return; } + mSettings.snapToGrid = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapToGrid = value.toBool(); + key = "snapToGrid"; + } + break; + case Option::AnchorPivot: + if (mSettings.snapAnchorPivot == value.toBool()) { return; } + mSettings.snapAnchorPivot = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapAnchorPivot = value.toBool(); + key = "snapAnchorPivot"; + } + break; + case Option::AnchorBounds: + if (mSettings.snapAnchorBounds == value.toBool()) { return; } + mSettings.snapAnchorBounds = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapAnchorBounds = value.toBool(); + key = "snapAnchorBounds"; + } + break; + case Option::AnchorNodes: + if (mSettings.snapAnchorNodes == value.toBool()) { return; } + mSettings.snapAnchorNodes = value.toBool(); + if (global) { + eSettings::sInstance->fGrid.snapAnchorNodes = value.toBool(); + key = "snapAnchorNodes"; + } + break; + case Option::MajorEveryX: + if (mSettings.majorEveryX == value.toInt()) { return; } + mSettings.majorEveryX = value.toInt(); + if (global) { + eSettings::sInstance->fGrid.majorEveryX = value.toInt(); + key = "majorEveryX"; + } + break; + case Option::MajorEveryY: + if (mSettings.majorEveryY == value.toInt()) { return; } + mSettings.majorEveryY = value.toInt(); + if (global) { + eSettings::sInstance->fGrid.majorEveryY = value.toInt(); + key = "majorEveryY"; + } + break; + case Option::Color: + if (mSettings.color == value.value()) { return; } + mSettings.color = value.value(); + if (global) { + eSettings::sInstance->fGrid.color = value.value(); + key = "color"; + } + break; + case Option::ColorMajor: + if (mSettings.colorMajor == value.value()) { return; } + mSettings.colorMajor = value.value(); + if (global) { + eSettings::sInstance->fGrid.colorMajor = value.value(); + key = "colorMajor"; + } + break; + default: return; + } + + if (global && !key.isEmpty()) { + AppSupport::setSettings("grid", key, value); + } + + emit changed(mSettings); +} + +QVariant Grid::getOption(const Option &option) +{ + switch(option){ + case Option::SizeX: + return mSettings.sizeX; + case Option::SizeY: + return mSettings.sizeY; + case Option::OriginX: + return mSettings.originX; + case Option::OriginY: + return mSettings.originY; + case Option::SnapThresholdPx: + return mSettings.snapThresholdPx; + case Option::Show: + return mSettings.show; + case Option::DrawOnTop: + return mSettings.drawOnTop; + case Option::SnapEnabled: + return mSettings.snapEnabled; + case Option::SnapToCanvas: + return mSettings.snapToCanvas; + case Option::SnapToBoxes: + return mSettings.snapToBoxes; + case Option::SnapToNodes: + return mSettings.snapToNodes; + case Option::SnapToPivots: + return mSettings.snapToPivots; + case Option::SnapToGrid: + return mSettings.snapToGrid; + case Option::AnchorPivot: + return mSettings.snapAnchorPivot; + case Option::AnchorBounds: + return mSettings.snapAnchorBounds; + case Option::AnchorNodes: + return mSettings.snapAnchorNodes; + case Option::MajorEveryX: + return mSettings.majorEveryX; + case Option::MajorEveryY: + return mSettings.majorEveryY; + case Option::Color: + return mSettings.color; + case Option::ColorMajor: + return mSettings.colorMajor; + default: return QVariant(); + } +} + +void Grid::writeDocument(eWriteStream &dst) const +{ + qDebug() << "write grid settings to document"; + debugSettings(mSettings); + + dst << mSettings.sizeX; + dst << mSettings.sizeY; + dst << mSettings.originX; + dst << mSettings.originY; + dst << mSettings.snapThresholdPx; + dst << mSettings.show; + dst << mSettings.drawOnTop; + dst << mSettings.snapEnabled; + dst << mSettings.snapToCanvas; + dst << mSettings.snapToBoxes; + dst << mSettings.snapToNodes; + dst << mSettings.snapToPivots; + dst << mSettings.snapToGrid; + dst << mSettings.snapAnchorPivot; + dst << mSettings.snapAnchorBounds; + dst << mSettings.snapAnchorNodes; + dst << mSettings.majorEveryX; + dst << mSettings.majorEveryY; + dst << mSettings.color; + dst << mSettings.colorMajor; +} + +void Grid::readDocument(eReadStream &src) +{ + Settings settings = mSettings; + src >> settings.sizeX; + src >> settings.sizeY; + src >> settings.originX; + src >> settings.originY; + src >> settings.snapThresholdPx; + src >> settings.show; + src >> settings.drawOnTop; + src >> settings.snapEnabled; + src >> settings.snapToCanvas; + src >> settings.snapToBoxes; + src >> settings.snapToNodes; + src >> settings.snapToPivots; + src >> settings.snapToGrid; + src >> settings.snapAnchorPivot; + src >> settings.snapAnchorBounds; + src >> settings.snapAnchorNodes; + src >> settings.majorEveryX; + src >> settings.majorEveryY; + src >> settings.color; + src >> settings.colorMajor; + + qDebug() << "read grid settings from document"; + debugSettings(settings); + + // we only restore some settings (we can change this in the future) + mSettings.sizeX = settings.sizeX; + mSettings.sizeY = settings.sizeY; + mSettings.originX = settings.originX; + mSettings.originY = settings.originY; + mSettings.snapThresholdPx = settings.snapThresholdPx; + mSettings.majorEveryX = settings.majorEveryX; + mSettings.majorEveryY = settings.majorEveryY; + mSettings.color = settings.color; + mSettings.colorMajor = settings.colorMajor; + mSettings.drawOnTop = settings.drawOnTop; + + emit changed(mSettings); +} + +void Grid::writeXML(QDomDocument &doc) const +{ + qDebug() << "write grid settings to document"; + debugSettings(mSettings); + + auto element = doc.createElement("Grid"); + element.setAttribute("sizeX", QString::number(mSettings.sizeX)); + element.setAttribute("sizeY", QString::number(mSettings.sizeY)); + element.setAttribute("originX", QString::number(mSettings.originX)); + element.setAttribute("originY", QString::number(mSettings.originY)); + element.setAttribute("snapThresholdPx", QString::number(mSettings.snapThresholdPx)); + element.setAttribute("show", mSettings.show ? "true" : "false"); + element.setAttribute("drawOnTop", mSettings.drawOnTop ? "true" : "false"); + element.setAttribute("snapEnabled", mSettings.snapEnabled ? "true" : "false"); + element.setAttribute("snapToCanvas", mSettings.snapToCanvas ? "true" : "false"); + element.setAttribute("snapToBoxes", mSettings.snapToBoxes ? "true" : "false"); + element.setAttribute("snapToNodes", mSettings.snapToNodes ? "true" : "false"); + element.setAttribute("snapToPivots", mSettings.snapToPivots ? "true" : "false"); + element.setAttribute("snapToGrid", mSettings.snapToGrid ? "true" : "false"); + element.setAttribute("snapAnchorPivot", mSettings.snapAnchorPivot ? "true" : "false"); + element.setAttribute("snapAnchorBounds", mSettings.snapAnchorBounds ? "true" : "false"); + element.setAttribute("snapAnchorNodes", mSettings.snapAnchorNodes ? "true" : "false"); + element.setAttribute("majorEveryX", QString::number(mSettings.majorEveryX)); + element.setAttribute("majorEveryY", QString::number(mSettings.majorEveryY)); + element.setAttribute("color", mSettings.color.name()); + element.setAttribute("colorMajor", mSettings.colorMajor.name()); + doc.appendChild(element); +} + +void Grid::readXML(const QDomElement &element) +{ + if (element.isNull()) { return; } + + qDebug() << "read grid settings from document"; + // we only restore some settings (we can change this in the future) + if (element.hasAttribute("sizeX")) { + mSettings.sizeX = element.attribute("sizeX").toDouble(); + } + if (element.hasAttribute("sizeY")) { + mSettings.sizeY = element.attribute("sizeY").toDouble(); + } + if (element.hasAttribute("originX")) { + mSettings.originX = element.attribute("originX").toDouble(); + } + if (element.hasAttribute("originY")) { + mSettings.originY = element.attribute("originY").toDouble(); + } + if (element.hasAttribute("snapThresholdPx")) { + mSettings.snapThresholdPx = element.attribute("snapThresholdPx").toInt(); + } + if (element.hasAttribute("majorEveryX")) { + mSettings.majorEveryX = element.attribute("majorEveryX").toInt(); + } + if (element.hasAttribute("majorEveryY")) { + mSettings.majorEveryY = element.attribute("majorEveryY").toInt(); + } + if (element.hasAttribute("color")) { + mSettings.color = QColor(element.attribute("color")); + } + if (element.hasAttribute("colorMajor")) { + mSettings.colorMajor = QColor(element.attribute("colorMajor")); + } + if (element.hasAttribute("drawOnTop")) { + mSettings.drawOnTop = element.attribute("drawOnTop") == "true" ? true : false; + } + + emit changed(mSettings); +} + +template +void Grid::forEachGridLine(const QRectF& viewport, + const QTransform& worldToScreen, + const qreal devicePixelRatio, + DrawLineFunc&& drawLine) const +{ + if (!mSettings.show) { return; } + + const double sizeX = mSettings.sizeX; + const double sizeY = mSettings.sizeY; + if (sizeX <= 0.0 || sizeY <= 0.0) { return; } + + QRectF baseView = viewport.normalized(); + if (!baseView.isValid() || baseView.isEmpty()) { return; } + const double expandX = std::max(baseView.width(), sizeX); + const double expandY = std::max(baseView.height(), sizeY); + QRectF view = baseView.adjusted(-expandX, -expandY, expandX, expandY); + + const int majorEveryX = std::max(1, mSettings.majorEveryX); + const int majorEveryY = std::max(1, mSettings.majorEveryY); + + const double spacingX = lineSpacingPx(worldToScreen, devicePixelRatio, {sizeX, 0.0}); + const double spacingY = lineSpacingPx(worldToScreen, devicePixelRatio, {0.0, sizeY}); + const double majorSpacingX = spacingX * majorEveryX; + const double majorSpacingY = spacingY * majorEveryY; + + const double majorAlphaX = fadeFactor(majorSpacingX); + const double majorAlphaY = fadeFactor(majorSpacingY); + + if (majorAlphaX <= 0.0 && majorAlphaY <= 0.0) { + return; + } + + const double minorAlphaX = fadeFactor(spacingX); + const double minorAlphaY = fadeFactor(spacingY); + + auto firstAligned = [](double start, + double origin, + double spacing) + { + const double steps = std::floor((start - origin) / spacing); + return origin + steps * spacing; + }; + + const double originX = mSettings.originX; + const double originY = mSettings.originY; + + const double xBegin = firstAligned(view.left(), originX, sizeX); + const double xEnd = view.right() + sizeX; + + for (double x = xBegin; x <= xEnd; x += sizeX) { + const long long index = static_cast(std::llround((x - originX) / sizeX)); + const bool major = (index % majorEveryX) == 0; + const double alpha = major ? majorAlphaX : minorAlphaX; + if (!major && alpha <= 0.0) { continue; } + const QPointF top(x, view.top()); + const QPointF bottom(x, view.bottom()); + drawLine(top, bottom, major, Orientation::Vertical, alpha); + } + + const double yBegin = firstAligned(view.top(), originY, sizeY); + const double yEnd = view.bottom() + sizeY; + + for (double y = yBegin; y <= yEnd; y += sizeY) { + const long long index = static_cast(std::llround((y - originY) / sizeY)); + const bool major = (index % majorEveryY) == 0; + const double alpha = major ? majorAlphaY : minorAlphaY; + if (!major && alpha <= 0.0) { continue; } + const QPointF left(view.left(), y); + const QPointF right(view.right(), y); + drawLine(left, right, major, Orientation::Horizontal, alpha); + } +} diff --git a/src/core/grid.h b/src/core/grid.h new file mode 100644 index 000000000..7ae008915 --- /dev/null +++ b/src/core/grid.h @@ -0,0 +1,164 @@ +/* +# +# 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, version 3. +# +# 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 FRICTION_GRID_H +#define FRICTION_GRID_H + +#include "core_global.h" +#include "include/core/SkCanvas.h" +#include "ReadWrite/ereadstream.h" +#include "ReadWrite/ewritestream.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Friction +{ + namespace Core + { + class CORE_EXPORT Grid : public QObject + { + Q_OBJECT + public: + enum class Option { + SizeX, + SizeY, + OriginX, + OriginY, + SnapThresholdPx, + Show, + DrawOnTop, + SnapEnabled, + SnapToCanvas, + SnapToBoxes, + SnapToNodes, + SnapToPivots, + SnapToGrid, + AnchorPivot, + AnchorBounds, + AnchorNodes, + MajorEveryX, + MajorEveryY, + Color, + ColorMajor + }; + + struct Settings { + double sizeX = 40.0; + double sizeY = 40.0; + double originX = 960.0; + double originY = 540.0; + int snapThresholdPx = 40; + bool show = false; + bool drawOnTop = false; + bool snapEnabled = true; + bool snapToCanvas = false; + bool snapToBoxes = true; + bool snapToNodes = false; + bool snapToPivots = false; + bool snapToGrid = true; + bool snapAnchorPivot = true; + bool snapAnchorBounds = true; + bool snapAnchorNodes = false; + int majorEveryX = 8; + int majorEveryY = 8; + QColor color = QColor(128, 127, 255, 75); + QColor colorMajor = QColor(255, 127, 234, 125); + }; + + explicit Grid(QObject *parent = nullptr); + + void drawGrid(QPainter* painter, + const QRectF& worldViewport, + const QTransform& worldToScreen, + qreal devicePixelRatio = 1.0) const; + void drawGrid(SkCanvas* canvas, + const QRectF& worldViewport, + const QTransform& worldToScreen, + qreal devicePixelRatio = 1.0) const; + + QPointF maybeSnapPivot(const QPointF& pivotWorld, + const QTransform& worldToScreen, + bool forceSnap, + bool bypassSnap, + const QRectF* canvasRectWorld = nullptr, + const std::vector* anchorOffsets = nullptr, + const std::vector* pivotTargets = nullptr, + const std::vector* boxTargets = nullptr, + const std::vector* nodeTargets = nullptr) const; + + static QColor scaledAlpha(const QColor& base, double factor); + static double lineSpacingPx(const QTransform& worldToScreen, + qreal devicePixelRatio, + const QPointF& delta); + static double effectiveScale(const QTransform& worldToScreen); + static double fadeFactor(double spacingPx); + + Settings getSettings(); + void setSettings(const Settings &settings, + const bool global = false); + + static const Settings loadSettings(); + static void saveSettings(const Settings &settings); + static void debugSettings(const Settings &settings); + static bool differSettings(const Settings &orig, + const Settings &diff); + + void setOption(const Option &option, + const QVariant &value, + const bool global = false); + QVariant getOption(const Option &option); + + void writeDocument(eWriteStream &dst) const; + void readDocument(eReadStream &src); + + void writeXML(QDomDocument& doc) const; + void readXML(const QDomElement& element); + + signals: + void changed(const Friction::Core::Grid::Settings &settings); + + private: + enum class Orientation { + Vertical, + Horizontal + }; + + Settings mSettings; + + template + void forEachGridLine(const QRectF& worldViewport, + const QTransform& worldToScreen, + qreal devicePixelRatio, + DrawLineFunc&& drawLine) const; + }; + } +} + +#endif // FRICTION_GRID_H diff --git a/src/core/simplemath.cpp b/src/core/simplemath.cpp index 6b018115f..9d3c48d5a 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; } @@ -252,3 +257,10 @@ QPointF gQPointFDisplace(const QPointF& pt, const qreal displ) { return QPointF(pt.x() + gRandF(-displ, displ), pt.y() + gRandF(-displ, displ)); } + +bool nearlyEqual(double lhs, + double rhs) +{ + constexpr double eps = 1e-6; + return std::abs(lhs - rhs) <= eps; +} diff --git a/src/core/simplemath.h b/src/core/simplemath.h index bb3cb48c3..9e2433e7b 100644 --- a/src/core/simplemath.h +++ b/src/core/simplemath.h @@ -196,4 +196,12 @@ extern QPointF gQPointFDisplace(const QPointF& pt, const qreal displ); CORE_EXPORT extern bool isPointZero(QPointF pos); +CORE_EXPORT +extern bool isPointFinite(const QPointF& point); + +CORE_EXPORT +extern bool nearlyEqual(double lhs, + double rhs); + + #endif // SIMPLEMATH_H diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index e60e94183..50a00572f 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -105,6 +105,7 @@ set( widgets/toolbar.cpp widgets/toolbox.cpp widgets/toolcontrols.cpp + widgets/toolinteract.cpp widgets/twocolumnlayout.cpp widgets/uilayout.cpp widgets/toolboxtoolbar.cpp @@ -181,6 +182,7 @@ set( widgets/toolbox.h widgets/toolbutton.h widgets/toolcontrols.h + widgets/toolinteract.h widgets/twocolumnlayout.h widgets/uilayout.h widgets/vlabel.h diff --git a/src/ui/widgets/canvastoolbar.cpp b/src/ui/widgets/canvastoolbar.cpp index 80d895d65..6947b5f44 100644 --- a/src/ui/widgets/canvastoolbar.cpp +++ b/src/ui/widgets/canvastoolbar.cpp @@ -69,7 +69,7 @@ CanvasToolBar::CanvasToolBar(QWidget *parent) } mMemoryLabel = addAction(QIcon::fromTheme("memory"), tr("0 MB")); - mMemoryLabel->setToolTip(tr("Memory used")); + mMemoryLabel->setToolTip(tr("0 MB used")); setupDimensions(); setupResolution(); @@ -110,6 +110,7 @@ QComboBox *CanvasToolBar::getResolutionComboBox() void CanvasToolBar::setMemoryUsage(const intMB &usage) { + mMemoryLabel->setToolTip(tr("%1 MB used").arg(usage.fValue)); mMemoryLabel->setText(tr("%1 MB").arg(usage.fValue)); } diff --git a/src/ui/widgets/toolbox.cpp b/src/ui/widgets/toolbox.cpp index e7b76ceec..8baa092b7 100644 --- a/src/ui/widgets/toolbox.cpp +++ b/src/ui/widgets/toolbox.cpp @@ -36,6 +36,7 @@ ToolBox::ToolBox(Actions &actions, , mMain(nullptr) , mControls(nullptr) , mExtra(nullptr) + , mInteract(nullptr) , mGroupMain(nullptr) , mGroupNodes(nullptr) , mGroupDraw(nullptr) @@ -52,14 +53,17 @@ ToolBox::ToolBox(Actions &actions, QToolBar *ToolBox::getToolBar(const Type &type) { switch (type) { + case Type::Main: + return mMain; case Type::Controls: return mControls; case Type::Extra: return mExtra; + case Type::Interact: + return mInteract; default: - return mMain; + return nullptr; } - return nullptr; } const QList ToolBox::getMainActions() @@ -88,6 +92,7 @@ void ToolBox::setupToolBox(QWidget *parent) parent, true); mControls = new ToolControls(parent); + mInteract = new ToolInteract(parent); // disable for now /*mExtra = new ToolboxToolBar(tr("Extra Tools"), "ToolBoxExtra", @@ -324,7 +329,6 @@ void ToolBox::setupNodesAction(const QIcon &icon, } }); mControls->addAction(mGroupNodes->addAction(act)); - mGroupNodes->addAction(mControls->addSeparator()); ThemeSupport::setToolbarButtonStyle("ToolBoxButton", mControls, act); } diff --git a/src/ui/widgets/toolbox.h b/src/ui/widgets/toolbox.h index 86fa5a573..75ac83b2e 100644 --- a/src/ui/widgets/toolbox.h +++ b/src/ui/widgets/toolbox.h @@ -35,6 +35,7 @@ #include "widgets/toolbar.h" #include "widgets/toolboxtoolbar.h" #include "widgets/toolcontrols.h" +#include "widgets/toolinteract.h" #include "widgets/qdoubleslider.h" namespace Friction @@ -48,7 +49,8 @@ namespace Friction enum Type { Main, Controls, - Extra + Extra, + Interact }; enum Node { NodeConnect, @@ -84,6 +86,7 @@ namespace Friction ToolBar *mMain; ToolControls *mControls; ToolboxToolBar *mExtra; + ToolInteract *mInteract; QActionGroup *mGroupMain; QActionGroup *mGroupNodes; diff --git a/src/ui/widgets/toolcontrols.cpp b/src/ui/widgets/toolcontrols.cpp index 35a5d4e08..ffed2b18a 100644 --- a/src/ui/widgets/toolcontrols.cpp +++ b/src/ui/widgets/toolcontrols.cpp @@ -27,7 +27,6 @@ #include "Boxes/circle.h" #include "Boxes/rectangle.h" -using namespace Friction; using namespace Friction::Ui; ToolControls::ToolControls(QWidget *parent) @@ -52,9 +51,6 @@ ToolControls::ToolControls(QWidget *parent) , mTransformBottomRight(nullptr) , mTransformPivot(nullptr) , mTransformOpacity(nullptr) - , mTransformInteractVisible(AppSupport::getSettings("gizmos", - "ShowInToolbar", - true).toBool()) { setToolButtonStyle(Qt::ToolButtonIconOnly); setContextMenuPolicy(Qt::NoContextMenu); @@ -107,22 +103,6 @@ void ToolControls::setCanvasMode(const CanvasMode &mode) mTransformOpacity->setVisible(hasOpacity && isBoxMode); mTransformRadius->setVisible(hasRadius && showRadius); mTransformBottomRight->setVisible(hasRectangle && showRectangle); - mTransformInteract->setVisible(isBoxMode && mTransformInteractVisible); -} - -void ToolControls::setTransformInteractVisibility(bool visible) -{ - mTransformInteract->setVisible(visible && - mCanvasMode == CanvasMode::boxTransform); - mTransformInteractVisible = visible; - AppSupport::setSettings("gizmos", - "ShowInToolbar", - visible); -} - -bool ToolControls::getTransformInteractVisibility() -{ - return mTransformInteractVisible; } void ToolControls::setTransform(BoundingBox * const target) @@ -132,8 +112,6 @@ void ToolControls::setTransform(BoundingBox * const target) if (!target || multiple) { resetWidgets(); - if (multiple && - mCanvasMode == CanvasMode::boxTransform) { mTransformInteract->setVisible(true); } return; } @@ -218,7 +196,6 @@ void ToolControls::resetWidgets() mTransformBottomRight->setVisible(false); mTransformPivot->setVisible(false); mTransformOpacity->setVisible(false); - mTransformInteract->setVisible(false); } void ToolControls::setupWidgets() @@ -230,7 +207,6 @@ void ToolControls::setupWidgets() mTransformBottomRight = new QActionGroup(this); mTransformPivot = new QActionGroup(this); mTransformOpacity = new QActionGroup(this); - mTransformInteract = new QActionGroup(this); setupTransform(); } @@ -295,15 +271,6 @@ void ToolControls::setupTransform() mTransformRadius->addAction(addSeparator()); mTransformRadius->addAction(addWidget(mTransformRY)); - mTransformInteract->setExclusionPolicy(QActionGroup::ExclusionPolicy::None); - mTransformInteract->addAction(addSpacer(true, true)); - mTransformInteract->addAction(addAction(QIcon::fromTheme("gizmos"), - tr("Transform Interacts"))); - setupTransformInteract(Core::Gizmos::Interact::Position); - setupTransformInteract(Core::Gizmos::Interact::Rotate); - setupTransformInteract(Core::Gizmos::Interact::Scale); - setupTransformInteract(Core::Gizmos::Interact::Shear); - resetWidgets(); mTransformX->setValueRange(0, 1); @@ -331,74 +298,3 @@ void ToolControls::setupTransform() mTransformOX->setValueRange(0, 100); mTransformOX->setDisplayedValue(100); } - -void ToolControls::setupTransformInteract(const Core::Gizmos::Interact &ti) -{ - mTransformInteract->addAction(addSeparator()); - - const auto mDocument = Document::sInstance; - - const bool visible = mDocument->getGizmoVisibility(ti); - QIcon iconOn; - QIcon iconOff; - QString textOn; - QString textOff; - - switch(ti) { - case Core::Gizmos::Interact::Position: - iconOn = QIcon::fromTheme("gizmo_translate_on"); - iconOff = QIcon::fromTheme("gizmo_translate_off"); - textOn = tr("Hide Position Interact"); - textOff = tr("Show Position Interact"); - break; - case Core::Gizmos::Interact::Rotate: - iconOn = QIcon::fromTheme("gizmo_rotate_on"); - iconOff = QIcon::fromTheme("gizmo_rotate_off"); - textOn = tr("Hide Rotate Interact"); - textOff = tr("Show Rotate Interact"); - break; - case Core::Gizmos::Interact::Scale: - iconOn = QIcon::fromTheme("gizmo_scale_on"); - iconOff = QIcon::fromTheme("gizmo_scale_off"); - textOn = tr("Hide Scale Interact"); - textOff = tr("Show Scale Interact"); - break; - case Core::Gizmos::Interact::Shear: - iconOn = QIcon::fromTheme("gizmo_shear_on"); - iconOff = QIcon::fromTheme("gizmo_shear_off"); - textOn = tr("Hide Shear Interact"); - textOff = tr("Show Shear Interact"); - break; - default: return; - } - - const auto interact = mTransformInteract->addAction(addAction(visible ? iconOn : iconOff, visible ? textOn : textOff)); - - interact->setCheckable(true); - interact->setChecked(visible); - - ThemeSupport::setToolbarButtonStyle("ToolBoxGizmo", this, interact); - - connect(interact, &QAction::triggered, - this, [this, mDocument, ti]() { - if (!mTransformInteractVisible) { return; } - mDocument->setGizmoVisibility(ti, !mDocument->getGizmoVisibility(ti)); - }); - - connect(mDocument, &Document::gizmoVisibilityChanged, - this, [this, - interact, - iconOn, - iconOff, - textOn, - textOff, - ti](Core::Gizmos::Interact i, - bool visible) { - if (ti != i || !mTransformInteractVisible) { return; } - interact->blockSignals(true); - interact->setChecked(visible); - interact->blockSignals(false); - interact->setText(visible ? textOn : textOff); - interact->setIcon(visible ? iconOn : iconOff); - }); -} diff --git a/src/ui/widgets/toolcontrols.h b/src/ui/widgets/toolcontrols.h index 3baecdd97..7bb0609fa 100644 --- a/src/ui/widgets/toolcontrols.h +++ b/src/ui/widgets/toolcontrols.h @@ -28,7 +28,6 @@ #include "widgets/toolbar.h" #include "widgets/qrealanimatorvalueslider.h" #include "canvas.h" -#include "gizmos.h" #include #include @@ -47,15 +46,11 @@ namespace Friction void setCurrentBox(BoundingBox * const target); void setCanvasMode(const CanvasMode &mode); - void setTransformInteractVisibility(bool visible); - bool getTransformInteractVisibility(); - private: void setTransform(BoundingBox * const target); void resetWidgets(); void setupWidgets(); void setupTransform(); - void setupTransformInteract(const Core::Gizmos::Interact &ti); ConnContextQPtr mCanvas; CanvasMode mCanvasMode; @@ -80,9 +75,6 @@ namespace Friction QActionGroup *mTransformBottomRight; QActionGroup *mTransformPivot; QActionGroup *mTransformOpacity; - QActionGroup *mTransformInteract; - - bool mTransformInteractVisible; }; } } diff --git a/src/ui/widgets/toolinteract.cpp b/src/ui/widgets/toolinteract.cpp new file mode 100644 index 000000000..fd993cf8e --- /dev/null +++ b/src/ui/widgets/toolinteract.cpp @@ -0,0 +1,662 @@ +/* +# +# 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, version 3. +# +# 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 "toolinteract.h" + +#include "Private/document.h" +#include "GUI/coloranimatorbutton.h" + +#include +#include +#include +#include +#include + +using namespace Friction; +using namespace Friction::Ui; + +ToolInteract::ToolInteract(QWidget *parent) + : ToolBar("ToolInteract", parent, true) +{ + setToolButtonStyle(Qt::ToolButtonIconOnly); + setContextMenuPolicy(Qt::NoContextMenu); + setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea); + setWindowTitle(tr("Tool Interact")); + + setupGizmoButton(); + setupSnapButton(); + setupGridButton(); +} + +void ToolInteract::setupGizmoButton() +{ + const auto button = new QToolButton(this); + const auto menu = new QMenu(button); + + button->setObjectName("ToolBoxGizmo"); + button->setPopupMode(QToolButton::MenuButtonPopup); + button->setFocusPolicy(Qt::NoFocus); + button->setMenu(menu); + + { + const auto icon = QIcon::fromTheme("gizmos_off"); + const auto iconChecked = QIcon::fromTheme("gizmos_on"); + const auto act = new QAction(tr("Gizmos"), button); + const auto doc = Document::sInstance; + const bool visible = doc->getGizmoVisibility(Core::Gizmos::Interact::All); + + const QString gizmosOn = tr("Gizmos in On"); + const QString gizmosOff = tr("Gizmos is Off"); + + act->setCheckable(true); + act->setChecked(visible); + act->setText(visible ? gizmosOn : gizmosOff); + act->setIcon(visible ? iconChecked : icon); + + menu->addAction(act); + button->setDefaultAction(act); + + connect(act, &QAction::triggered, + this, [doc] () { + const auto gizmo = Core::Gizmos::Interact::All; + doc->setGizmoVisibility(gizmo, !doc->getGizmoVisibility(gizmo)); + }); + connect(doc, &Document::gizmoVisibilityChanged, + this, [act, + icon, + iconChecked, + gizmosOn, + gizmosOff] (Core::Gizmos::Interact i, bool visible) { + const auto gizmo = Core::Gizmos::Interact::All; + if (gizmo != i) { return; } + act->blockSignals(true); + act->setChecked(visible); + act->setText(visible ? gizmosOn : gizmosOff); + act->setIcon(visible ? iconChecked : icon); + act->blockSignals(false); + }); + } + menu->addSeparator(); + + setupGizmoAction(button, Core::Gizmos::Interact::Position); + setupGizmoAction(button, Core::Gizmos::Interact::Rotate); + setupGizmoAction(button, Core::Gizmos::Interact::Scale); + setupGizmoAction(button, Core::Gizmos::Interact::Shear); + + addWidget(button); +} + +void ToolInteract::setupGizmoAction(QToolButton *button, + const Core::Gizmos::Interact &ti) +{ + const auto mDocument = Document::sInstance; + const bool visible = mDocument->getGizmoVisibility(ti); + QString text; + + switch(ti) { + case Core::Gizmos::Interact::Position: + text = tr("Position"); + break; + case Core::Gizmos::Interact::Rotate: + text = tr("Rotate"); + break; + case Core::Gizmos::Interact::Scale: + text = tr("Scale"); + break; + case Core::Gizmos::Interact::Shear: + text = tr("Shear"); + break; + default: return; + } + + const auto act = button->menu()->addAction(text); + + act->setCheckable(true); + act->setChecked(visible); + + connect(act, &QAction::triggered, + this, [mDocument, ti] () { + mDocument->setGizmoVisibility(ti, !mDocument->getGizmoVisibility(ti)); + }); + + connect(mDocument, &Document::gizmoVisibilityChanged, + this, [act, ti] (Core::Gizmos::Interact i, + bool visible) { + if (ti != i) { return; } + act->blockSignals(true); + act->setChecked(visible); + act->blockSignals(false); + }); +} + +void ToolInteract::setupSnapButton() +{ + const auto grid = Document::sInstance->getGrid(); + const auto button = new QToolButton(this); + const auto menu = new QMenu(button); + + button->setObjectName("ToolBoxGizmo"); + button->setPopupMode(QToolButton::MenuButtonPopup); + button->setFocusPolicy(Qt::NoFocus); + button->setMenu(menu); + + { + const auto icon = QIcon::fromTheme("snap_off"); + const auto iconChecked = QIcon::fromTheme("snap_on"); + const QString snapOn = tr("Snapping is On"); + const QString snapOff = tr("Snapping is Off"); + + const auto act = new QAction(button); + const bool snap = grid->getSettings().snapEnabled; + + act->setCheckable(true); + act->setChecked(snap); + act->setText(snap ? snapOn : snapOff); + act->setIcon(snap ? iconChecked : icon); + act->setShortcut(QKeySequence("Shift+Tab")); + + connect(act, &QAction::triggered, + this, [grid, + act, + icon, + iconChecked, + snapOn, + snapOff] (const bool checked) { + act->setText(checked ? snapOn : snapOff); + act->setIcon(checked ? iconChecked : icon); + grid->setOption(Core::Grid::Option::SnapEnabled, checked, true); + }); + connect(grid, &Core::Grid::changed, + this, [act, + icon, + iconChecked, + snapOn, + snapOff] (const Core::Grid::Settings &settings) { + if (settings.snapEnabled == act->isChecked()) { return; } + act->blockSignals(true); + act->setChecked(settings.snapEnabled); + act->setText(settings.snapEnabled ? snapOn : snapOff); + act->setIcon(settings.snapEnabled ? iconChecked : icon); + act->blockSignals(false); + }); + + menu->addAction(act); + button->setDefaultAction(act); + } + + menu->addSeparator(); + setupSnapAction(button, Core::Grid::Option::SnapToCanvas); + setupSnapAction(button, Core::Grid::Option::SnapToBoxes); + setupSnapAction(button, Core::Grid::Option::SnapToNodes); + setupSnapAction(button, Core::Grid::Option::SnapToPivots); + setupSnapAction(button, Core::Grid::Option::SnapToGrid); + menu->addSeparator(); + setupSnapAction(button, Core::Grid::Option::AnchorPivot); + setupSnapAction(button, Core::Grid::Option::AnchorBounds); + setupSnapAction(button, Core::Grid::Option::AnchorNodes); + menu->addSeparator(); + + addWidget(button); +} + +void ToolInteract::setupSnapAction(QToolButton *button, + const Core::Grid::Option &option) +{ + if (!button) { return; } + if (!button->menu()) { return; } + + const auto grid = Document::sInstance->getGrid(); + const auto act = new QAction(button); + + act->setCheckable(true); + act->setChecked(grid->getOption(option).toBool()); + + switch (option) { + case Core::Grid::Option::SnapToCanvas: + act->setText(tr("Snap to Canvas")); + break; + case Core::Grid::Option::SnapToBoxes: + act->setText(tr("Snap to Boxes")); + break; + case Core::Grid::Option::SnapToNodes: + act->setText(tr("Snap to Nodes")); + break; + case Core::Grid::Option::SnapToPivots: + act->setText(tr("Snap to Pivots")); + break; + case Core::Grid::Option::SnapToGrid: + act->setText(tr("Snap to Grid (if visible)")); + break; + case Core::Grid::Option::AnchorPivot: + act->setText(tr("Anchor Pivot")); + break; + case Core::Grid::Option::AnchorBounds: + act->setText(tr("Anchor Bounds")); + break; + case Core::Grid::Option::AnchorNodes: + act->setText(tr("Anchor Nodes")); + break; + default: + qWarning() << "Unknown Snap Option!" << (int)option; + } + + button->menu()->addAction(act); + + connect(act, &QAction::triggered, + this, [grid, option] (const bool checked) { + grid->setOption(option, checked, true); + }); + connect(grid, &Core::Grid::changed, + this, [act, option] (const Core::Grid::Settings &settings) { + bool checked = false; + switch (option) { + case Core::Grid::Option::SnapToCanvas: + checked = settings.snapToCanvas; + break; + case Core::Grid::Option::SnapToBoxes: + checked = settings.snapToBoxes; + break; + case Core::Grid::Option::SnapToNodes: + checked = settings.snapToNodes; + break; + case Core::Grid::Option::SnapToPivots: + checked = settings.snapToPivots; + break; + case Core::Grid::Option::SnapToGrid: + checked = settings.snapToGrid; + break; + case Core::Grid::Option::AnchorPivot: + checked = settings.snapAnchorPivot; + break; + case Core::Grid::Option::AnchorBounds: + checked = settings.snapAnchorBounds; + break; + case Core::Grid::Option::AnchorNodes: + checked = settings.snapAnchorNodes; + break; + default: return; + } + if (checked == act->isChecked()) { return; } + act->blockSignals(true); + act->setChecked(checked); + act->blockSignals(false); + }); +} + +void ToolInteract::setupGridButton() +{ + const auto grid = Document::sInstance->getGrid(); + const auto button = new QToolButton(this); + const auto menu = new QMenu(button); + + button->setObjectName("ToolBoxGizmo"); + button->setPopupMode(QToolButton::MenuButtonPopup); + button->setFocusPolicy(Qt::NoFocus); + button->setMenu(menu); + + { + const auto icon = QIcon::fromTheme("grid_off"); + const auto iconChecked = QIcon::fromTheme("grid_on"); + + const QString gridOn = tr("Grid is On"); + const QString gridOff = tr("Grid is Off"); + + const auto act = new QAction(tr("Grid"), button); + const bool gridShow = grid->getSettings().show; + + act->setCheckable(true); + act->setChecked(gridShow); + act->setText(gridShow ? gridOn : gridOff); + act->setIcon(gridShow ? iconChecked : icon); + + menu->addAction(act); + button->setDefaultAction(act); + + connect(act, &QAction::triggered, + this, [grid, + act, + icon, + iconChecked, + gridOn, + gridOff] (const bool checked) { + act->setText(checked ? gridOn : gridOff); + act->setIcon(checked ? iconChecked : icon); + grid->setOption(Core::Grid::Option::Show, + checked, true); + }); + connect(grid, &Core::Grid::changed, + this, [act, + icon, + iconChecked, + gridOn, + gridOff] (const Core::Grid::Settings &settings) { + if (settings.show == act->isChecked()) { return; } + act->blockSignals(true); + act->setChecked(settings.snapEnabled); + act->setText(settings.snapEnabled ? gridOn : gridOff); + act->setIcon(settings.snapEnabled ? iconChecked : icon); + act->blockSignals(false); + }); + } + + menu->addSeparator(); + const auto optMenu = menu->addMenu(QIcon::fromTheme("preferences"), + tr("Settings")); + + menu->addSeparator(); + setupGridAction(button, Core::Grid::Option::SizeX); + setupGridAction(button, Core::Grid::Option::SizeY); + menu->addSeparator(); + setupGridAction(button, Core::Grid::Option::OriginX); + setupGridAction(button, Core::Grid::Option::OriginY); + menu->addSeparator(); + setupGridAction(button, Core::Grid::Option::MajorEveryX); + setupGridAction(button, Core::Grid::Option::MajorEveryY); + menu->addSeparator(); + + { + const auto act = new QWidgetAction(this); + const auto wid = new QWidget(this); + const auto lay = new QHBoxLayout(wid); + const auto spin = new QSpinBox(wid); + const auto label = new QLabel(tr("Threshold"), wid); + + wid->setContentsMargins(0, 0, 0, 0); + lay->setMargin(4); + + lay->addWidget(label); + lay->addWidget(spin); + + act->setDefaultWidget(wid); + menu->addAction(act); + + spin->setRange(0, 9999); + spin->setValue(grid->getSettings().snapThresholdPx); + + connect(spin, qOverload(&QSpinBox::valueChanged), + this, [grid] (const int value) { + grid->setOption(Core::Grid::Option::SnapThresholdPx, + value, true); + }); + connect(grid, &Core::Grid::changed, + this, [spin] (const Core::Grid::Settings &settings) { + if (settings.snapThresholdPx == spin->value()) { return; } + spin->blockSignals(true); + spin->setValue(settings.snapThresholdPx); + spin->blockSignals(false); + }); + } + + menu->addSeparator(); + setupGridAction(button, Core::Grid::Option::Color); + menu->addSeparator(); + setupGridAction(button, Core::Grid::Option::DrawOnTop); + menu->addSeparator(); + + { + const auto act = new QAction(QIcon::fromTheme("loop_back"), + tr("Reset Grid"), this); + optMenu->addAction(act); + connect(act, &QAction::triggered, + this, [grid] () { + auto settings = grid->getSettings(); + const auto fallback = Core::Grid::Settings(); + settings.sizeX = fallback.sizeX; + settings.sizeY = fallback.sizeY; + settings.originX = fallback.originX; + settings.originY = fallback.originY; + settings.snapThresholdPx = fallback.snapThresholdPx; + settings.majorEveryX = fallback.majorEveryX; + settings.majorEveryY = fallback.majorEveryY; + settings.color = fallback.color; + settings.colorMajor = fallback.colorMajor; + settings.drawOnTop = fallback.drawOnTop; + grid->setSettings(settings); + }); + } + + { + const auto act = new QAction(QIcon::fromTheme("loop_back"), + tr("Reset Default"), this); + optMenu->addAction(act); + connect(act, &QAction::triggered, + this, [grid] () { + auto settings = eSettings::instance().fGrid; + const auto fallback = Core::Grid::Settings(); + settings.sizeX = fallback.sizeX; + settings.sizeY = fallback.sizeY; + settings.originX = fallback.originX; + settings.originY = fallback.originY; + settings.snapThresholdPx = fallback.snapThresholdPx; + settings.majorEveryX = fallback.majorEveryX; + settings.majorEveryY = fallback.majorEveryY; + settings.color = fallback.color; + settings.colorMajor = fallback.colorMajor; + settings.drawOnTop = fallback.drawOnTop; + grid->saveSettings(settings); + eSettings::sInstance->fGrid = settings; + }); + } + + optMenu->addSeparator(); + + { + const auto act = new QAction(QIcon::fromTheme("file_folder"), + tr("Load from Default"), this); + optMenu->addAction(act); + connect(act, &QAction::triggered, + this, [grid] () { + auto settings = grid->getSettings(); + const auto fallback = eSettings::instance().fGrid; + settings.sizeX = fallback.sizeX; + settings.sizeY = fallback.sizeY; + settings.originX = fallback.originX; + settings.originY = fallback.originY; + settings.snapThresholdPx = fallback.snapThresholdPx; + settings.majorEveryX = fallback.majorEveryX; + settings.majorEveryY = fallback.majorEveryY; + settings.color = fallback.color; + settings.colorMajor = fallback.colorMajor; + settings.drawOnTop = fallback.drawOnTop; + grid->setSettings(settings); + }); + } + + { + const auto act = new QAction(QIcon::fromTheme("disk_drive"), + tr("Save as Default"), this); + optMenu->addAction(act); + connect(act, &QAction::triggered, + this, [grid] () { + const auto settings = grid->getSettings(); + auto defaults = eSettings::instance().fGrid; + defaults.sizeX = settings.sizeX; + defaults.sizeY = settings.sizeY; + defaults.originX = settings.originX; + defaults.originY = settings.originY; + defaults.snapThresholdPx = settings.snapThresholdPx; + defaults.majorEveryX = settings.majorEveryX; + defaults.majorEveryY = settings.majorEveryY; + defaults.color = settings.color; + defaults.colorMajor = settings.colorMajor; + defaults.drawOnTop = settings.drawOnTop; + grid->saveSettings(defaults); + eSettings::sInstance->fGrid = defaults; + }); + } + + addWidget(button); +} + +void ToolInteract::setupGridAction(QToolButton *button, + const Core::Grid::Option &option) +{ + if (!button) { return; } + if (!button->menu()) { return; } + + const auto grid = Document::sInstance->getGrid(); + const auto& settings = grid->getSettings(); + + if (option == Core::Grid::Option::DrawOnTop) { + const auto act = new QAction(tr("Draw on Top"), button); + + act->setCheckable(true); + act->setChecked(settings.drawOnTop); + button->menu()->addAction(act); + + connect(act, &QAction::triggered, + this, [grid, option] (const bool checked) { + grid->setOption(option, checked, false); + }); + connect(grid, &Core::Grid::changed, + this, [act] (const Core::Grid::Settings &settings) { + const bool checked = settings.drawOnTop; + if (checked == act->isChecked()) { return; } + act->blockSignals(true); + act->setChecked(checked); + act->blockSignals(false); + }); + } else if (option == Core::Grid::Option::Color) { + const auto act = new QWidgetAction(this); + const auto wid = new QWidget(this); + const auto lay = new QHBoxLayout(wid); + const auto label = new QLabel(tr("Colors"), wid); + const auto color1 = new ColorAnimatorButton(settings.color, wid); + const auto color2 = new ColorAnimatorButton(settings.colorMajor, wid); + + wid->setContentsMargins(0, 0, 0, 0); + lay->setContentsMargins(5, 2, 10, 2); + + lay->addWidget(label); + lay->addWidget(color1); + lay->addWidget(color2); + + act->setDefaultWidget(wid); + button->menu()->addAction(act); + + connect(color1, &ColorAnimatorButton::colorChanged, + this, [grid] (const QColor &color) { + grid->setOption(Core::Grid::Option::Color, + color, false); + }); + connect(color2, &ColorAnimatorButton::colorChanged, + this, [grid] (const QColor &color) { + grid->setOption(Core::Grid::Option::ColorMajor, + color, false); + }); + connect(grid, &Core::Grid::changed, + this, [color1, color2] (const Core::Grid::Settings &settings) { + if (settings.color != color1->color()) { + color1->blockSignals(true); + color1->setColor(settings.color); + color1->blockSignals(false); + } + if (settings.colorMajor != color2->color()) { + color2->blockSignals(true); + color2->setColor(settings.colorMajor); + color2->blockSignals(false); + } + }); + } else { + const auto act = new QWidgetAction(this); + const auto wid = new QWidget(this); + const auto lay = new QHBoxLayout(wid); + const auto spin = new QSpinBox(wid); + + QString labelText; + spin->setRange(0, 9999); + + switch (option) { + case Core::Grid::Option::SizeX: + spin->setValue(settings.sizeX); + labelText = tr("Size X"); + break; + case Core::Grid::Option::SizeY: + spin->setValue(settings.sizeY); + labelText = tr("Size Y"); + break; + case Core::Grid::Option::OriginX: + spin->setValue(settings.originX); + labelText = tr("Origin X"); + break; + case Core::Grid::Option::OriginY: + spin->setValue(settings.originY); + labelText = tr("Origin Y"); + break; + case Core::Grid::Option::MajorEveryX: + spin->setValue(settings.majorEveryX); + labelText = tr("Major X"); + break; + case Core::Grid::Option::MajorEveryY: + spin->setValue(settings.majorEveryY); + labelText = tr("Major Y"); + break; + default: + qWarning() << "Unknown Grid Option!" << (int)option; + } + + const auto label = new QLabel(labelText, wid); + + wid->setContentsMargins(0, 0, 0, 0); + lay->setContentsMargins(5, 2, 5, 2); + + lay->addWidget(label); + lay->addWidget(spin); + + act->setDefaultWidget(wid); + button->menu()->addAction(act); + + connect(spin, qOverload(&QSpinBox::valueChanged), + this, [grid, option] (const int value){ + grid->setOption(option, value, false); + }); + connect(grid, &Core::Grid::changed, + this, [spin, option] (const Core::Grid::Settings &settings) { + int value = -1; + switch (option) { + case Core::Grid::Option::SizeX: + value = settings.sizeX; + break; + case Core::Grid::Option::SizeY: + value = settings.sizeY; + break; + case Core::Grid::Option::OriginX: + value = settings.originX; + break; + case Core::Grid::Option::OriginY: + value = settings.originY; + break; + case Core::Grid::Option::MajorEveryX: + value = settings.majorEveryX; + break; + case Core::Grid::Option::MajorEveryY: + value = settings.majorEveryY; + break; + default: return; + } + if (value == spin->value()) { return; } + spin->blockSignals(true); + spin->setValue(value); + spin->blockSignals(false); + }); + } +} diff --git a/src/ui/widgets/toolinteract.h b/src/ui/widgets/toolinteract.h new file mode 100644 index 000000000..8b29b1df9 --- /dev/null +++ b/src/ui/widgets/toolinteract.h @@ -0,0 +1,60 @@ +/* +# +# 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, version 3. +# +# 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 FRICTION_TOOL_INTERACT_H +#define FRICTION_TOOL_INTERACT_H + +#include "ui_global.h" + +#include "widgets/toolbar.h" +#include "gizmos.h" +#include "grid.h" + +#include + +namespace Friction +{ + namespace Ui + { + class UI_EXPORT ToolInteract : public ToolBar + { + Q_OBJECT + + public: + explicit ToolInteract(QWidget *parent = nullptr); + + private: + void setupGizmoButton(); + void setupGizmoAction(QToolButton *button, + const Core::Gizmos::Interact &ti); + void setupSnapButton(); + void setupSnapAction(QToolButton *button, + const Core::Grid::Option &option); + void setupGridButton(); + void setupGridAction(QToolButton *button, + const Core::Grid::Option &option); + + }; + } +} + +#endif // FRICTION_TOOL_INTERACT_H