From abba35d64e696250b003c3d7d055396e366a6c44 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 20 May 2024 02:07:10 -0500 Subject: [PATCH] [jasper/world/studio] Add tile placement in world editing --- .../world/include/jasper/world/worlddoc.hpp | 22 ++++- .../include/jasper/world/worldobject.hpp | 1 + .../include/jasper/world/worldstatic.hpp | 6 +- .../src/studio/worldeditor/CMakeLists.txt | 1 + .../studio/worldeditor/commands/commands.hpp | 1 + .../worldeditor/commands/modifytiles.cpp | 42 +++++++++ .../worldeditor/commands/modifytiles.hpp | 40 +++++++++ .../studio/worldeditor/worldeditor-imgui.cpp | 89 +++++++++++++++---- .../studio/worldeditor/worldeditor-imgui.hpp | 5 ++ .../studio/worldeditor/worldeditorview.cpp | 5 ++ .../studio/worldeditor/worldeditorview.hpp | 6 ++ src/jasper/modules/world/src/worldstatic.cpp | 2 +- 12 files changed, 197 insertions(+), 23 deletions(-) create mode 100644 src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.cpp create mode 100644 src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.hpp diff --git a/src/jasper/modules/world/include/jasper/world/worlddoc.hpp b/src/jasper/modules/world/include/jasper/world/worlddoc.hpp index acdeb89..4b518c1 100644 --- a/src/jasper/modules/world/include/jasper/world/worlddoc.hpp +++ b/src/jasper/modules/world/include/jasper/world/worlddoc.hpp @@ -87,6 +87,24 @@ struct WorldDoc { TileMap tiles; }; +[[nodiscard]] +constexpr ox::Result 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 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 auto &doc, size_t lyr, size_t col, size_t row) noexcept { - return doc.tiles[lyr][row][col]; +auto &tile(ox::CommonRefWith auto &doc, size_t lyr, ox::Integer_c auto col, ox::Integer_c auto row) noexcept { + return doc.tiles[lyr][static_cast(row)][static_cast(col)]; } void resize(WorldDoc &doc, ox::Size const&sz) noexcept; diff --git a/src/jasper/modules/world/include/jasper/world/worldobject.hpp b/src/jasper/modules/world/include/jasper/world/worldobject.hpp index b215a6e..0148d02 100644 --- a/src/jasper/modules/world/include/jasper/world/worldobject.hpp +++ b/src/jasper/modules/world/include/jasper/world/worldobject.hpp @@ -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 palettes; diff --git a/src/jasper/modules/world/include/jasper/world/worldstatic.hpp b/src/jasper/modules/world/include/jasper/world/worldstatic.hpp index 9799b01..b4a44a6 100644 --- a/src/jasper/modules/world/include/jasper/world/worldstatic.hpp +++ b/src/jasper/modules/world/include/jasper/world/worldstatic.hpp @@ -105,11 +105,13 @@ oxModelBegin(WorldStatic) oxModelField(map) oxModelEnd() +void loadTile(ObjectCache const&objCache, TileStatic &dst, TileDoc const&src) noexcept; + ox::Result loadWorldStatic(ObjectCache const&objCache, WorldDoc const&doc) noexcept; [[nodiscard]] -auto &tile(ox::CommonRefWith auto &ws, size_t lyr, size_t col, size_t row) noexcept { - auto const idx = row * static_cast(ws.columns) + col; +auto &tile(ox::CommonRefWith auto &ws, size_t lyr, ox::Integer_c auto col, ox::Integer_c auto row) noexcept { + auto const idx = static_cast(row) * static_cast(ws.columns) + static_cast(col); return ws.map[lyr].tiles[idx]; } diff --git a/src/jasper/modules/world/src/studio/worldeditor/CMakeLists.txt b/src/jasper/modules/world/src/studio/worldeditor/CMakeLists.txt index 2d54841..8367489 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/CMakeLists.txt +++ b/src/jasper/modules/world/src/studio/worldeditor/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources( JasperWorld-Studio PRIVATE commands/addrmobjectset.cpp + commands/modifytiles.cpp commands/editsize.cpp objectexplorer.cpp worldeditor-imgui.cpp diff --git a/src/jasper/modules/world/src/studio/worldeditor/commands/commands.hpp b/src/jasper/modules/world/src/studio/worldeditor/commands/commands.hpp index 96acb75..80292e7 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/commands/commands.hpp +++ b/src/jasper/modules/world/src/studio/worldeditor/commands/commands.hpp @@ -11,6 +11,7 @@ enum class WorldEditorCommand { AddObjectSet, RmObjectSet, EditWorldSize, + ModifyTiles, }; } diff --git a/src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.cpp b/src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.cpp new file mode 100644 index 0000000..91ee67a --- /dev/null +++ b/src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.cpp @@ -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 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(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); + } +} + +} \ No newline at end of file diff --git a/src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.hpp b/src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.hpp new file mode 100644 index 0000000..3f7b940 --- /dev/null +++ b/src/jasper/modules/world/src/studio/worldeditor/commands/modifytiles.hpp @@ -0,0 +1,40 @@ +/* + * Copyright 2023 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +#include + +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 m_mods; + public: + ModifyTilesCommand( + WorldDoc &doc, + WorldStatic &worldStatic, + ObjectCache &objCache, + ox::Vector mods); + void redo() noexcept override; + void undo() noexcept override; + [[nodiscard]] + int commandId() const noexcept override; + private: + void swap() 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 3b48ec8..ec29b51 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.cpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.cpp @@ -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(fbSz.width); + auto const fbh = static_cast(fbSz.height); + auto const mw = static_cast(mapSz.width); + auto const mh = static_cast(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(sw * fbx), + static_cast(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("WorldTile")); - oxDebugf("{}: {}", obj.setPath, obj.objName); + std::ignore = ig::dragDropTarget([&, this] { + oxRequire(objId, ig::getDragDropPayload("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( + 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{}; }); } @@ -203,9 +251,14 @@ ox::Error WorldEditorImGui::loadObjectSets() noexcept { m_objSets.clear(); for (auto const&set : m_doc.objSets) { oxRequireM(os, readObj(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 {}; +} + } 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 248987c..1c15852 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.hpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditor-imgui.hpp @@ -4,6 +4,8 @@ #pragma once +#include + #include #include @@ -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 set; }; ox::Vector m_objSets; @@ -63,6 +67,7 @@ class WorldEditorImGui: public studio::Editor { ox::Error loadObjectSets() noexcept; + ox::Error undoStackChanged(studio::UndoCommand const*); }; } diff --git a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp index 2f65780..b09eedb 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.cpp @@ -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); +} + } diff --git a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp index cf8eecf..1bb4f35 100644 --- a/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp +++ b/src/jasper/modules/world/src/studio/worldeditor/worldeditorview.hpp @@ -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; + }; } diff --git a/src/jasper/modules/world/src/worldstatic.cpp b/src/jasper/modules/world/src/worldstatic.cpp index 8146723..13ce27a 100644 --- a/src/jasper/modules/world/src/worldstatic.cpp +++ b/src/jasper/modules/world/src/worldstatic.cpp @@ -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(obj.tileIdx);