[jasper/world/studio] Add tile placement in world editing
All checks were successful
Build / build (push) Successful in 3m11s
All checks were successful
Build / build (push) Successful in 3m11s
This commit is contained in:
parent
3d2b3da7f5
commit
abba35d64e
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
target_sources(
|
||||
JasperWorld-Studio PRIVATE
|
||||
commands/addrmobjectset.cpp
|
||||
commands/modifytiles.cpp
|
||||
commands/editsize.cpp
|
||||
objectexplorer.cpp
|
||||
worldeditor-imgui.cpp
|
||||
|
@ -11,6 +11,7 @@ enum class WorldEditorCommand {
|
||||
AddObjectSet,
|
||||
RmObjectSet,
|
||||
EditWorldSize,
|
||||
ModifyTiles,
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
@ -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 {};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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*);
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user