diff --git a/CHANGELOG.md b/CHANGELOG.md index ae65cbcc36..28620803e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -234,6 +234,7 @@ Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking Feature #3537: Shader-based water ripples Feature #5173: Support for NiFogProperty + Feature #5197: Editor: terrain vertex paint editmode Feature #5492: Let rain and snow collide with statics Feature #5926: Refraction based on water depth Feature #5944: Option to use camera as sound listener diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index a131c56358..6cd7722280 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -80,7 +80,7 @@ opencs_units (view/world ) opencs_units (view/widget - scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton + scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton scenetoolvertexpaintbrush scenetooltoggle2 scenetooltexturebrush scenetoolshapebrush completerpopup coloreditor colorpickerpopup droplineedit ) @@ -88,7 +88,7 @@ opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget previewwidget editmode instancemode instanceselectionmode instancemovemode orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller - cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands + cellwater terraintexturemode terrainvertexpaintmode actor terrainselection terrainshapemode brushdraw commands ) opencs_units (view/render diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 3fd35b7740..360ee98761 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -42,6 +42,7 @@ #include "mask.hpp" #include "terrainshapemode.hpp" #include "terraintexturemode.hpp" +#include "terrainvertexpaintmode.hpp" class QWidget; @@ -170,9 +171,8 @@ void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons(CSVWidget::Sce /// \todo replace EditMode with suitable subclasses tool->addButton(new TerrainShapeMode(this, mRootNode, tool), "terrain-shape"); tool->addButton(new TerrainTextureMode(this, mRootNode, tool), "terrain-texture"); - const QIcon vertexIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-vertex-paint"); + tool->addButton(new TerrainVertexPaintMode(this, mRootNode, tool), "terrain-vertex"); const QIcon movementIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-movement"); - tool->addButton(new EditMode(this, vertexIcon, Mask_Reference, "Terrain vertex paint editing"), "terrain-vertex"); tool->addButton(new EditMode(this, movementIcon, Mask_Reference, "Terrain movement"), "terrain-move"); } diff --git a/apps/opencs/view/render/terrainvertexpaintmode.cpp b/apps/opencs/view/render/terrainvertexpaintmode.cpp new file mode 100644 index 0000000000..93cdf1d37b --- /dev/null +++ b/apps/opencs/view/render/terrainvertexpaintmode.cpp @@ -0,0 +1,867 @@ +#include "terrainvertexpaintmode.hpp" + +#include <algorithm> +#include <cmath> +#include <memory> +#include <string> + +#include <QComboBox> +#include <QDropEvent> +#include <QEvent> +#include <QIcon> +#include <QWidget> + +#include <osg/Camera> +#include <osg/Vec3f> + +#include <apps/opencs/model/doc/document.hpp> +#include <apps/opencs/model/prefs/category.hpp> +#include <apps/opencs/model/prefs/setting.hpp> +#include <apps/opencs/model/world/cellselection.hpp> +#include <apps/opencs/model/world/columnimp.hpp> +#include <apps/opencs/model/world/columns.hpp> +#include <apps/opencs/model/world/data.hpp> +#include <apps/opencs/model/world/idcollection.hpp> +#include <apps/opencs/model/world/idtable.hpp> +#include <apps/opencs/model/world/land.hpp> +#include <apps/opencs/model/world/record.hpp> +#include <apps/opencs/model/world/universalid.hpp> +#include <apps/opencs/view/widget/brushshapes.hpp> +#include <apps/opencs/view/widget/scenetool.hpp> + +#include <components/debug/debuglog.hpp> +#include <components/esm3/loadland.hpp> + +#include "../widget/scenetoolbar.hpp" +#include "../widget/scenetoolvertexpaintbrush.hpp" + +#include "../../model/prefs/state.hpp" +#include "../../model/world/commands.hpp" +#include "../../model/world/idtree.hpp" + +#include "brushdraw.hpp" +#include "commands.hpp" +#include "editmode.hpp" +#include "mask.hpp" +#include "pagedworldspacewidget.hpp" +#include "terrainselection.hpp" +#include "worldspacewidget.hpp" + +class QPoint; +class QWidget; + +namespace CSMWorld +{ + struct Cell; +} + +namespace osg +{ + class Group; +} + +CSVRender::TerrainVertexPaintMode::TerrainVertexPaintMode( + WorldspaceWidget* worldspaceWidget, osg::Group* parentNode, QWidget* parent) + : EditMode(worldspaceWidget, QIcon{ ":scenetoolbar/editing-terrain-vertex-paint" }, Mask_Terrain, + "Terrain vertex paint editing", parent) + , mParentNode(parentNode) + , mVertexPaintEditToolColor(Qt::white) +{ +} + +void CSVRender::TerrainVertexPaintMode::activate(CSVWidget::SceneToolbar* toolbar) +{ + if (!mTerrainSelection) + { + mTerrainSelection + = std::make_shared<TerrainSelection>(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape); + } + + if (!mVertexPaintBrushScenetool) + { + mVertexPaintBrushScenetool = new CSVWidget::SceneToolVertexPaintBrush( + toolbar, "scenetoolvertexpaintbrush", getWorldspaceWidget().getDocument()); + connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow, &CSVWidget::VertexPaintBrushWindow::passBrushSize, + this, &TerrainVertexPaintMode::setBrushSize); + connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow, &CSVWidget::VertexPaintBrushWindow::passBrushShape, + this, &TerrainVertexPaintMode::setBrushShape); + connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow->mSizeSliders->mBrushSizeSlider, + &QSlider::valueChanged, this, &TerrainVertexPaintMode::setBrushSize); + connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow->mToolSelector, + qOverload<int>(&QComboBox::currentIndexChanged), this, &TerrainVertexPaintMode::setVertexPaintEditTool); + connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow->mColorButtonWidget, + &CSVWidget::ColorButtonWidget::colorChanged, this, &TerrainVertexPaintMode::setVertexPaintColor); + } + + if (!mBrushDraw) + mBrushDraw = std::make_unique<BrushDraw>(mParentNode); + + EditMode::activate(toolbar); + toolbar->addTool(mVertexPaintBrushScenetool); +} + +void CSVRender::TerrainVertexPaintMode::deactivate(CSVWidget::SceneToolbar* toolbar) +{ + if (mVertexPaintBrushScenetool) + { + toolbar->removeTool(mVertexPaintBrushScenetool); + } + + if (mTerrainSelection) + { + mTerrainSelection.reset(); + } + + if (mBrushDraw) + mBrushDraw.reset(); + + EditMode::deactivate(toolbar); +} + +void CSVRender::TerrainVertexPaintMode::primaryOpenPressed(const WorldspaceHitResult& hit) // Apply changes here +{ +} + +void CSVRender::TerrainVertexPaintMode::primaryEditPressed(const WorldspaceHitResult& hit) +{ + if (hit.hit && hit.tag == nullptr) + { + if (mDragMode == InteractionType_PrimaryEdit) + { + editVertexColourGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); + } + } + endVertexPaintEditing(); +} + +void CSVRender::TerrainVertexPaintMode::primarySelectPressed(const WorldspaceHitResult& hit) +{ + if (hit.hit && hit.tag == nullptr) + { + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + mTerrainSelection->clearTemporarySelection(); + } +} + +void CSVRender::TerrainVertexPaintMode::secondarySelectPressed(const WorldspaceHitResult& hit) +{ + if (hit.hit && hit.tag == nullptr) + { + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + mTerrainSelection->clearTemporarySelection(); + } +} + +bool CSVRender::TerrainVertexPaintMode::primaryEditStartDrag(const QPoint& pos) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + + mDragMode = InteractionType_PrimaryEdit; + + if (hit.hit && hit.tag == nullptr) + { + mEditingPos = hit.worldPos; + mIsEditing = true; + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + QUndoStack& undoStack = document.getUndoStack(); + undoStack.beginMacro("Set land vertex colours"); + } + + return true; +} + +bool CSVRender::TerrainVertexPaintMode::secondaryEditStartDrag(const QPoint& pos) +{ + return false; +} + +bool CSVRender::TerrainVertexPaintMode::primarySelectStartDrag(const QPoint& pos) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + mDragMode = InteractionType_PrimarySelect; + if (!hit.hit || hit.tag != nullptr) + { + mDragMode = InteractionType_None; + return false; + } + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + return true; +} + +bool CSVRender::TerrainVertexPaintMode::secondarySelectStartDrag(const QPoint& pos) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + mDragMode = InteractionType_SecondarySelect; + if (!hit.hit || hit.tag != nullptr) + { + mDragMode = InteractionType_None; + return false; + } + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + return true; +} + +void CSVRender::TerrainVertexPaintMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) +{ + if (mDragMode == InteractionType_PrimaryEdit) + { + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + mTotalDiffY += diffY; + if (mIsEditing) + editVertexColourGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); + } + + if (mDragMode == InteractionType_PrimarySelect) + { + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == nullptr) + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + } + + if (mDragMode == InteractionType_SecondarySelect) + { + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + if (hit.hit && hit.tag == nullptr) + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + } +} + +void CSVRender::TerrainVertexPaintMode::dragCompleted(const QPoint& pos) +{ + if (mDragMode == InteractionType_PrimaryEdit) + { + if (mIsEditing) + { + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + QUndoStack& undoStack = document.getUndoStack(); + undoStack.endMacro(); + } + endVertexPaintEditing(); + } + if (mDragMode == InteractionType_PrimarySelect || mDragMode == InteractionType_SecondarySelect) + { + mTerrainSelection->clearTemporarySelection(); + } +} + +void CSVRender::TerrainVertexPaintMode::dragAborted() +{ + if (mIsEditing) + { + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + QUndoStack& undoStack = document.getUndoStack(); + undoStack.endMacro(); + } + endVertexPaintEditing(); + mDragMode = InteractionType_None; +} + +void CSVRender::TerrainVertexPaintMode::dragWheel(int diff, double speedFactor) {} + +void CSVRender::TerrainVertexPaintMode::endVertexPaintEditing() +{ + mIsEditing = false; + mTerrainSelection->update(); +} + +void CSVRender::TerrainVertexPaintMode::editVertexColourGrid( + const std::pair<int, int>& vertexCoords, bool dragOperation) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable + = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + + std::string mCellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); + if (allowLandColourEditing(mCellId)) + { + } + + std::pair<CSMWorld::CellCoordinates, bool> cellCoordinates_pair = CSMWorld::CellCoordinates::fromId(mCellId); + + int cellX = cellCoordinates_pair.first.getX(); + int cellY = cellCoordinates_pair.first.getY(); + + int xHitInCell = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first); + int yHitInCell = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); + if (xHitInCell < 0) + { + xHitInCell = xHitInCell + ESM::Land::LAND_SIZE; + cellX = cellX - 1; + } + if (yHitInCell > 64) + { + yHitInCell = yHitInCell - ESM::Land::LAND_SIZE; + cellY = cellY + 1; + } + + mCellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); + if (allowLandColourEditing(mCellId)) + { + } + + std::string iteratedCellId; + + int colourColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandColoursIndex); + + int r = std::max(static_cast<float>(mBrushSize) / 2.0f, 1.0f); + + if (mBrushShape == CSVWidget::BrushShape_Point) + { + CSMWorld::LandColoursColumn::DataType newTerrain + = landTable.data(landTable.getModelIndex(mCellId, colourColumn)) + .value<CSMWorld::LandColoursColumn::DataType>(); + + if (allowLandColourEditing(mCellId)) + { + alterColour(newTerrain, xHitInCell, yHitInCell, 0.0f); + pushEditToCommand(newTerrain, document, landTable, mCellId); + } + } + + if (mBrushShape == CSVWidget::BrushShape_Square) + { + int upperLeftCellX = cellX - std::floor(r / ESM::Land::LAND_SIZE); + int upperLeftCellY = cellY - std::floor(r / ESM::Land::LAND_SIZE); + if (xHitInCell - (r % ESM::Land::LAND_SIZE) < 0) + upperLeftCellX--; + if (yHitInCell - (r % ESM::Land::LAND_SIZE) < 0) + upperLeftCellY--; + + int lowerrightCellX = cellX + std::floor(r / ESM::Land::LAND_SIZE); + int lowerrightCellY = cellY + std::floor(r / ESM::Land::LAND_SIZE); + if (xHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1) + lowerrightCellX++; + if (yHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1) + lowerrightCellY++; + + for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) + { + for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) + { + iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); + if (allowLandColourEditing(iteratedCellId)) + { + CSMWorld::LandColoursColumn::DataType newTerrain + = landTable.data(landTable.getModelIndex(iteratedCellId, colourColumn)) + .value<CSMWorld::LandColoursColumn::DataType>(); + for (int i = 0; i < ESM::Land::LAND_SIZE; i++) + { + for (int j = 0; j < ESM::Land::LAND_SIZE; j++) + { + + if (i_cell == cellX && j_cell == cellY && abs(i - xHitInCell) < r + && abs(j - yHitInCell) < r) + { + alterColour(newTerrain, i, j, 0.0f); + } + else + { + int distanceX(0); + int distanceY(0); + if (i_cell < cellX) + distanceX = xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) - i; + if (j_cell < cellY) + distanceY = yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) - j; + if (i_cell > cellX) + distanceX = -xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) + i; + if (j_cell > cellY) + distanceY = -yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) + j; + if (i_cell == cellX) + distanceX = abs(i - xHitInCell); + if (j_cell == cellY) + distanceY = abs(j - yHitInCell); + if (distanceX < r && distanceY < r) + alterColour(newTerrain, i, j, 0.0f); + } + } + } + pushEditToCommand(newTerrain, document, landTable, iteratedCellId); + } + } + } + } + + if (mBrushShape == CSVWidget::BrushShape_Circle) + { + int upperLeftCellX = cellX - std::floor(r / ESM::Land::LAND_SIZE); + int upperLeftCellY = cellY - std::floor(r / ESM::Land::LAND_SIZE); + if (xHitInCell - (r % ESM::Land::LAND_SIZE) < 0) + upperLeftCellX--; + if (yHitInCell - (r % ESM::Land::LAND_SIZE) < 0) + upperLeftCellY--; + + int lowerrightCellX = cellX + std::floor(r / ESM::Land::LAND_SIZE); + int lowerrightCellY = cellY + std::floor(r / ESM::Land::LAND_SIZE); + if (xHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1) + lowerrightCellX++; + if (yHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1) + lowerrightCellY++; + + for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) + { + for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) + { + iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); + if (allowLandColourEditing(iteratedCellId)) + { + CSMWorld::LandColoursColumn::DataType newTerrain + = landTable.data(landTable.getModelIndex(iteratedCellId, colourColumn)) + .value<CSMWorld::LandColoursColumn::DataType>(); + for (int i = 0; i < ESM::Land::LAND_SIZE; i++) + { + for (int j = 0; j < ESM::Land::LAND_SIZE; j++) + { + if (i_cell == cellX && j_cell == cellY && abs(i - xHitInCell) < r + && abs(j - yHitInCell) < r) + { + int distanceX = abs(i - xHitInCell); + int distanceY = abs(j - yHitInCell); + float distance = std::round(sqrt(pow(distanceX, 2) + pow(distanceY, 2))); + if (distance < r) + alterColour(newTerrain, i, j, 0.0f); + } + else + { + int distanceX(0); + int distanceY(0); + if (i_cell < cellX) + distanceX = xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) - i; + if (j_cell < cellY) + distanceY = yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) - j; + if (i_cell > cellX) + distanceX = -xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) + i; + if (j_cell > cellY) + distanceY = -yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) + j; + if (i_cell == cellX) + distanceX = abs(i - xHitInCell); + if (j_cell == cellY) + distanceY = abs(j - yHitInCell); + float distance = std::round(sqrt(pow(distanceX, 2) + pow(distanceY, 2))); + if (distance < r) + alterColour(newTerrain, i, j, 0.0f); + } + } + } + pushEditToCommand(newTerrain, document, landTable, iteratedCellId); + } + } + } + } +} + +void CSVRender::TerrainVertexPaintMode::alterColour( + CSMWorld::LandColoursColumn::DataType& landColorsNew, int inCellX, int inCellY, float alteredHeight, bool useTool) +{ + const int red = mVertexPaintEditToolColor.red(); + const int green = mVertexPaintEditToolColor.green(); + const int blue = mVertexPaintEditToolColor.blue(); + + // TODO: handle different smoothing/blend types with different tools, right now this expects Replace + landColorsNew[(inCellY * ESM::Land::LAND_SIZE + inCellX) * 3 + 0] = red; + landColorsNew[(inCellY * ESM::Land::LAND_SIZE + inCellX) * 3 + 1] = green; + landColorsNew[(inCellY * ESM::Land::LAND_SIZE + inCellX) * 3 + 2] = blue; +} + +void CSVRender::TerrainVertexPaintMode::pushEditToCommand(const CSMWorld::LandColoursColumn::DataType& newLandColours, + CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId) +{ + QVariant changedLand; + changedLand.setValue(newLandColours); + + QModelIndex index( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandColoursIndex))); + + QUndoStack& undoStack = document.getUndoStack(); + undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand)); +} + +bool CSVRender::TerrainVertexPaintMode::isInCellSelection(int globalSelectionX, int globalSelectionY) +{ + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget())) + { + std::pair<int, int> vertexCoords = std::make_pair(globalSelectionX, globalSelectionY); + std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); + return paged->getCellSelection().has(CSMWorld::CellCoordinates::fromId(cellId).first) && isLandLoaded(cellId); + } + return false; +} + +void CSVRender::TerrainVertexPaintMode::handleSelection( + int globalSelectionX, int globalSelectionY, std::vector<std::pair<int, int>>* selections) +{ + if (isInCellSelection(globalSelectionX, globalSelectionY)) + selections->emplace_back(globalSelectionX, globalSelectionY); + else + { + int moduloX = globalSelectionX % (ESM::Land::LAND_SIZE - 1); + int moduloY = globalSelectionY % (ESM::Land::LAND_SIZE - 1); + bool xIsAtCellBorder = moduloX == 0; + bool yIsAtCellBorder = moduloY == 0; + if (!xIsAtCellBorder && !yIsAtCellBorder) + return; + int selectionX = globalSelectionX; + int selectionY = globalSelectionY; + + /* + The northern and eastern edges don't belong to the current cell. + If the corresponding adjacent cell is not loaded, some special handling is necessary to select border + vertices. + */ + if (xIsAtCellBorder && yIsAtCellBorder) + { + /* + Handle the NW, NE, and SE corner vertices. + NW corner: (+1, -1) offset to reach current cell. + NE corner: (-1, -1) offset to reach current cell. + SE corner: (-1, +1) offset to reach current cell. + */ + if (isInCellSelection(globalSelectionX - 1, globalSelectionY - 1) + || isInCellSelection(globalSelectionX + 1, globalSelectionY - 1) + || isInCellSelection(globalSelectionX - 1, globalSelectionY + 1)) + { + selections->emplace_back(globalSelectionX, globalSelectionY); + } + } + else if (xIsAtCellBorder) + { + selectionX--; + } + else if (yIsAtCellBorder) + { + selectionY--; + } + + if (isInCellSelection(selectionX, selectionY)) + selections->emplace_back(globalSelectionX, globalSelectionY); + } +} + +void CSVRender::TerrainVertexPaintMode::selectTerrainShapes( + const std::pair<int, int>& vertexCoords, unsigned char selectMode) +{ + int r = mBrushSize / 2; + std::vector<std::pair<int, int>> selections; + + if (mBrushShape == CSVWidget::BrushShape_Point) + { + handleSelection(vertexCoords.first, vertexCoords.second, &selections); + } + + if (mBrushShape == CSVWidget::BrushShape_Square) + { + for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + { + for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + { + handleSelection(i, j, &selections); + } + } + } + + if (mBrushShape == CSVWidget::BrushShape_Circle) + { + for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) + { + for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) + { + int distanceX = abs(i - vertexCoords.first); + int distanceY = abs(j - vertexCoords.second); + float distance = sqrt(pow(distanceX, 2) + pow(distanceY, 2)); + + // Using floating-point radius here to prevent selecting too few vertices. + if (distance <= mBrushSize / 2.0f) + handleSelection(i, j, &selections); + } + } + } + + std::string selectAction; + + if (selectMode == 0) + selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); + else + selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); + + if (selectAction == "Select only") + mTerrainSelection->onlySelect(selections); + else if (selectAction == "Add to selection") + mTerrainSelection->addSelect(selections); + else if (selectAction == "Remove from selection") + mTerrainSelection->removeSelect(selections); + else if (selectAction == "Invert selection") + mTerrainSelection->toggleSelect(selections); +} + +bool CSVRender::TerrainVertexPaintMode::noCell(const std::string& cellId) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + const CSMWorld::IdCollection<CSMWorld::Cell>& cellCollection = document.getData().getCells(); + return cellCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1; +} + +bool CSVRender::TerrainVertexPaintMode::noLand(const std::string& cellId) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + const CSMWorld::IdCollection<CSMWorld::Land>& landCollection = document.getData().getLand(); + return landCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1; +} + +bool CSVRender::TerrainVertexPaintMode::noLandLoaded(const std::string& cellId) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + const CSMWorld::IdCollection<CSMWorld::Land>& landCollection = document.getData().getLand(); + return !landCollection.getRecord(ESM::RefId::stringRefId(cellId)).get().isDataLoaded(ESM::Land::DATA_VNML); +} + +bool CSVRender::TerrainVertexPaintMode::isLandLoaded(const std::string& cellId) +{ + if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId)) + return true; + return false; +} + +void CSVRender::TerrainVertexPaintMode::createNewLandData(const CSMWorld::CellCoordinates& cellCoords) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable + = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTable& ltexTable + = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); + int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); + int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); + + float defaultHeight = 0.f; + int averageDivider = 0; + CSMWorld::CellCoordinates cellLeftCoords = cellCoords.move(-1, 0); + CSMWorld::CellCoordinates cellRightCoords = cellCoords.move(1, 0); + CSMWorld::CellCoordinates cellUpCoords = cellCoords.move(0, -1); + CSMWorld::CellCoordinates cellDownCoords = cellCoords.move(0, 1); + + std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); + std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellLeftCoords.getX(), cellLeftCoords.getY()); + std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellRightCoords.getX(), cellRightCoords.getY()); + std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellUpCoords.getX(), cellUpCoords.getY()); + std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellDownCoords.getX(), cellDownCoords.getY()); + + float leftCellSampleHeight = 0.0f; + float rightCellSampleHeight = 0.0f; + float upCellSampleHeight = 0.0f; + float downCellSampleHeight = 0.0f; + + const CSMWorld::LandHeightsColumn::DataType landShapePointer + = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) + .value<CSMWorld::LandHeightsColumn::DataType>(); + const CSMWorld::LandNormalsColumn::DataType landNormalsPointer + = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn)) + .value<CSMWorld::LandNormalsColumn::DataType>(); + CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); + CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); + + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget())) + { + if (isLandLoaded(cellLeftId)) + { + const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer + = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn)) + .value<CSMWorld::LandHeightsColumn::DataType>(); + + ++averageDivider; + leftCellSampleHeight + = landLeftShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1]; + if (paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2)) + leftCellSampleHeight + += *paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2); + } + if (isLandLoaded(cellRightId)) + { + const CSMWorld::LandHeightsColumn::DataType landRightShapePointer + = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn)) + .value<CSMWorld::LandHeightsColumn::DataType>(); + + ++averageDivider; + rightCellSampleHeight = landRightShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE]; + if (paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2)) + rightCellSampleHeight += *paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2); + } + if (isLandLoaded(cellUpId)) + { + const CSMWorld::LandHeightsColumn::DataType landUpShapePointer + = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn)) + .value<CSMWorld::LandHeightsColumn::DataType>(); + + ++averageDivider; + upCellSampleHeight + = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE / 2)]; + if (paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1)) + upCellSampleHeight + += *paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1); + } + if (isLandLoaded(cellDownId)) + { + const CSMWorld::LandHeightsColumn::DataType landDownShapePointer + = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn)) + .value<CSMWorld::LandHeightsColumn::DataType>(); + + ++averageDivider; + downCellSampleHeight = landDownShapePointer[ESM::Land::LAND_SIZE / 2]; + if (paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0)) + downCellSampleHeight += *paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0); + } + } + if (averageDivider > 0) + defaultHeight = (leftCellSampleHeight + rightCellSampleHeight + upCellSampleHeight + downCellSampleHeight) + / averageDivider; + + for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) + { + for (int j = 0; j < ESM::Land::LAND_SIZE; ++j) + { + landShapeNew[j * ESM::Land::LAND_SIZE + i] = defaultHeight; + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = 0; + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = 0; + landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = 127; + } + } + QVariant changedShape; + changedShape.setValue(landShapeNew); + QVariant changedNormals; + changedNormals.setValue(landNormalsNew); + QModelIndex indexShape( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex))); + QModelIndex indexNormal( + landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex))); + document.getUndoStack().push(new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); + document.getUndoStack().push(new CSMWorld::ModifyCommand(landTable, indexShape, changedShape)); + document.getUndoStack().push(new CSMWorld::ModifyCommand(landTable, indexNormal, changedNormals)); +} + +bool CSVRender::TerrainVertexPaintMode::allowLandColourEditing(const std::string& cellId, bool useTool) +{ + CSMDoc::Document& document = getWorldspaceWidget().getDocument(); + CSMWorld::IdTable& landTable + = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land)); + CSMWorld::IdTree& cellTable + = dynamic_cast<CSMWorld::IdTree&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); + + if (noCell(cellId)) + { + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); + + // target cell does not exist + if (mode == "Discard") + return false; + + if (mode == "Create cell and land, then edit" && useTool) + { + auto createCommand = std::make_unique<CSMWorld::CreateCommand>(cellTable, cellId); + int parentIndex = cellTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); + int index = cellTable.findNestedColumnIndex(parentIndex, CSMWorld::Columns::ColumnId_Interior); + createCommand->addNestedValue(parentIndex, index, false); + document.getUndoStack().push(createCommand.release()); + + if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget())) + { + CSMWorld::CellSelection selection = paged->getCellSelection(); + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); + } + } + } + else if (CSVRender::PagedWorldspaceWidget* paged + = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget())) + { + CSMWorld::CellSelection selection = paged->getCellSelection(); + if (!selection.has(CSMWorld::CellCoordinates::fromId(cellId).first)) + { + // target cell exists, but is not shown + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString(); + + if (mode == "Discard") + return false; + + if (mode == "Show cell and edit" && useTool) + { + selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); + paged->setCellSelection(selection); + } + } + } + + if (noLand(cellId)) + { + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); + + // target cell does not exist + if (mode == "Discard") + return false; + + if (mode == "Create cell and land, then edit" && useTool) + { + document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, cellId)); + createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); + } + } + else if (noLandLoaded(cellId)) + { + std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); + + if (mode == "Discard") + return false; + + if (mode == "Create cell and land, then edit" && useTool) + { + createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); + } + } + + if (useTool && (noCell(cellId) || noLand(cellId) || noLandLoaded(cellId))) + { + Log(Debug::Warning) << "Land creation failed at cell id: " << cellId; + return false; + } + return true; +} + +void CSVRender::TerrainVertexPaintMode::dragMoveEvent(QDragMoveEvent* event) {} + +void CSVRender::TerrainVertexPaintMode::mouseMoveEvent(QMouseEvent* event) +{ + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); + if (hit.hit && mBrushDraw) + mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); + if (!hit.hit && mBrushDraw) + mBrushDraw->hide(); +} + +std::shared_ptr<CSVRender::TerrainSelection> CSVRender::TerrainVertexPaintMode::getTerrainSelection() +{ + return mTerrainSelection; +} + +void CSVRender::TerrainVertexPaintMode::setBrushSize(int brushSize) +{ + mBrushSize = brushSize; +} + +void CSVRender::TerrainVertexPaintMode::setBrushShape(CSVWidget::BrushShape brushShape) +{ + mBrushShape = brushShape; +} + +void CSVRender::TerrainVertexPaintMode::setVertexPaintEditTool(int shapeEditTool) +{ + mVertexPaintEditTool = shapeEditTool; +} + +void CSVRender::TerrainVertexPaintMode::setVertexPaintColor(const QColor& color) +{ + mVertexPaintEditToolColor = color; +} + +CSVRender::PagedWorldspaceWidget& CSVRender::TerrainVertexPaintMode::getPagedWorldspaceWidget() +{ + return dynamic_cast<PagedWorldspaceWidget&>(getWorldspaceWidget()); +} diff --git a/apps/opencs/view/render/terrainvertexpaintmode.hpp b/apps/opencs/view/render/terrainvertexpaintmode.hpp new file mode 100644 index 0000000000..a713175fe4 --- /dev/null +++ b/apps/opencs/view/render/terrainvertexpaintmode.hpp @@ -0,0 +1,180 @@ +#ifndef CSV_RENDER_TERRAINVERTEXPAINTMODE_H +#define CSV_RENDER_TERRAINVERTEXPAINTMODE_H + +#include "editmode.hpp" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include <apps/opencs/model/world/cellcoordinates.hpp> + +#include <osg/Vec3d> + +#ifndef Q_MOC_RUN +#include "../../model/world/columnimp.hpp" +#include "../widget/brushshapes.hpp" +#endif + +#include "brushdraw.hpp" + +class QDragMoveEvent; +class QMouseEvent; +class QObject; +class QPoint; +class QWidget; + +namespace osg +{ + class Group; +} + +namespace CSMDoc +{ + class Document; +} + +namespace CSVWidget +{ + class SceneToolVertexPaintBrush; +} + +namespace CSMWorld +{ + class IdTable; +} + +namespace CSVRender +{ + class PagedWorldspaceWidget; + class TerrainSelection; + class WorldspaceWidget; + struct WorldspaceHitResult; + class SceneToolbar; + + /// \brief EditMode for handling the terrain shape editing + class TerrainVertexPaintMode : public EditMode + { + Q_OBJECT + + public: + enum InteractionType + { + InteractionType_PrimaryEdit, + InteractionType_PrimarySelect, + InteractionType_SecondaryEdit, + InteractionType_SecondarySelect, + InteractionType_None + }; + + enum VertexPaintEditTool + { + VertexPaintEditTool_Replace = 0 + }; + + /// Editmode for terrain vertex colour grid + TerrainVertexPaintMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); + + void primaryOpenPressed(const WorldspaceHitResult& hit) override; + + /// Create single command for one-click vertex paint editing + void primaryEditPressed(const WorldspaceHitResult& hit) override; + + /// Open brush settings window + void primarySelectPressed(const WorldspaceHitResult&) override; + + void secondarySelectPressed(const WorldspaceHitResult&) override; + + void activate(CSVWidget::SceneToolbar*) override; + void deactivate(CSVWidget::SceneToolbar*) override; + + /// Start vertex paint editing command macro + bool primaryEditStartDrag(const QPoint& pos) override; + + bool secondaryEditStartDrag(const QPoint& pos) override; + bool primarySelectStartDrag(const QPoint& pos) override; + bool secondarySelectStartDrag(const QPoint& pos) override; + + /// Handle vertex paint edit behavior during dragging + void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) override; + + /// End vertex paint editing command macro + void dragCompleted(const QPoint& pos) override; + + /// Cancel vertex paint editing, and reset all pending changes + void dragAborted() override; + + void dragWheel(int diff, double speedFactor) override; + void dragMoveEvent(QDragMoveEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + + std::shared_ptr<TerrainSelection> getTerrainSelection(); + + private: + /// Reset everything in the current edit + void endVertexPaintEditing(); + + /// Handle brush mechanics for colour editing + void editVertexColourGrid(const std::pair<int, int>& vertexCoords, bool dragOperation); + + /// Alter one pixel's colour + void alterColour(CSMWorld::LandColoursColumn::DataType& landColorsNew, int inCellX, int inCellY, + float alteredHeight, bool useTool = true); + + /// Check if global selection coordinate belongs to cell in view + bool isInCellSelection(int globalSelectionX, int globalSelectionY); + + /// Select vertex at global selection coordinate + void handleSelection(int globalSelectionX, int globalSelectionY, std::vector<std::pair<int, int>>* selections); + + /// Handle brush mechanics for terrain selection + void selectTerrainShapes(const std::pair<int, int>& vertexCoords, unsigned char selectMode); + + bool noCell(const std::string& cellId); + + bool noLand(const std::string& cellId); + + bool noLandLoaded(const std::string& cellId); + + bool isLandLoaded(const std::string& cellId); + + /// Push terrain vertex coloir edits to command macro + void pushEditToCommand(const CSMWorld::LandColoursColumn::DataType& newLandColours, CSMDoc::Document& document, + CSMWorld::IdTable& landTable, const std::string& cellId); + + /// Create new blank height record and new normals, if there are valid adjancent cell, take sample points and + /// set the average height based on that + void createNewLandData(const CSMWorld::CellCoordinates& cellCoords); + + /// Create new cell and land if needed, only user tools may ask for opening new cells (useTool == false is for + /// automated land changes) + bool allowLandColourEditing(const std::string& textureFileName, bool useTool = true); + + std::string mBrushTexture; + int mBrushSize = 1; + CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; + std::unique_ptr<BrushDraw> mBrushDraw; + CSVWidget::SceneToolVertexPaintBrush* mVertexPaintBrushScenetool = nullptr; + int mDragMode = InteractionType_None; + osg::Group* mParentNode; + bool mIsEditing = false; + std::shared_ptr<TerrainSelection> mTerrainSelection; + int mTotalDiffY = 0; + std::vector<CSMWorld::CellCoordinates> mAlteredCells; + osg::Vec3d mEditingPos; + int mVertexPaintEditTool = VertexPaintEditTool_Replace; + QColor mVertexPaintEditToolColor; + int mTargetHeight = 0; + + PagedWorldspaceWidget& getPagedWorldspaceWidget(); + + public slots: + void setBrushSize(int brushSize); + void setBrushShape(CSVWidget::BrushShape brushShape); + void setVertexPaintEditTool(int shapeEditTool); + void setVertexPaintColor(const QColor& color); + }; +} + +#endif diff --git a/apps/opencs/view/widget/scenetoolvertexpaintbrush.cpp b/apps/opencs/view/widget/scenetoolvertexpaintbrush.cpp new file mode 100644 index 0000000000..d5da14e991 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolvertexpaintbrush.cpp @@ -0,0 +1,240 @@ +#include "scenetoolvertexpaintbrush.hpp" + +#include <QButtonGroup> +#include <QComboBox> +#include <QDragEnterEvent> +#include <QDropEvent> +#include <QFrame> +#include <QGroupBox> +#include <QHBoxLayout> +#include <QHeaderView> +#include <QIcon> +#include <QLabel> +#include <QSizePolicy> +#include <QSlider> +#include <QTableWidget> +#include <QVBoxLayout> +#include <QWidget> + +#include <apps/opencs/model/prefs/category.hpp> +#include <apps/opencs/model/prefs/setting.hpp> +#include <apps/opencs/view/widget/pushbutton.hpp> + +#include "brushshapes.hpp" +#include "scenetool.hpp" + +#include "../../model/prefs/state.hpp" + +namespace CSVWidget +{ + class SceneToolbar; +} + +namespace CSMDoc +{ + class Document; +} + +CSVWidget::VertexPaintBrushSizeControls::VertexPaintBrushSizeControls(const QString& title, QWidget* parent) + : QGroupBox(title, parent) +{ + mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); + mBrushSizeSlider->setTickInterval(10); + mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mBrushSizeSlider->setSingleStep(1); + + mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mBrushSizeSpinBox->setSingleStep(1); + + QHBoxLayout* layoutSliderSize = new QHBoxLayout; + layoutSliderSize->addWidget(mBrushSizeSlider); + layoutSliderSize->addWidget(mBrushSizeSpinBox); + + connect(mBrushSizeSlider, &QSlider::valueChanged, mBrushSizeSpinBox, &QSpinBox::setValue); + connect(mBrushSizeSpinBox, qOverload<int>(&QSpinBox::valueChanged), mBrushSizeSlider, &QSlider::setValue); + + setLayout(layoutSliderSize); +} + +CSVWidget::VertexPaintBrushWindow::VertexPaintBrushWindow(CSMDoc::Document& document, QWidget* parent) + : QFrame(parent, Qt::Popup) + , mDocument(document) +{ + mButtonPoint = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-point")), "", this); + mButtonSquare = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-square")), "", this); + mButtonCircle = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-circle")), "", this); + + mSizeSliders = new VertexPaintBrushSizeControls("Brush size", this); + + QVBoxLayout* layoutMain = new QVBoxLayout; + layoutMain->setSpacing(0); + layoutMain->setContentsMargins(4, 0, 4, 4); + + QHBoxLayout* layoutHorizontal = new QHBoxLayout; + layoutHorizontal->setSpacing(0); + layoutHorizontal->setContentsMargins(QMargins(0, 0, 0, 0)); + + configureButtonInitialSettings(mButtonPoint); + configureButtonInitialSettings(mButtonSquare); + configureButtonInitialSettings(mButtonCircle); + + mButtonPoint->setToolTip(toolTipPoint); + mButtonSquare->setToolTip(toolTipSquare); + mButtonCircle->setToolTip(toolTipCircle); + + QButtonGroup* brushButtonGroup = new QButtonGroup(this); + brushButtonGroup->addButton(mButtonPoint); + brushButtonGroup->addButton(mButtonSquare); + brushButtonGroup->addButton(mButtonCircle); + + brushButtonGroup->setExclusive(true); + + layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop); + layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop); + layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop); + + mHorizontalGroupBox = new QGroupBox(tr("")); + mHorizontalGroupBox->setLayout(layoutHorizontal); + + mToolSelector = new QComboBox(this); + mToolSelector->addItem(tr("Replace")); + // TOOD: in the future could add types like smooth blend, multiply etc + + QLabel* colorLabel = new QLabel(this); + colorLabel->setText("Color:"); + mColorButtonWidget = new ColorButtonWidget(); + + layoutMain->addWidget(mHorizontalGroupBox); + layoutMain->addWidget(mSizeSliders); + layoutMain->addWidget(mToolSelector); + layoutMain->addWidget(colorLabel); + layoutMain->addWidget(mColorButtonWidget); + + setLayout(layoutMain); + + connect(mButtonPoint, &QPushButton::clicked, this, &VertexPaintBrushWindow::setBrushShape); + connect(mButtonSquare, &QPushButton::clicked, this, &VertexPaintBrushWindow::setBrushShape); + connect(mButtonCircle, &QPushButton::clicked, this, &VertexPaintBrushWindow::setBrushShape); +} + +void CSVWidget::VertexPaintBrushWindow::configureButtonInitialSettings(QPushButton* button) +{ + button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + button->setContentsMargins(QMargins(0, 0, 0, 0)); + button->setIconSize(QSize(48 - 6, 48 - 6)); + button->setFixedSize(48, 48); + button->setCheckable(true); +} + +void CSVWidget::VertexPaintBrushWindow::setBrushSize(int brushSize) +{ + mBrushSize = brushSize; + emit passBrushSize(mBrushSize); +} + +void CSVWidget::VertexPaintBrushWindow::setBrushShape() +{ + if (mButtonPoint->isChecked()) + mBrushShape = BrushShape_Point; + if (mButtonSquare->isChecked()) + mBrushShape = BrushShape_Square; + if (mButtonCircle->isChecked()) + mBrushShape = BrushShape_Circle; + emit passBrushShape(mBrushShape); +} + +void CSVWidget::SceneToolVertexPaintBrush::adjustToolTips() {} + +CSVWidget::SceneToolVertexPaintBrush::SceneToolVertexPaintBrush( + SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document) + : SceneTool(parent, Type_TopAction) + , mToolTip(toolTip) + , mDocument(document) + , mVertexPaintBrushWindow(new VertexPaintBrushWindow(document, this)) +{ + setAcceptDrops(true); + connect(mVertexPaintBrushWindow, &VertexPaintBrushWindow::passBrushShape, this, + &SceneToolVertexPaintBrush::setButtonIcon); + setButtonIcon(mVertexPaintBrushWindow->mBrushShape); + + mPanel = new QFrame(this, Qt::Popup); + + QHBoxLayout* layout = new QHBoxLayout(mPanel); + + layout->setContentsMargins(QMargins(0, 0, 0, 0)); + + mTable = new QTableWidget(0, 2, this); + + mTable->setShowGrid(true); + mTable->verticalHeader()->hide(); + mTable->horizontalHeader()->hide(); + mTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + mTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + mTable->setSelectionMode(QAbstractItemView::NoSelection); + + layout->addWidget(mTable); + + connect(mTable, &QTableWidget::clicked, this, &SceneToolVertexPaintBrush::clicked); +} + +void CSVWidget::SceneToolVertexPaintBrush::setButtonIcon(CSVWidget::BrushShape brushShape) +{ + QString tooltip = "Change brush settings <p>Currently selected: "; + + switch (brushShape) + { + case BrushShape_Point: + + setIcon(QIcon(QPixmap(":scenetoolbar/brush-point"))); + tooltip += mVertexPaintBrushWindow->toolTipPoint; + break; + + case BrushShape_Square: + + setIcon(QIcon(QPixmap(":scenetoolbar/brush-square"))); + tooltip += mVertexPaintBrushWindow->toolTipSquare; + break; + + case BrushShape_Circle: + + setIcon(QIcon(QPixmap(":scenetoolbar/brush-circle"))); + tooltip += mVertexPaintBrushWindow->toolTipCircle; + break; + + case BrushShape_Custom: + + setIcon(QIcon(QPixmap(":scenetoolbar/brush-custom"))); + tooltip += mVertexPaintBrushWindow->toolTipCustom; + break; + } + + setToolTip(tooltip); +} + +void CSVWidget::SceneToolVertexPaintBrush::showPanel(const QPoint& position) {} + +void CSVWidget::SceneToolVertexPaintBrush::updatePanel() {} + +void CSVWidget::SceneToolVertexPaintBrush::clicked(const QModelIndex& index) {} + +void CSVWidget::SceneToolVertexPaintBrush::activate() +{ + QPoint position = QCursor::pos(); + mVertexPaintBrushWindow->mSizeSliders->mBrushSizeSlider->setRange( + 1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mVertexPaintBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange( + 1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); + mVertexPaintBrushWindow->move(position); + mVertexPaintBrushWindow->show(); +} + +void CSVWidget::SceneToolVertexPaintBrush::dragEnterEvent(QDragEnterEvent* event) +{ + emit passEvent(event); + event->accept(); +} +void CSVWidget::SceneToolVertexPaintBrush::dropEvent(QDropEvent* event) +{ + emit passEvent(event); + event->accept(); +} diff --git a/apps/opencs/view/widget/scenetoolvertexpaintbrush.hpp b/apps/opencs/view/widget/scenetoolvertexpaintbrush.hpp new file mode 100644 index 0000000000..a9b845d231 --- /dev/null +++ b/apps/opencs/view/widget/scenetoolvertexpaintbrush.hpp @@ -0,0 +1,165 @@ +#ifndef CSV_WIDGET_SCENETOOLVERTEXPAINTBRUSH_H +#define CSV_WIDGET_SCENETOOLVERTEXPAINTBRUSH_H + +#include <QColorDialog> +#include <QFrame> +#include <QGroupBox> +#include <QSlider> +#include <QSpinBox> + +#ifndef Q_MOC_RUN +#include "brushshapes.hpp" +#include "scenetool.hpp" +#endif + +class QComboBox; +class QDragEnterEvent; +class QDropEvent; +class QModelIndex; +class QObject; +class QPoint; +class QPushButton; +class QWidget; + +namespace CSMDoc +{ + class Document; +} + +class QTableWidget; + +namespace CSVRender +{ + class TerrainVertexPaintMode; +} + +namespace CSVWidget +{ + class SceneToolbar; + + class ColorButtonWidget : public QPushButton + { + Q_OBJECT + + public: + ColorButtonWidget(QWidget* parent = nullptr) + : QPushButton(parent) + { + this->setFixedSize(50, 25); + this->setObjectName("colorSwatchButton"); + this->setStyleSheet("QPushButton#colorSwatchButton { border: 1px solid #ccc; }"); + + connect(this, &QPushButton::clicked, this, &ColorButtonWidget::openColorDialog); + } + + private: + QColor mColor = Qt::white; + + signals: + void colorChanged(const QColor& newColor); + + private slots: + void openColorDialog() + { + QColor color = QColorDialog::getColor(mColor, this, "Select Color"); + if (color.isValid()) + { + mColor = color; + QString css = QString("QPushButton#colorSwatchButton { background-color: %1; border: 1px solid #ccc; }") + .arg(color.name()); + this->setStyleSheet(css); + emit colorChanged(color); + } + } + }; + + /// \brief Layout-box for some brush button settings + class VertexPaintBrushSizeControls : public QGroupBox + { + Q_OBJECT + + public: + VertexPaintBrushSizeControls(const QString& title, QWidget* parent); + + private: + QSlider* mBrushSizeSlider = new QSlider(Qt::Horizontal); + QSpinBox* mBrushSizeSpinBox = new QSpinBox; + + friend class SceneToolVertexPaintBrush; + friend class CSVRender::TerrainVertexPaintMode; + }; + + /// \brief Brush settings window + class VertexPaintBrushWindow : public QFrame + { + Q_OBJECT + + public: + VertexPaintBrushWindow(CSMDoc::Document& document, QWidget* parent = nullptr); + void configureButtonInitialSettings(QPushButton* button); + + const QString toolTipPoint = "Paint single point"; + const QString toolTipSquare = "Paint with square brush"; + const QString toolTipCircle = "Paint with circle brush"; + const QString toolTipCustom = "Paint with custom brush"; + + private: + CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point; + int mBrushSize = 1; + CSMDoc::Document& mDocument; + QGroupBox* mHorizontalGroupBox; + QComboBox* mToolSelector; + QPushButton* mButtonPoint; + QPushButton* mButtonSquare; + QPushButton* mButtonCircle; + VertexPaintBrushSizeControls* mSizeSliders; + ColorButtonWidget* mColorButtonWidget; + + friend class SceneToolVertexPaintBrush; + friend class CSVRender::TerrainVertexPaintMode; + + public slots: + void setBrushShape(); + void setBrushSize(int brushSize); + + signals: + void passBrushSize(int brushSize); + void passBrushShape(CSVWidget::BrushShape brushShape); + }; + + class SceneToolVertexPaintBrush : public SceneTool + { + Q_OBJECT + + QString mToolTip; + CSMDoc::Document& mDocument; + QFrame* mPanel; + QTableWidget* mTable; + VertexPaintBrushWindow* mVertexPaintBrushWindow; + + private: + void adjustToolTips(); + + public: + SceneToolVertexPaintBrush(SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document); + + void showPanel(const QPoint& position) override; + void updatePanel(); + + void dropEvent(QDropEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + + friend class CSVRender::TerrainVertexPaintMode; + + public slots: + void setButtonIcon(CSVWidget::BrushShape brushShape); + void clicked(const QModelIndex& index); + void activate() override; + + signals: + void passEvent(QDropEvent* event); + void passEvent(QDragEnterEvent* event); + }; +} + +#endif