[jasper/world/studio] Add tile placement in world editing
All checks were successful
Build / build (push) Successful in 3m11s

This commit is contained in:
Gary Talent 2024-05-20 02:07:10 -05:00
parent 3d2b3da7f5
commit abba35d64e
12 changed files with 197 additions and 23 deletions

View File

@ -87,6 +87,24 @@ struct WorldDoc {
TileMap tiles;
};
[[nodiscard]]
constexpr ox::Result<ox::String> objectSetPath(WorldDoc const&wd, uint64_t setId) noexcept {
auto obj = ox::find_if(wd.objSets.begin(), wd.objSets.end(), [setId](ObjectSetEntry const&e) {
return e.id == setId;
});
oxReturnError(OxError(obj == wd.objSets.end(), "Obj set not found"));
return obj->path;
}
[[nodiscard]]
constexpr ox::Result<uint64_t> objectSetId(WorldDoc const&wd, ox::StringView setPath) noexcept {
auto obj = ox::find_if(wd.objSets.begin(), wd.objSets.end(), [setPath](ObjectSetEntry const&e) {
return e.path == setPath;
});
oxReturnError(OxError(obj == wd.objSets.end(), "Obj set not found"));
return obj->id;
}
oxModelBegin(WorldDoc)
oxModelFieldRename(objSets, object_sets)
oxModelFieldRename(objSetIdIdx, object_set_id_idx)
@ -99,8 +117,8 @@ oxModelEnd()
ObjectSetEntry const*objSetEntry(WorldDoc const&doc, size_t id) noexcept;
[[nodiscard]]
auto &tile(ox::CommonRefWith<WorldDoc> auto &doc, size_t lyr, size_t col, size_t row) noexcept {
return doc.tiles[lyr][row][col];
auto &tile(ox::CommonRefWith<WorldDoc> auto &doc, size_t lyr, ox::Integer_c auto col, ox::Integer_c auto row) noexcept {
return doc.tiles[lyr][static_cast<size_t>(row)][static_cast<size_t>(col)];
}
void resize(WorldDoc &doc, ox::Size const&sz) noexcept;

View File

@ -77,6 +77,7 @@ struct WorldObjectSet {
static constexpr auto TypeName = "net.drinkingtea.jasper.world.WorldObjectSet";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
// index used to assign unique IDs to objects in the set
ObjectId objIdIdx = 0;
ox::FileAddress tilesheet;
ox::Vector<PaletteCycle> palettes;

View File

@ -105,11 +105,13 @@ oxModelBegin(WorldStatic)
oxModelField(map)
oxModelEnd()
void loadTile(ObjectCache const&objCache, TileStatic &dst, TileDoc const&src) noexcept;
ox::Result<WorldStatic> loadWorldStatic(ObjectCache const&objCache, WorldDoc const&doc) noexcept;
[[nodiscard]]
auto &tile(ox::CommonRefWith<WorldStatic> auto &ws, size_t lyr, size_t col, size_t row) noexcept {
auto const idx = row * static_cast<size_t>(ws.columns) + col;
auto &tile(ox::CommonRefWith<WorldStatic> auto &ws, size_t lyr, ox::Integer_c auto col, ox::Integer_c auto row) noexcept {
auto const idx = static_cast<size_t>(row) * static_cast<size_t>(ws.columns) + static_cast<size_t>(col);
return ws.map[lyr].tiles[idx];
}

View File

@ -1,6 +1,7 @@
target_sources(
JasperWorld-Studio PRIVATE
commands/addrmobjectset.cpp
commands/modifytiles.cpp
commands/editsize.cpp
objectexplorer.cpp
worldeditor-imgui.cpp

View File

@ -11,6 +11,7 @@ enum class WorldEditorCommand {
AddObjectSet,
RmObjectSet,
EditWorldSize,
ModifyTiles,
};
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "modifytiles.hpp"
namespace jasper::world {
ModifyTilesCommand::ModifyTilesCommand(WorldDoc &doc,
WorldStatic &worldStatic,
ObjectCache &objCache,
ox::Vector<Mod> mods):
m_doc(doc),
m_worldStatic(worldStatic),
m_objCache(objCache),
m_mods(std::move(mods)) {
}
void ModifyTilesCommand::redo() noexcept {
swap();
}
void ModifyTilesCommand::undo() noexcept {
swap();
}
int ModifyTilesCommand::commandId() const noexcept {
return static_cast<int>(WorldEditorCommand::ModifyTiles);
}
void ModifyTilesCommand::swap() noexcept {
for (auto &mod : m_mods) {
auto &docTile = tile(m_doc, mod.layer, mod.tileAddr.x, mod.tileAddr.y);
auto &activeTile = tile(m_worldStatic, mod.layer, mod.tileAddr.x, mod.tileAddr.y);
std::swap(docTile.obj.worldObjectId, mod.objId);
std::swap(docTile.obj.worldObjectSetId, mod.setId);
loadTile(m_objCache, activeTile, docTile);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/undostack.hpp>
#include <jasper/world/world.hpp>
namespace jasper::world {
class ModifyTilesCommand: public studio::UndoCommand {
public:
struct Mod {
uint8_t layer{};
ox::Point tileAddr;
ObjectId objId{};
uint64_t setId{};
};
private:
WorldDoc &m_doc;
WorldStatic &m_worldStatic;
ObjectCache &m_objCache;
ox::Vector<Mod> m_mods;
public:
ModifyTilesCommand(
WorldDoc &doc,
WorldStatic &worldStatic,
ObjectCache &objCache,
ox::Vector<Mod> mods);
void redo() noexcept override;
void undo() noexcept override;
[[nodiscard]]
int commandId() const noexcept override;
private:
void swap() noexcept;
};
}

View File

@ -12,6 +12,7 @@
#include "commands/addrmobjectset.hpp"
#include "commands/editsize.hpp"
#include "commands/modifytiles.hpp"
#include "worldeditor-imgui.hpp"
@ -25,13 +26,13 @@ constexpr auto SqrBtnSize = ImVec2(ig::BtnSz.y, ig::BtnSz.y);
struct WorldTileDragDrop {
static constexpr auto TypeName = "net.drinkingtea.jasper.world.studio.WorldTileDragDrop";
static constexpr auto TypeVersion = 1;
ox::String setPath;
ox::String objName;
uint64_t setId{};
ObjectId objId{};
};
oxModelBegin(WorldTileDragDrop)
oxModelField(setPath)
oxModelField(objName)
oxModelField(setId)
oxModelField(objId)
oxModelEnd()
@ -45,6 +46,38 @@ static WorldDoc makeValid(WorldDoc doc) noexcept {
return doc;
}
[[nodiscard]]
constexpr ox::Point fbPtToTileAddr(
ox::Vec2 const&fbPt,
ox::Size const&fbSz,
ox::Size const&mapSz) noexcept {
// fbw - framebuffer width
// fbh - framebuffer height
// fbx - framebuffer pt x
// fby - framebuffer pt y
// sw - scaled width
// sh - scaled height
// mw - map width
// mw - map height
// mx - map loc x
// my - map loc y
// sw = fbw / mw
// sh = fbh / mh
// tile addr x = sw * fbx
// tile addr y = sh * fby
auto const fbw = static_cast<float>(fbSz.width);
auto const fbh = static_cast<float>(fbSz.height);
auto const mw = static_cast<float>(mapSz.width);
auto const mh = static_cast<float>(mapSz.height);
auto const sw = fbw / mw;
auto const sh = fbh / mh;
auto const fbx = fbPt.x / fbw;
auto const fby = fbPt.y / fbh;
return {
static_cast<int>(sw * fbx),
static_cast<int>(sh * fby),
};
}
WorldEditorImGui::WorldEditorImGui(studio::StudioContext &sctx, ox::StringView path):
Editor(path),
@ -57,6 +90,7 @@ WorldEditorImGui::WorldEditorImGui(studio::StudioContext &sctx, ox::StringView p
setRequiresConstantRefresh(false);
m_objSetPicker.filePicked.connect(this, &WorldEditorImGui::addObjSet);
m_sctx.project->fileUpdated.connect(this, &WorldEditorImGui::handleObjectSetUpdate);
studio::Editor::undoStack()->changeTriggered.connect(this, &WorldEditorImGui::undoStackChanged);
}
void WorldEditorImGui::draw(studio::StudioContext&) noexcept {
@ -100,17 +134,18 @@ void WorldEditorImGui::drawObjSetSelector() noexcept {
void WorldEditorImGui::drawObjSelector() noexcept {
ig::IDStackItem const idStackItem("ObjSelector");
for (auto const&set : m_objSets) {
for (auto const&[setName, setId, set] : m_objSets) {
constexpr auto flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (ImGui::TreeNodeEx(set.name.c_str(), flags)) {
for (auto const&obj : set.set->objects) {
if (ImGui::TreeNodeEx(setName.c_str(), flags)) {
for (auto const&obj : set->objects) {
if (ImGui::TreeNodeEx(obj.name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
ImGui::TreePop();
}
ig::dragDropSource([set, obj] {
std::ignore = ig::setDragDropPayload("WorldTile", WorldTileDragDrop{
.setPath = ox::String(set.name),
.objName = ox::String(obj.name)});
ig::dragDropSource([&] {
std::ignore = ig::setDragDropPayload("WorldTile", WorldTileDragDrop{
.setId = setId,
.objId = obj.id,
});
ImGui::Text("%s", obj.name.c_str());
});
}
@ -170,13 +205,26 @@ void WorldEditorImGui::drawWorldView() noexcept {
paneSize,
ImVec2(0, 1),
ImVec2(xScale, 1 - yScale));
std::ignore = ig::dragDropTarget([] {
oxRequire(obj, ig::getDragDropPayload<WorldTileDragDrop>("WorldTile"));
oxDebugf("{}: {}", obj.setPath, obj.objName);
std::ignore = ig::dragDropTarget([&, this] {
oxRequire(objId, ig::getDragDropPayload<WorldTileDragDrop>("WorldTile"));
auto const&io = ImGui::GetIO();
auto const mousePos = ox::Vec2(io.MousePos);
auto const dropPos = world::dropPos(mousePos);
std::cout << dropPos.x << ", " << dropPos.y << '\n';
auto const dropPos = world::dropPos(ox::Vec2(io.MousePos));
auto const mw = m_doc.columns * ncore::TileWidth * 2;
auto const mh = m_doc.rows * ncore::TileHeight * 2;
auto const tileAddr = fbPtToTileAddr(dropPos, fb.size(), {mw, mh});
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{};
});
}
@ -203,9 +251,14 @@ ox::Error WorldEditorImGui::loadObjectSets() noexcept {
m_objSets.clear();
for (auto const&set : m_doc.objSets) {
oxRequireM(os, readObj<WorldObjectSet>(keelCtx(m_sctx.tctx), set.path));
m_objSets.emplace_back(ObjSetRef{ox::String(set.path), std::move(os)});
m_objSets.emplace_back(ox::String(set.path), set.id, std::move(os));
}
return {};
}
ox::Error WorldEditorImGui::undoStackChanged(const studio::UndoCommand *) {
oxReturnError(m_view.setupWorld());
return {};
}
}

View File

@ -4,6 +4,8 @@
#pragma once
#include <ox/std/smallmap.hpp>
#include <studio/studio.hpp>
#include <turbine/context.hpp>
@ -17,6 +19,7 @@ namespace jasper::world {
class WorldEditorImGui: public studio::Editor {
private:
uint8_t m_activeLayer{};
studio::StudioContext &m_sctx;
studio::ig::FilePicker m_objSetPicker{
m_sctx, ox::String("Choose Object Set"), ox::String(FileExt_jwob)};
@ -24,6 +27,7 @@ class WorldEditorImGui: public studio::Editor {
ObjectCache m_objCache;
struct ObjSetRef {
ox::String name;
uint64_t id{};
keel::AssetRef<WorldObjectSet> set;
};
ox::Vector<ObjSetRef> m_objSets;
@ -63,6 +67,7 @@ class WorldEditorImGui: public studio::Editor {
ox::Error loadObjectSets() noexcept;
ox::Error undoStackChanged(studio::UndoCommand const*);
};
}

View File

@ -23,6 +23,7 @@ void WorldEditorView::draw(ox::Size const&targetSz) noexcept {
auto const scaleSz = targetSz / ncore::gl::drawSize(1);
if (m_scaleSz != scaleSz) [[unlikely]] {
m_scale = ox::max(1, ox::max(scaleSz.width, scaleSz.height));
m_scaleSz = ncore::gl::drawSize(m_scale);
glutils::resizeInitFrameBuffer(m_frameBuffer, ncore::gl::drawSize(m_scale));
}
glutils::FrameBufferBind const frameBufferBind(m_frameBuffer);
@ -33,4 +34,8 @@ glutils::FrameBuffer const&WorldEditorView::framebuffer() const noexcept {
return m_frameBuffer;
}
ox::Size WorldEditorView::drawSize() const noexcept {
return ncore::gl::drawSize(m_scale);
}
}

View File

@ -34,6 +34,12 @@ class WorldEditorView {
[[nodiscard]]
glutils::FrameBuffer const&framebuffer() const noexcept;
[[nodiscard]]
constexpr int scale() const noexcept { return m_scale; }
[[nodiscard]]
ox::Size drawSize() const noexcept;
};
}

View File

@ -8,7 +8,7 @@
namespace jasper::world {
static void loadTile(ObjectCache const&objCache, TileStatic &dst, TileDoc const&src) noexcept {
void loadTile(ObjectCache const&objCache, TileStatic &dst, TileDoc const&src) noexcept {
auto const obj = objCache.obj(src.obj.worldObjectSetId, src.obj.worldObjectId).or_value({});
dst.palBank = src.palBank;
dst.tileIdx = static_cast<uint16_t>(obj.tileIdx);