From ee86436d5881fd0d35f7fa3309dd5d171441a696 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sat, 25 May 2024 21:36:42 -0500 Subject: [PATCH] [jasper/world/studio] Add map tile selection --- .../modules/world/src/studio/CMakeLists.txt | 1 + .../world/src/studio/maptilehighlighter.cpp | 143 ++++++++++++++++++ .../world/src/studio/maptilehighlighter.hpp | 50 ++++++ .../studio/worldeditor/worldeditor-imgui.cpp | 104 +++++++++---- .../studio/worldeditor/worldeditor-imgui.hpp | 9 +- .../studio/worldeditor/worldeditorview.cpp | 47 ++++++ .../studio/worldeditor/worldeditorview.hpp | 18 +++ 7 files changed, 338 insertions(+), 34 deletions(-) create mode 100644 src/jasper/modules/world/src/studio/maptilehighlighter.cpp create mode 100644 src/jasper/modules/world/src/studio/maptilehighlighter.hpp diff --git a/src/jasper/modules/world/src/studio/CMakeLists.txt b/src/jasper/modules/world/src/studio/CMakeLists.txt index d41b7f7..9d944b5 100644 --- a/src/jasper/modules/world/src/studio/CMakeLists.txt +++ b/src/jasper/modules/world/src/studio/CMakeLists.txt @@ -1,5 +1,6 @@ add_library( JasperWorld-Studio + maptilehighlighter.cpp studiomodule.cpp worldeditor/commands/commands.hpp ) diff --git a/src/jasper/modules/world/src/studio/maptilehighlighter.cpp b/src/jasper/modules/world/src/studio/maptilehighlighter.cpp new file mode 100644 index 0000000..aff30bb --- /dev/null +++ b/src/jasper/modules/world/src/studio/maptilehighlighter.cpp @@ -0,0 +1,143 @@ + +/* + * Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include "maptilehighlighter.hpp" + +namespace jasper::world { + +const glutils::ProgramSource MapTileHighlighter::s_programSrc = { + .shaderParams = { + { + .len = 2, + .name = ox::String("vPosition"), + }, + { + .len = 1, + .name = ox::String("vSelection"), + }, + }, + .vertShader = ox::sfmt(R"( + {} + in vec2 vPosition; + in float vSelection; + out float fSelection; + void main() { + gl_Position = vec4(vPosition, 0.0, 1.0); + fSelection = vSelection; + })", ncore::gl::GlslVersion), + .fragShader = ox::sfmt(R"( + {} + in float fSelection; + out vec4 outColor; + void main() { + outColor = vec4(0.0, 0.7, 1.0, 0.4) * fSelection; + })", ncore::gl::GlslVersion), +}; + +MapTileHighlighter::MapTileHighlighter() { + m_bufferSet.vao = glutils::generateVertexArrayObject(); + m_bufferSet.vbo = glutils::generateBuffer(); + m_bufferSet.ebo = glutils::generateBuffer(); + glBindVertexArray(m_bufferSet.vao); + sendVbo(m_bufferSet); + sendEbo(m_bufferSet); + m_shader = glutils::buildShaderProgram(s_programSrc).unwrapThrow(); + glBindVertexArray(0); +} + +ox::Error MapTileHighlighter::setup(ox::Size const&sz) noexcept { + m_subsheetTilesWidth = sz.width; + m_subsheetTilesHeight = sz.height; + initGlBuffers(); + return {}; +} + +void MapTileHighlighter::draw() noexcept { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glUseProgram(m_shader); + glBindVertexArray(m_bufferSet.vao); + sendVbo(m_bufferSet); + sendEbo(m_bufferSet); + auto const elmCnt = static_cast(m_bufferSet.elements.size()); + glDrawElements(GL_TRIANGLES, elmCnt, GL_UNSIGNED_INT, nullptr); + glBindVertexArray(0); + glUseProgram(0); + glDisable(GL_BLEND); +} + +ox::Error MapTileHighlighter::setTileHighlight(ox::Point const&addr, bool const hl) noexcept { + if (addr.x >= m_subsheetTilesWidth || addr.y >= m_subsheetTilesHeight) { + return OxError(1, "tile addr out of bounds"); + } + auto const vboLength = static_cast(s_programSrc.rowLen) * 4; + auto constexpr eboLength = 6; + auto const [x, y] = addr; + auto const i = static_cast(m_subsheetTilesWidth * y + x); + auto const vbo = &m_bufferSet.vertices[i * vboLength]; + auto const ebo = &m_bufferSet.elements[i * eboLength]; + setPixelBufferObject( + static_cast(i * 4), + static_cast(x), + static_cast(y), + hl, + vbo, + ebo); + return {}; +} + +int MapTileHighlighter::scale() noexcept { + return s_scale; +} + +void MapTileHighlighter::setPixelBufferObject( + uint_t vertexRow, + float x, float y, + bool const selected, + float *vbo, + GLuint *ebo) noexcept { + auto constexpr xmod = static_cast(ncore::TileWidth) / 240.f * 4; + auto constexpr ymod = static_cast(ncore::TileHeight) / 160.f * 4; + x *= xmod; + y *= -ymod; + x -= 1.0f; + y += 1.0f - ymod; + auto const selection = 1.f * static_cast(selected); + // don't worry, these memcpys gets optimized to something much more ideal + std::array const vertices{ + x, y, selection, // bottom left + x + xmod, y, selection, // bottom right + x + xmod, y + ymod, selection, // top right + x, y + ymod, selection, // top left + }; + memcpy(vbo, vertices.data(), sizeof(vertices)); + ox::Array const elms{ + vertexRow + 0, vertexRow + 1, vertexRow + 2, + vertexRow + 2, vertexRow + 3, vertexRow + 0, + }; + memcpy(ebo, elms.data(), sizeof(elms)); +} + +void MapTileHighlighter::initGlBuffers() noexcept { + auto const vboLength = static_cast(s_programSrc.rowLen) * 4; + auto constexpr eboLength = 6; + auto const tileCnt = + static_cast(m_subsheetTilesWidth) * static_cast(m_subsheetTilesHeight); + m_bufferSet.vertices.resize(tileCnt * vboLength); + m_bufferSet.elements.resize(tileCnt * eboLength); + for (int32_t y = 0; y < m_subsheetTilesHeight; ++y) { + for (int32_t x = 0; x < m_subsheetTilesWidth; ++x) { + std::ignore = setTileHighlight({x, y}, false); + } + } + glUseProgram(m_shader); + glBindVertexArray(m_bufferSet.vao); + sendVbo(m_bufferSet); + sendEbo(m_bufferSet); + glBindVertexArray(0); + glUseProgram(0); +} + +} diff --git a/src/jasper/modules/world/src/studio/maptilehighlighter.hpp b/src/jasper/modules/world/src/studio/maptilehighlighter.hpp new file mode 100644 index 0000000..4a334ec --- /dev/null +++ b/src/jasper/modules/world/src/studio/maptilehighlighter.hpp @@ -0,0 +1,50 @@ +/* + * Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include +#include + +#include + +namespace jasper::world { + +namespace ncore = nostalgia::core; + +class MapTileHighlighter { + private: + static const glutils::ProgramSource s_programSrc; + static constexpr int s_scale = 5; + glutils::GLProgram m_shader; + glutils::BufferSet m_bufferSet; + int32_t m_subsheetTilesWidth{}; + int32_t m_subsheetTilesHeight{}; + + public: + MapTileHighlighter(); + + ox::Error setup(ox::Size const&sz) noexcept; + + void draw() noexcept; + + ox::Error setTileHighlight(ox::Point const&addr, bool hl) noexcept; + + [[nodiscard]] + static int scale() noexcept; + + private: + static void setPixelBufferObject( + uint_t vertexRow, + float x, float y, + bool selected, + float *vbo, + GLuint *ebo) noexcept; + + void initGlBuffers() noexcept; +}; + +} diff --git a/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.cpp b/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.cpp index e7a0d00..ddaea9f 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.cpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.cpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -47,11 +48,11 @@ static WorldDoc makeValid(WorldDoc doc) noexcept { } [[nodiscard]] -static ox::Vec2 dropPos(ox::Vec2 dropPos) noexcept { +static ox::Vec2 fbPos(ox::Vec2 fbPos) noexcept { auto const winPos = ImGui::GetWindowPos(); - dropPos.x -= winPos.x; - dropPos.y -= winPos.y; - return dropPos; + fbPos.x -= winPos.x; + fbPos.y -= winPos.y; + return fbPos; } [[nodiscard]] @@ -70,6 +71,22 @@ constexpr ox::Point fbPtToTileAddr( }; } +[[nodiscard]] +constexpr ox::Point fbPtToTileAddr( + ox::Point const&fbPt, + ox::Vec2 const&mapSz) noexcept { + return fbPtToTileAddr(static_cast(fbPt), mapSz); +} + +[[nodiscard]] +constexpr studio::Selection fbPtToTileAddr( + studio::Selection sel, + ox::Vec2 const&mapSz) noexcept { + sel.a = fbPtToTileAddr(sel.a, mapSz); + sel.b = fbPtToTileAddr(sel.b, mapSz); + return sel; +} + WorldEditorImGui::WorldEditorImGui(studio::StudioContext &sctx, ox::StringView path): Editor(path), m_sctx(sctx), @@ -218,33 +235,59 @@ void WorldEditorImGui::drawWorldView() noexcept { paneSize, ImVec2(0, 1), ImVec2(xScale, 1 - yScale)); - std::ignore = ig::dragDropTarget([&, this] { - oxRequire(objId, ig::getDragDropPayload("WorldTile")); - auto const&io = ImGui::GetIO(); - auto const dropPos = world::dropPos(ox::Vec2{io.MousePos}); - auto const viewSz = m_view.drawSize(); - auto const tileAddr = fbPtToTileAddr( - dropPos, - ox::Vec2{ + std::ignore = ig::dragDropTarget([this, fbPaneScale] { + return handleDrop(fbPaneScale); + }); + handleSelection(static_cast(ox::Vec2(paneSize)), fbPaneScale); +} + +void WorldEditorImGui::handleSelection(ox::Size const&paneSz, float fbPaneScale) noexcept { + auto const&io = ImGui::GetIO(); + if (io.MouseDown[0]) { + auto const fbPos = world::fbPos(ox::Vec2{io.MousePos}); + auto constexpr inside = [](auto val, int min, int max) { + auto const v = static_cast(val); + return v < max && v > min; + }; + auto const startSel = io.MouseClicked[0] + && inside(fbPos.x, 0, paneSz.width) + && inside(fbPos.y, 0, paneSz.height); + m_selection.updateCursorPoint(fbPos, startSel); + auto const scaledViewSz = static_cast(m_view.drawSize()) * fbPaneScale; + if (m_selection.selectionOngoing()) { + m_view.setSelection(fbPtToTileAddr(m_selection.selection(), scaledViewSz)); + } + } else if (io.MouseReleased[0]) { + m_selection.finishSelection(); + } +} + +ox::Error WorldEditorImGui::handleDrop(float fbPaneScale) noexcept { + oxRequire(objId, ig::getDragDropPayload("WorldTile")); + auto const&io = ImGui::GetIO(); + auto const fbPos = world::fbPos(ox::Vec2{io.MousePos}); + auto const viewSz = m_view.drawSize(); + auto const tileAddr = fbPtToTileAddr( + fbPos, + ox::Vec2{ static_cast(viewSz.width) * fbPaneScale, static_cast(viewSz.height) * fbPaneScale}); - if (tileAddr.x < m_doc.columns && tileAddr.y < m_doc.rows) { - std::ignore = pushCommand( - m_doc, - m_worldStatic, - m_objCache, - ox::Vector{ - { - .layer = m_activeLayer, - .tileAddr = tileAddr, - .objId = objId.objId, - .setId = objId.setId, - }, - }); - } - oxReturnError(loadWorldStatic(m_objCache, m_doc).moveTo(m_worldStatic)); - return ox::Error{}; - }); + if (tileAddr.x < m_doc.columns && tileAddr.y < m_doc.rows) { + std::ignore = pushCommand( + m_doc, + m_worldStatic, + m_objCache, + ox::Vector{ + { + .layer = m_activeLayer, + .tileAddr = tileAddr, + .objId = objId.objId, + .setId = objId.setId, + }, + }); + } + oxReturnError(loadWorldStatic(m_objCache, m_doc).moveTo(m_worldStatic)); + return ox::Error{}; } ox::Error WorldEditorImGui::addObjSet(ox::StringView path) noexcept { @@ -278,7 +321,8 @@ ox::Error WorldEditorImGui::loadObjectSets() noexcept { return {}; } -ox::Error WorldEditorImGui::undoStackChanged(studio::UndoCommand const*) { +ox::Error WorldEditorImGui::undoStackChanged(studio::UndoCommand const*cmd) { + auto const clearSelection = dynamic_cast(cmd) != nullptr; oxReturnError(m_view.setupWorld()); return {}; } diff --git a/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.hpp b/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.hpp index f2a186e..8ddd931 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.hpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.hpp @@ -19,10 +19,7 @@ namespace jasper::world { class WorldEditorImGui: public studio::Editor { private: - struct Selection { - ox::Point begin, end; - }; - ox::Optional m_selection; + studio::SelectionTracker m_selection; uint8_t m_activeLayer{}; studio::StudioContext &m_sctx; studio::ig::FilePicker m_objSetPicker{ @@ -74,6 +71,10 @@ class WorldEditorImGui: public studio::Editor { void drawWorldView() noexcept; + void handleSelection(ox::Size const&paneSz, float fbPaneScale) noexcept; + + ox::Error handleDrop(float fbPaneScale) noexcept; + ox::Error addObjSet(ox::StringView path) noexcept; void rmObjSet() noexcept; diff --git a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp index b09eedb..616f7fc 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp @@ -16,6 +16,12 @@ WorldEditorView::WorldEditorView(turbine::Context &tctx, WorldStatic const&world ox::Error WorldEditorView::setupWorld() noexcept { glutils::resizeInitFrameBuffer(m_frameBuffer, ncore::gl::drawSize(m_scale)); + if (m_columns != m_worldStatic.columns || m_rows != m_worldStatic.rows) { + oxReturnError(m_highlighter.setup({m_worldStatic.columns, m_worldStatic.rows})); + m_columns = m_worldStatic.columns; + m_rows = m_worldStatic.rows; + m_selection.reset(); + } return m_world.setupDisplay(*m_cctx); } @@ -28,6 +34,19 @@ void WorldEditorView::draw(ox::Size const&targetSz) noexcept { } glutils::FrameBufferBind const frameBufferBind(m_frameBuffer); ncore::gl::draw(*m_cctx, m_scale); + m_highlighter.draw(); +} + +void WorldEditorView::setSelection(studio::Selection const&sel) noexcept { + if (m_selection) { + studio::iterateSelection(*m_selection, [this](int32_t x, int32_t y) { + std::ignore = m_highlighter.setTileHighlight({x, y}, false); + }); + } + m_selection.emplace(sel); + studio::iterateSelection(*m_selection, [this](int32_t x, int32_t y) { + std::ignore = m_highlighter.setTileHighlight({x, y}, true); + }); } glutils::FrameBuffer const&WorldEditorView::framebuffer() const noexcept { @@ -38,4 +57,32 @@ ox::Size WorldEditorView::drawSize() const noexcept { return ncore::gl::drawSize(m_scale); } +void WorldEditorView::setPixelBufferObject( + unsigned vertexRow, + float x, float y, + bool const selected, + float *vbo, + GLuint *ebo) const noexcept { + auto constexpr xmod = static_cast(ncore::TileWidth) / 240.f * 4; + auto constexpr ymod = static_cast(ncore::TileHeight) / 160.f * 4; + x *= xmod; + y *= -ymod; + x -= 1.0f; + y += 1.0f - ymod; + auto const selection = 1.f * static_cast(selected); + // don't worry, these memcpys gets optimized to something much more ideal + std::array const vertices{ + x, y, selection, // bottom left + x + xmod, y, selection, // bottom right + x + xmod, y + ymod, selection, // top right + x, y + ymod, selection, // top left + }; + memcpy(vbo, vertices.data(), sizeof(vertices)); + ox::Array const elms{ + vertexRow + 0, vertexRow + 1, vertexRow + 2, + vertexRow + 2, vertexRow + 3, vertexRow + 0, + }; + memcpy(ebo, elms.data(), sizeof(elms)); +} + } diff --git a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp index 1bb4f35..f6eb4eb 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp @@ -6,10 +6,15 @@ #include +#include + #include #include + #include +#include "../maptilehighlighter.hpp" + namespace jasper::world { namespace ncore = nostalgia::core; @@ -19,10 +24,13 @@ class WorldEditorView { private: ncore::ContextUPtr m_cctx; WorldStatic const&m_worldStatic; + int m_columns{}, m_rows{}; World m_world; glutils::FrameBuffer m_frameBuffer; int m_scale = 1; ox::Size m_scaleSz = ncore::gl::drawSize(m_scale); + ox::Optional m_selection; + MapTileHighlighter m_highlighter; public: WorldEditorView(turbine::Context &ctx, WorldStatic const&worldStatic); @@ -31,6 +39,8 @@ class WorldEditorView { void draw(ox::Size const&targetSz) noexcept; + void setSelection(studio::Selection const&sel) noexcept; + [[nodiscard]] glutils::FrameBuffer const&framebuffer() const noexcept; @@ -40,6 +50,14 @@ class WorldEditorView { [[nodiscard]] ox::Size drawSize() const noexcept; + private: + void setPixelBufferObject( + unsigned vertexRow, + float x, float y, + bool const selected, + float *vbo, + GLuint *ebo) const noexcept; + }; }