[jasper/world/studio] Add map tile selection
This commit is contained in:
parent
9aec5772f4
commit
ee86436d58
@ -1,5 +1,6 @@
|
||||
add_library(
|
||||
JasperWorld-Studio
|
||||
maptilehighlighter.cpp
|
||||
studiomodule.cpp
|
||||
worldeditor/commands/commands.hpp
|
||||
)
|
||||
|
143
src/jasper/modules/world/src/studio/maptilehighlighter.cpp
Normal file
143
src/jasper/modules/world/src/studio/maptilehighlighter.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
50
src/jasper/modules/world/src/studio/maptilehighlighter.hpp
Normal file
50
src/jasper/modules/world/src/studio/maptilehighlighter.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
@ -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 {};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user