[jasper/world/studio] Add map tile selection

This commit is contained in:
Gary Talent 2024-05-25 21:36:42 -05:00
parent 9aec5772f4
commit ee86436d58
7 changed files with 338 additions and 34 deletions

View File

@ -1,5 +1,6 @@
add_library(
JasperWorld-Studio
maptilehighlighter.cpp
studiomodule.cpp
worldeditor/commands/commands.hpp
)

View File

@ -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<GLsizei>(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<size_t>(s_programSrc.rowLen) * 4;
auto constexpr eboLength = 6;
auto const [x, y] = addr;
auto const i = static_cast<size_t>(m_subsheetTilesWidth * y + x);
auto const vbo = &m_bufferSet.vertices[i * vboLength];
auto const ebo = &m_bufferSet.elements[i * eboLength];
setPixelBufferObject(
static_cast<uint_t>(i * 4),
static_cast<float>(x),
static_cast<float>(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<float>(ncore::TileWidth) / 240.f * 4;
auto constexpr ymod = static_cast<float>(ncore::TileHeight) / 160.f * 4;
x *= xmod;
y *= -ymod;
x -= 1.0f;
y += 1.0f - ymod;
auto const selection = 1.f * static_cast<float>(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<GLuint, 6> 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<size_t>(s_programSrc.rowLen) * 4;
auto constexpr eboLength = 6;
auto const tileCnt =
static_cast<size_t>(m_subsheetTilesWidth) * static_cast<size_t>(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);
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <glutils/glutils.hpp>
#include <studio/context.hpp>
#include <nostalgia/core/core.hpp>
#include <jasper/world/worldobject.hpp>
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;
};
}

View File

@ -4,6 +4,7 @@
#include <imgui.h>
#include <ox/std/conv.hpp>
#include <ox/std/ignore.hpp>
#include <keel/media.hpp>
@ -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<ox::Vec2>(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<WorldTileDragDrop>("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::Size>(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<int>(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<ox::Vec2>(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<WorldTileDragDrop>("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<float>(viewSz.width) * fbPaneScale,
static_cast<float>(viewSz.height) * fbPaneScale});
if (tileAddr.x < m_doc.columns && tileAddr.y < m_doc.rows) {
std::ignore = pushCommand<ModifyTilesCommand>(
m_doc,
m_worldStatic,
m_objCache,
ox::Vector<ModifyTilesCommand::Mod>{
{
.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<ModifyTilesCommand>(
m_doc,
m_worldStatic,
m_objCache,
ox::Vector<ModifyTilesCommand::Mod>{
{
.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<EditWorldSizeCommand const*>(cmd) != nullptr;
oxReturnError(m_view.setupWorld());
return {};
}

View File

@ -19,10 +19,7 @@ namespace jasper::world {
class WorldEditorImGui: public studio::Editor {
private:
struct Selection {
ox::Point begin, end;
};
ox::Optional<Selection> 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;

View File

@ -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<float>(ncore::TileWidth) / 240.f * 4;
auto constexpr ymod = static_cast<float>(ncore::TileHeight) / 160.f * 4;
x *= xmod;
y *= -ymod;
x -= 1.0f;
y += 1.0f - ymod;
auto const selection = 1.f * static_cast<float>(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<GLuint, 6> const elms{
vertexRow + 0, vertexRow + 1, vertexRow + 2,
vertexRow + 2, vertexRow + 3, vertexRow + 0,
};
memcpy(ebo, elms.data(), sizeof(elms));
}
}

View File

@ -6,10 +6,15 @@
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/context.hpp>
#include <nostalgia/core/gfx.hpp>
#include <jasper/world/world.hpp>
#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<studio::Selection> 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;
};
}