Merge commit 'c42adc290cd8a27d01bb6d9877032dd2c963a4b7'

This commit is contained in:
Gary Talent 2025-02-01 22:55:46 -06:00
commit 8459d3baea
35 changed files with 537 additions and 78 deletions

View File

@ -185,8 +185,7 @@ Error PassThroughFS::writeFileInode(uint64_t, const void*, uint64_t, FileType) n
}
std::string_view PassThroughFS::stripSlash(StringView path) noexcept {
const auto pathLen = ox::strlen(path);
for (auto i = 0u; i < pathLen && path[0] == '/'; i++) {
for (auto i = 0u; i < path.len() && path[0] == '/'; i++) {
path = substr(path, 1);
}
return {path.data(), path.bytes()};

View File

@ -4,6 +4,8 @@
* Add PaletteV5 to accommodate namespace change.
* Add TileSheetV5. TileSheetV5 retains the bpp field for the sake of
CompactTileSheet, but always store it pixel as 8 bpp for itself.
* Add ability to move subsheets in the subsheet tree.
* Add Flip X and Flip Y button for TileSheet Editor.
* Replace file picker combo boxes with a browse button and file picker, and
support for dragging files from the project explorer.
* Add ability to create directories.
@ -12,3 +14,4 @@
* Ctrl-<num key> keyboard shortcuts for jumping between tabs.
* Fix Palette Editor to ignore keyboard input when popups are open.
* Palette Editor move color mechanism now uses drag and drop.
* Add ability to reorder Palette pages.

View File

@ -1 +1,28 @@
K1;0f75977f-1c52-45f8-9793-52ea2dc200a0;M2;net.drinkingtea.nostalgia.core.Palette;1;ûÿ³Ö
K1;0f75977f-1c52-45f8-9793-52ea2dc200a0;O1;net.drinkingtea.nostalgia.gfx.Palette;5;{
"colorNames" :
[
"Color 1",
"Color 2"
],
"pages" :
[
{
"colors" :
[
{
"a" : 1,
"b" : 31,
"g" : 31,
"r" : 31
},
{
"a" : 1,
"b" : 22,
"g" : 22,
"r" : 22
}
],
"name" : "Page 1"
}
]
}

View File

@ -1 +1,36 @@
K1;c79f21e2-f74f-4ad9-90ed-32b0ef7da6ed;M2;net.drinkingtea.nostalgia.core.Palette;1;PÛ{³ÖCˆ
K1;c79f21e2-f74f-4ad9-90ed-32b0ef7da6ed;O1;net.drinkingtea.nostalgia.gfx.Palette;5;{
"colorNames" :
[
"Color 1",
"Color 2",
"Color 3",
"Color 4"
],
"pages" :
[
{
"colors" :
[
{
"b" : 5
},
{
"b" : 22,
"g" : 22,
"r" : 22
},
{
"b" : 27,
"g" : 27,
"r" : 27
},
{
"b" : 20,
"g" : 8,
"r" : 8
}
],
"name" : "Page 1"
}
]
}

Binary file not shown.

View File

@ -289,7 +289,7 @@ constexpr ox::Error repair(TileSheetV4 &ts) noexcept {
struct TileSheetV5 {
using SubSheetIdx = ox::Vector<std::size_t, 4>;
using SubSheetIdx = ox::Vector<uint32_t, 4>;
struct SubSheet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.gfx.TileSheet.SubSheet";
@ -445,23 +445,25 @@ TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubShe
[[nodiscard]]
TileSheet::SubSheet const&getSubSheet(
TileSheet::SubSheetIdx const&idx,
ox::SpanView<uint32_t> const&idx,
std::size_t idxIt,
TileSheet::SubSheet const&pSubsheet) noexcept;
[[nodiscard]]
TileSheet::SubSheet &getSubSheet(
TileSheet::SubSheetIdx const&idx,
ox::SpanView<uint32_t> const&idx,
std::size_t idxIt,
TileSheet::SubSheet &pSubsheet) noexcept;
[[nodiscard]]
TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, TileSheet::SubSheetIdx const&idx) noexcept;
TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, ox::SpanView<uint32_t> const &idx) noexcept;
[[nodiscard]]
TileSheet::SubSheet &getSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept;
TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const &idx) noexcept;
ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept;
ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const &idx) noexcept;
ox::Error insertSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const &idx, TileSheet::SubSheet ss) noexcept;
ox::Error rmSubSheet(
TileSheet &ts,

View File

@ -1,18 +1,6 @@
target_sources(
NostalgiaCore-Studio PRIVATE
commands/addcolorcommand.cpp
commands/addpagecommand.cpp
commands/applycolorallpagescommand.cpp
commands/duplicatepagecommand.cpp
commands/movecolorcommand.cpp
commands/removecolorcommand.cpp
commands/removepagecommand.cpp
commands/renamepagecommand.cpp
commands/updatecolorcommand.cpp
commands/updatecolorinfocommand.cpp
)
target_sources(
NostalgiaCore-Studio-ImGui PRIVATE
paletteeditor-imgui.cpp
)
add_subdirectory(commands)

View File

@ -0,0 +1,15 @@
target_sources(
NostalgiaCore-Studio PRIVATE
addcolorcommand.cpp
addpagecommand.cpp
applycolorallpagescommand.cpp
duplicatepagecommand.cpp
movecolorcommand.cpp
movepagecommand.cpp
removecolorcommand.cpp
removepagecommand.cpp
renamepagecommand.cpp
updatecolorcommand.cpp
updatecolorinfocommand.cpp
)

View File

@ -12,6 +12,7 @@ enum class PaletteEditorCommandId {
AddPage,
DuplicatePage,
RemovePage,
MovePage,
AddColor,
RemoveColor,
UpdateColorInfo,

View File

@ -0,0 +1,40 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "movepagecommand.hpp"
namespace nostalgia::gfx {
MovePageCommand::MovePageCommand(
Palette &pal,
size_t const srcIdx,
size_t const dstIdx) noexcept:
m_pal(pal),
m_srcIdx(srcIdx),
m_dstIdx(dstIdx) {}
int MovePageCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::MovePage);
}
ox::Error MovePageCommand::redo() noexcept {
movePage(m_srcIdx, m_dstIdx);
return {};
}
ox::Error MovePageCommand::undo() noexcept {
movePage(m_dstIdx, m_srcIdx);
return {};
}
void MovePageCommand::movePage(
size_t const srcIdx, size_t const dstIdx) noexcept {
auto page = std::move(m_pal.pages[srcIdx]);
std::ignore = m_pal.pages.erase(srcIdx);
m_pal.pages.emplace(dstIdx, std::move(page));
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class MovePageCommand: public studio::UndoCommand {
private:
Palette &m_pal;
std::size_t const m_srcIdx = 0;
std::size_t const m_dstIdx = 0;
public:
MovePageCommand(Palette &pal, size_t srcIdx, size_t dstIdx) noexcept;
~MovePageCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
ox::Error redo() noexcept override;
ox::Error undo() noexcept override;
private:
void movePage(size_t srcIdx, size_t dstIdx) noexcept;
};
}

View File

@ -11,6 +11,7 @@
#include "commands/applycolorallpagescommand.hpp"
#include "commands/duplicatepagecommand.hpp"
#include "commands/movecolorcommand.hpp"
#include "commands/movepagecommand.hpp"
#include "commands/removecolorcommand.hpp"
#include "commands/removepagecommand.hpp"
#include "commands/renamepagecommand.hpp"
@ -19,6 +20,7 @@
#include "paletteeditor-imgui.hpp"
namespace nostalgia::gfx {
namespace ig = studio::ig;
@ -33,6 +35,16 @@ OX_MODEL_BEGIN(ColorDragDrop)
OX_MODEL_FIELD(i)
OX_MODEL_END()
struct PageDragDrop {
static constexpr auto TypeName = "nostalgia.gfx.PageDragDrop";
static constexpr auto TypeVersion = 1;
uint32_t page{};
};
OX_MODEL_BEGIN(PageDragDrop)
OX_MODEL_FIELD(page)
OX_MODEL_END()
void PaletteEditorImGui::PageRenameDialog::draw(turbine::Context &tctx) noexcept {
if (!m_show) {
return;
@ -235,6 +247,16 @@ void PaletteEditorImGui::drawPagesEditor() noexcept {
if (ImGui::Selectable("##PageRow", i == m_page, ImGuiSelectableFlags_SpanAllColumns)) {
m_page = i;
}
std::ignore = ig::dragDropSource([this, i] {
ImGui::Text("%s", m_pal.pages[i].name.c_str());
return ig::setDragDropPayload(PageDragDrop{i});
}, ImGuiDragDropFlags_SourceAllowNullID);
if (ig::DragDropTarget const d; d) {
auto const [src, err] = ig::getDragDropPayload<PageDragDrop>();
if (!err) {
std::ignore = pushCommand<MovePageCommand>(m_pal, src.page, i);
}
}
}
}
ImGui::EndTable();

View File

@ -5,6 +5,7 @@ target_sources(
deletetilescommand.cpp
drawcommand.cpp
flipcommand.cpp
movesubsheetcommand.cpp
inserttilescommand.cpp
palettechangecommand.cpp
rmsubsheetcommand.cpp

View File

@ -15,9 +15,10 @@ enum class CommandId {
AddSubSheet,
RmSubSheet,
DeleteTile,
FlipXCommand,
FlipYCommand,
FlipX,
FlipY,
InsertTile,
MoveSubSheet,
UpdateSubSheet,
Cut,
Paste,

View File

@ -2,10 +2,63 @@
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <cmath>
#include "drawcommand.hpp"
namespace nostalgia::gfx {
constexpr void iterateLine(ox::Point const &a, ox::Point const &b, auto const &f) noexcept {
auto const rise = b.y - a.y;
auto const run = b.x - a.x;
auto const slope = static_cast<double>(rise) / static_cast<double>(run);
auto y = static_cast<double>(a.y);
auto const w = abs(run);
if (rise > 0) {
if (run > 0) {
auto constexpr xmod = 1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(ceil(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else if (run < 0) {
auto constexpr xmod = -1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(floor(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else {
f(a.x, a.y, rise);
}
} else if (rise < 0) {
if (run > 0) {
auto constexpr xmod = 1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(floor(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else if (run < 0) {
auto constexpr xmod = -1;
for (int32_t i = 0; i < w; ++i) {
auto const climb = static_cast<int32_t>(ceil(y + slope - y));
f(a.x + i * xmod, static_cast<int32_t>(y), climb * xmod);
y += slope * xmod;
}
} else {
f(a.x, a.y, rise - 1);
}
} else {
auto const xmod = run > 0 ? 1 : -1;
for (int32_t i = 0; i < w; ++i) {
f(a.x + i * xmod, static_cast<int32_t>(y), 1);
}
}
}
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
@ -21,7 +74,7 @@ DrawCommand::DrawCommand(
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Vector<std::size_t> const&idxList,
ox::SpanView<std::size_t> const&idxList,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)),
@ -32,11 +85,12 @@ DrawCommand::DrawCommand(
}
}
bool DrawCommand::append(std::size_t idx) noexcept {
bool DrawCommand::append(std::size_t const idx) noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
if (m_changes.back().value->idx != idx && getPixel(subsheet, idx) != m_palIdx) {
// duplicate entries are bad
auto existing = ox::find_if(m_changes.cbegin(), m_changes.cend(), [idx](auto const&c) {
auto existing = find_if(
m_changes.cbegin(), m_changes.cend(), [idx](auto const&c) {
return c.idx == idx;
});
if (existing == m_changes.cend()) {
@ -48,7 +102,7 @@ bool DrawCommand::append(std::size_t idx) noexcept {
return false;
}
bool DrawCommand::append(ox::Vector<std::size_t> const&idxList) noexcept {
bool DrawCommand::append(ox::SpanView<std::size_t> const&idxList) noexcept {
auto out = false;
for (auto idx : idxList) {
out = append(idx) || out;
@ -56,6 +110,26 @@ bool DrawCommand::append(ox::Vector<std::size_t> const&idxList) noexcept {
return out;
}
void DrawCommand::lineUpdate(ox::Point a, ox::Point b) noexcept {
std::ignore = undo();
m_changes.clear();
auto &ss = getSubSheet(m_img, m_subSheetIdx);
if (a.x < b.x) { ++b.x; }
if (a.y < b.y) { ++b.y; }
if (a.x > b.x) { --b.x; }
iterateLine(a, b, [this, &ss](int32_t const x, int32_t const y, int32_t const h) {
int const mod = h < 0 ? -1 : 1;
auto const range = h * mod;
for (int32_t i{}; i < range; ++i) {
auto const idx = ptToIdx(x, y + i * mod, ss.columns * TileWidth);
if (idx < ss.pixels.size()) {
m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(ss, idx));
}
}
});
std::ignore = redo();
}
ox::Error DrawCommand::redo() noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
for (auto const&c : m_changes) {

View File

@ -33,12 +33,14 @@ class DrawCommand: public TileSheetCommand {
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Vector<std::size_t> const&idxList,
ox::SpanView<std::size_t> const&idxList,
int palIdx) noexcept;
bool append(std::size_t idx) noexcept;
bool append(ox::Vector<std::size_t> const&idxList) noexcept;
bool append(ox::SpanView<std::size_t> const&idxList) noexcept;
void lineUpdate(ox::Point a, ox::Point b) noexcept;
ox::Error redo() noexcept final;
@ -52,4 +54,4 @@ class DrawCommand: public TileSheetCommand {
};
}
}

View File

@ -31,7 +31,7 @@ ox::Error FlipXCommand::undo() noexcept {
}
int FlipXCommand::commandId() const noexcept {
return static_cast<int>(CommandId::FlipXCommand);
return static_cast<int>(CommandId::FlipX);
}
TileSheet::SubSheetIdx const &FlipXCommand::subsheetIdx() const noexcept {
@ -62,7 +62,7 @@ ox::Error FlipYCommand::undo() noexcept {
}
int FlipYCommand::commandId() const noexcept {
return static_cast<int>(CommandId::FlipYCommand);
return static_cast<int>(CommandId::FlipY);
}
TileSheet::SubSheetIdx const &FlipYCommand::subsheetIdx() const noexcept {

View File

@ -0,0 +1,40 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "movesubsheetcommand.hpp"
namespace nostalgia::gfx {
MoveSubSheetCommand::MoveSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx src,
TileSheet::SubSheetIdx dst) noexcept:
m_img{img},
m_src{std::move(src)},
m_dst{std::move(dst)} {
}
ox::Error MoveSubSheetCommand::redo() noexcept {
m_active = &m_dst;
TileSheet::SubSheet ss = std::move(getSubSheet(m_img, m_src));
OX_RETURN_ERROR(rmSubSheet(m_img, m_src));
return insertSubSheet(m_img, m_dst, std::move(ss));
}
ox::Error MoveSubSheetCommand::undo() noexcept {
m_active = &m_src;
TileSheet::SubSheet ss = std::move(getSubSheet(m_img, m_dst));
OX_RETURN_ERROR(rmSubSheet(m_img, m_dst));
return insertSubSheet(m_img, m_src, std::move(ss));
}
int MoveSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::MoveSubSheet);
}
TileSheet::SubSheetIdx const&MoveSubSheetCommand::subsheetIdx() const noexcept {
return *m_active;
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class MoveSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_src;
TileSheet::SubSheetIdx m_dst;
TileSheet::SubSheetIdx *m_active = &m_dst;
public:
MoveSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx src, TileSheet::SubSheetIdx dst) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -15,6 +15,16 @@ namespace nostalgia::gfx {
namespace ig = studio::ig;
struct SubSheetRef {
static constexpr auto TypeName = "nostalgia.gfx.studio.SubSheetRef";
static constexpr auto TypeVersion = 1;
TileSheet::SubSheetIdx subsheet{};
};
OX_MODEL_BEGIN(SubSheetRef)
OX_MODEL_FIELD(subsheet)
OX_MODEL_END()
struct TileSheetEditorConfig {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.gfx.studio.TileSheetEditorConfig";
static constexpr auto TypeVersion = 1;
@ -142,6 +152,12 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key const key, bool const do
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key == turbine::Key::Alpha_E) {
m_tool = TileSheetTool::Line;
setCopyEnabled(false);
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key == turbine::Key::Alpha_S) {
m_tool = TileSheetTool::Select;
setCopyEnabled(true);
@ -204,7 +220,7 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
ImGui::BeginChild("Controls", {s_palViewWidth - 8, paneSize.y}, true);
{
auto const controlsSize = ImGui::GetContentRegionAvail();
ImGui::BeginChild("ToolBox", {s_palViewWidth - 24, 30}, true);
ImGui::BeginChild("ToolBox", {0, 32}, true);
{
auto const btnSz = ImVec2{45, 14};
if (ImGui::Selectable("Select", m_tool == TileSheetTool::Select, 0, btnSz)) {
@ -220,11 +236,19 @@ void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
m_tool = TileSheetTool::Fill;
m_model.clearSelection();
}
ImGui::SameLine();
if (ImGui::Selectable("Line", m_tool == TileSheetTool::Line, 0, btnSz)) {
m_tool = TileSheetTool::Line;
m_model.clearSelection();
}
//ImGui::SameLine();
//size_t i{};
//ig::ComboBox("##Operations", ox::Array<ox::CStringView, 1>{"Operations"}, i);
}
ImGui::EndChild();
ImGui::BeginChild("OperationsBox", {s_palViewWidth - 24, ig::BtnSz.y + 17}, true);
ImGui::BeginChild("OperationsBox", {0, 32}, true);
{
auto constexpr btnSz = ImVec2{55, ig::BtnSz.y};
auto constexpr btnSz = ImVec2{55, 16};
if (ig::PushButton("Flip X", btnSz)) {
oxLogError(m_model.flipX());
}
@ -309,6 +333,16 @@ void TileSheetEditorImGui::drawSubsheetSelector(
| (rowSelected ? ImGuiTreeNodeFlags_Selected : 0);
ImGui::TableNextColumn();
auto const open = ImGui::TreeNodeEx(lbl.c_str(), flags);
std::ignore = ig::dragDropSource([&subsheet, &path] {
ImGui::Text("%s", subsheet.name.c_str());
return ig::setDragDropPayload(SubSheetRef{path});
});
if (ig::DragDropTarget const dragDropTarget; dragDropTarget) {
auto const [ref, err] = ig::getDragDropPayload<SubSheetRef>();
if (!err) {
oxLogError(m_model.moveSubSheet(ref.subsheet, path));
}
}
ImGui::SameLine();
if (ImGui::IsItemClicked()) {
setActiveSubsheet(path);
@ -330,7 +364,7 @@ void TileSheetEditorImGui::drawSubsheetSelector(
ImGui::Text("--");
}
if (open) {
for (auto i = 0ul; auto &child : subsheet.subsheets) {
for (uint32_t i{}; auto &child : subsheet.subsheets) {
path.push_back(i);
ImGui::PushID(static_cast<int>(i));
ig::IndentStackItem const indentStackItem{-indentReduce};
@ -427,6 +461,9 @@ void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept {
case TileSheetTool::Fill:
m_view.clickFill(fbSize, clickPos(winPos, mousePos));
break;
case TileSheetTool::Line:
m_view.clickLine(fbSize, clickPos(winPos, mousePos));
break;
case TileSheetTool::Select:
m_view.clickSelect(fbSize, clickPos(winPos, mousePos));
break;

View File

@ -45,7 +45,7 @@ class TileSheetEditorImGui: public studio::Editor {
[[nodiscard]]
constexpr bool isOpen() const noexcept { return m_show; }
};
static constexpr float s_palViewWidth = 300;
static constexpr float s_palViewWidth = 335;
studio::StudioContext &m_sctx;
turbine::Context &m_tctx;
ox::Vector<ox::String> m_paletteList;
@ -58,6 +58,7 @@ class TileSheetEditorImGui: public studio::Editor {
ox::Vec2 m_prevMouseDownPos;
TileSheetTool m_tool = TileSheetTool::Draw;
bool m_palPathFocused{};
ox::Vector<ox::UPtr<studio::UndoCommand>, 1> m_deferredCmds;
public:
TileSheetEditorImGui(studio::StudioContext &sctx, ox::StringParam path);

View File

@ -11,18 +11,19 @@
#include <nostalgia/gfx/ptidxconv.hpp>
#include "commands/commands.hpp"
#include "commands/addsubsheetcommand.hpp"
#include "commands/commands.hpp"
#include "commands/cutpastecommand.hpp"
#include "commands/deletetilescommand.hpp"
#include "commands/drawcommand.hpp"
#include "commands/flipcommand.hpp"
#include "commands/inserttilescommand.hpp"
#include "commands/palettechangecommand.hpp"
#include "commands/rmsubsheetcommand.hpp"
#include "commands/updatesubsheetcommand.hpp"
#include "tilesheeteditormodel.hpp"
#include "commands/flipcommand.hpp"
#include "commands/movesubsheetcommand.hpp"
namespace nostalgia::gfx {
@ -139,11 +140,15 @@ size_t TileSheetEditorModel::palettePage() const noexcept {
return m_palettePage;
}
void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t const palIdx) noexcept {
void TileSheetEditorModel::drawCommand(ox::Point const &pt, std::size_t const palIdx) noexcept {
if (m_lastDrawUpdatePt == pt) {
return;
}
auto const &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
m_lastDrawUpdatePt = pt;
auto const idx = gfx::idx(activeSubSheet, pt);
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
@ -153,6 +158,26 @@ void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t const pal
}
}
void TileSheetEditorModel::drawLineCommand(ox::Point const &pt, std::size_t const palIdx) noexcept {
if (m_lastDrawUpdatePt == pt) {
return;
}
auto const &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
m_lastDrawUpdatePt = pt;
auto const idx = gfx::idx(activeSubSheet, pt);
if (m_ongoingDrawCommand) {
m_ongoingDrawCommand->lineUpdate(m_lineStartPt, pt);
m_updated = true;
} else if (getPixel(activeSubSheet, idx) != palIdx) {
std::ignore = pushCommand(ox::make<DrawCommand>(
m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
m_lineStartPt = pt;
}
}
void TileSheetEditorModel::endDrawCommand() noexcept {
m_ongoingDrawCommand = nullptr;
}
@ -303,6 +328,10 @@ ox::Error TileSheetEditorModel::flipY() noexcept {
return pushCommand(ox::make<FlipYCommand>(m_img, m_activeSubsSheetIdx, a, b));
}
ox::Error TileSheetEditorModel::moveSubSheet(TileSheet::SubSheetIdx src, TileSheet::SubSheetIdx dst) noexcept {
return pushCommand(ox::make<MoveSubSheetCommand>(m_img, std::move(src), std::move(dst)));
}
void TileSheetEditorModel::getFillPixels(
TileSheet::SubSheet const&activeSubSheet,
ox::Span<bool> pixels,

View File

@ -34,6 +34,8 @@ class TileSheetEditorModel: public ox::SignalHandler {
class DrawCommand *m_ongoingDrawCommand = nullptr;
studio::SelectionTracker m_selTracker;
ox::Optional<studio::Selection> m_selection;
ox::Point m_lineStartPt;
ox::Point m_lastDrawUpdatePt;
bool m_updated = false;
public:
@ -71,6 +73,8 @@ class TileSheetEditorModel: public ox::SignalHandler {
void drawCommand(ox::Point const&pt, std::size_t palIdx) noexcept;
void drawLineCommand(ox::Point const&pt, std::size_t palIdx) noexcept;
void endDrawCommand() noexcept;
void addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept;
@ -130,6 +134,8 @@ class TileSheetEditorModel: public ox::SignalHandler {
ox::Error flipY() noexcept;
ox::Error moveSubSheet(TileSheet::SubSheetIdx src, TileSheet::SubSheetIdx dst) noexcept;
private:
void getFillPixels(
TileSheet::SubSheet const&activeSubSheet,

View File

@ -11,7 +11,8 @@
namespace nostalgia::gfx {
TileSheetEditorView::TileSheetEditorView(studio::StudioContext &sctx, ox::StringView path, studio::UndoStack &undoStack):
TileSheetEditorView::TileSheetEditorView(
studio::StudioContext &sctx, ox::StringViewCR path, studio::UndoStack &undoStack):
m_model(sctx, path, undoStack),
m_pixelsDrawer(m_model) {
glBindVertexArray(0);
@ -76,6 +77,11 @@ void TileSheetEditorView::clickDraw(ox::Vec2 const&paneSize, ox::Vec2 const&clic
m_model.drawCommand(pt, m_palIdx);
}
void TileSheetEditorView::clickLine(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
m_model.drawLineCommand(pt, m_palIdx);
}
void TileSheetEditorView::clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
m_model.select(pt);
@ -90,6 +96,7 @@ void TileSheetEditorView::releaseMouseButton(TileSheetTool tool) noexcept {
switch (tool) {
case TileSheetTool::Draw:
case TileSheetTool::Fill:
case TileSheetTool::Line:
m_model.endDrawCommand();
break;
case TileSheetTool::Select:

View File

@ -23,6 +23,7 @@ enum class TileSheetTool {
Select,
Draw,
Fill,
Line,
};
[[nodiscard]]
@ -34,6 +35,8 @@ constexpr auto toString(TileSheetTool t) noexcept {
return "Draw";
case TileSheetTool::Fill:
return "Fill";
case TileSheetTool::Line:
return "Line";
case TileSheetTool::None:
return "None";
}
@ -53,7 +56,7 @@ class TileSheetEditorView: public ox::SignalHandler {
std::size_t m_palIdx = 0;
public:
TileSheetEditorView(studio::StudioContext &sctx, ox::StringView path, studio::UndoStack &undoStack);
TileSheetEditorView(studio::StudioContext &sctx, ox::StringViewCR path, studio::UndoStack &undoStack);
~TileSheetEditorView() override = default;
@ -65,6 +68,8 @@ class TileSheetEditorView: public ox::SignalHandler {
void clickDraw(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickLine(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickFill(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;

View File

@ -177,7 +177,7 @@ TileSheet::SubSheetIdx validateSubSheetIdx(
pIdx.resize(pIdxIt);
return std::move(pIdx);
} else {
currentIdx = pSubsheet.subsheets.size() - 1;
currentIdx = static_cast<uint32_t>(pSubsheet.subsheets.size() - 1);
}
}
return validateSubSheetIdx(std::move(pIdx), pIdxIt + 1, pSubsheet.subsheets[currentIdx]);
@ -188,9 +188,9 @@ TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubShe
}
TileSheet::SubSheet const&getSubSheet(
TileSheet::SubSheetIdx const&idx,
ox::SpanView<uint32_t> const &idx,
std::size_t const idxIt,
TileSheet::SubSheet const&pSubsheet) noexcept {
TileSheet::SubSheet const &pSubsheet) noexcept {
if (idxIt == idx.size()) {
return pSubsheet;
}
@ -202,7 +202,7 @@ TileSheet::SubSheet const&getSubSheet(
}
TileSheet::SubSheet &getSubSheet(
TileSheet::SubSheetIdx const&idx,
ox::SpanView<uint32_t> const &idx,
std::size_t const idxIt,
TileSheet::SubSheet &pSubsheet) noexcept {
if (idxIt == idx.size()) {
@ -211,11 +211,11 @@ TileSheet::SubSheet &getSubSheet(
return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[idx[idxIt]]);
}
TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, TileSheet::SubSheetIdx const&idx) noexcept {
TileSheet::SubSheet const&getSubSheet(TileSheet const &ts, ox::SpanView<uint32_t> const &idx) noexcept {
return gfx::getSubSheet(idx, 0, ts.subsheet);
}
TileSheet::SubSheet &getSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept {
TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const&idx) noexcept {
return gfx::getSubSheet(idx, 0, ts.subsheet);
}
@ -227,7 +227,20 @@ ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept
parent.subsheets.emplace_back(++ts.idIt, "Subsheet 0", parent.columns, parent.rows);
parent.subsheets.emplace_back(++ts.idIt, "Subsheet 1", 1, 1);
}
return ox::Error(0);
return {};
}
ox::Error insertSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const&idx, TileSheet::SubSheet ss) noexcept {
if (idx.empty()) {
return ox::Error{1, "invalid insert idx"};
}
auto &parent = getSubSheet(ts, {idx.data(), idx.size() - 1});
auto const insertIdx = idx[idx.size() - 1];
if (insertIdx > parent.subsheets.size()) {
return ox::Error{1, "invalid insert idx"};
}
parent.subsheets.emplace(insertIdx, std::move(ss));
return {};
}
ox::Error rmSubSheet(

View File

@ -295,7 +295,9 @@ class AssetManager {
}
ox::Error reloadAsset(ox::StringViewCR assetId) noexcept {
m_fileUpdated[assetId]->emit(assetId);
if (m_fileUpdated.contains(assetId)) {
m_fileUpdated[assetId]->emit(assetId);
}
return {};
}

View File

@ -47,17 +47,17 @@ void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept {
}
void ProjectExplorer::dirContextMenu(ox::StringViewCR path) const noexcept {
if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Add Item")) {
addItem.emit(path);
}
if (ImGui::MenuItem("Add Directory")) {
addDir.emit(path);
}
if (ImGui::MenuItem("Delete")) {
if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Add Item")) {
addItem.emit(path);
}
if (ImGui::MenuItem("Add Directory")) {
addDir.emit(path);
}
if (path.len() && ImGui::MenuItem("Delete")) {
deleteItem.emit(path);
}
ImGui::EndPopup();
ImGui::EndPopup();
}
}

View File

@ -41,18 +41,35 @@ void RenameFile::draw(StudioContext &ctx) noexcept {
case Stage::Opening:
ImGui::OpenPopup(title().c_str());
m_open = true;
m_fileExists = false;
m_stage = Stage::Open;
[[fallthrough]];
case Stage::Open:
setSize({250, 0});
drawWindow(ctx.tctx, m_open, [this] {
drawWindow(ctx.tctx, m_open, [this, &ctx] {
if (ImGui::IsWindowAppearing()) {
ImGui::SetKeyboardFocusHere();
}
ig::InputText("Name", m_name);
ImGui::Text("%s%s", m_path.c_str(), m_name.c_str());
if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) {
moveFile.emit(m_oldPath, m_path + m_name);
m_fileExists = !ig::InputText("Name", m_name) && m_fileExists;
auto const nameInputFocused = ImGui::IsItemFocused();
if (m_fileExists) {
ImGui::Text("File %s already exists.", m_name.c_str());
} else {
ImGui::Text("%s%s", m_path.c_str(), m_name.c_str());
}
bool b{};
auto const response = ig::PopupControlsOkCancel(b);
if (response == ig::PopupResponse::OK ||
(nameInputFocused && ImGui::IsKeyPressed(ImGuiKey_Enter))) {
auto const newPath = m_path + m_name;
if (!ctx.project->exists(newPath)) {
moveFile.emit(m_oldPath, newPath);
close();
} else {
m_open = true;
m_fileExists = true;
}
} else if (response == ig::PopupResponse::Cancel) {
close();
}
});

View File

@ -21,6 +21,7 @@ class RenameFile: public Popup {
ox::String m_oldPath;
ox::String m_path;
ox::IString<255> m_name;
bool m_fileExists{};
bool m_open{};
public:

View File

@ -349,10 +349,6 @@ void StudioUI::handleKeyInput() noexcept {
m_activeEditorUpdatePending = m_activeEditor;
}
}
if (ImGui::IsKeyPressed(ImGuiKey_0)) {
m_activeEditor = m_editors[10 < m_editors.size() ? 10 : range].get();
m_activeEditorUpdatePending = m_activeEditor;
}
}
}
}
@ -396,6 +392,18 @@ ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR ne
return m_projectExplorer.refreshProjectTreeModel();
}
ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept {
for (size_t i{}; auto &e : m_editors) {
if (path == e->itemPath()) {
oxLogError(m_editors.erase(i).error);
oxLogError(closeFile(path));
break;
}
++i;
}
return m_projectExplorer.refreshProjectTreeModel();
}
ox::Error StudioUI::createOpenProject(ox::StringViewCR path) noexcept {
std::error_code ec;
std::filesystem::create_directories(toStdStringView(path), ec);
@ -416,7 +424,7 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir);
m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_project->fileDeleted.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_project->fileDeleted.connect(this, &StudioUI::handleDeleteFile);
m_project->fileMoved.connect(this, &StudioUI::handleMoveFile);
m_openFiles.clear();
m_editors.clear();
@ -499,11 +507,15 @@ ox::Error StudioUI::queueFileMove(ox::StringParam src, ox::StringParam dst) noex
void StudioUI::procFileMoves() noexcept {
for (auto const &m : m_queuedMoves) {
oxLogError(m_project->moveItem(m.a, m.b));
if (!m_project->exists(m.b)) {
oxLogError(m_project->moveItem(m.a, m.b));
}
}
m_queuedMoves.clear();
for (auto const &m : m_queuedDirMoves) {
oxLogError(m_project->moveDir(m.a, m.b));
if (!m_project->exists(m.b)) {
oxLogError(m_project->moveDir(m.a, m.b));
}
}
m_queuedDirMoves.clear();
}

View File

@ -104,6 +104,8 @@ class StudioUI: public ox::SignalHandler {
ox::Error handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR newPath, ox::UUID const&id) noexcept;
ox::Error handleDeleteFile(ox::StringViewCR path) noexcept;
ox::Error createOpenProject(ox::StringViewCR path) noexcept;
ox::Error openProjectPath(ox::StringParam path) noexcept;

View File

@ -88,7 +88,7 @@ FileTreeModel::FileTreeModel(
void FileTreeModel::draw(turbine::Context &tctx) const noexcept {
constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
auto const selected = m_explorer.selected(this) ? ImGuiTreeNodeFlags_Selected : 0;
if (!m_children.empty()) {
if (m_fileType == ox::FileType::Directory) {
auto const nodeOpen = ImGui::TreeNodeEx(m_name.c_str(), dirFlags | selected);
if (ImGui::IsItemActivated() || ImGui::IsItemClicked(1)) {
m_explorer.setSelectedNode(this);
@ -104,7 +104,8 @@ void FileTreeModel::draw(turbine::Context &tctx) const noexcept {
if (ig::DragDropTarget const dragDropTarget; dragDropTarget) {
auto const [ref, err] = ig::getDragDropPayload<FileRef>("FileRef");
if (!err) {
auto const name = substr(ref.path, find(ref.path.rbegin(), ref.path.rend(), '/').offset() + 1);
auto const name = substr(
ref.path, find(ref.path.rbegin(), ref.path.rend(), '/').offset() + 1);
if (ref.isDir) {
m_explorer.dirMoved(ref.path, sfmt("{}/{}", m_fullPath, name));
} else {

View File

@ -64,13 +64,11 @@ PopupResponse PopupControlsOkCancel(
ImGui::Separator();
ImGui::SetCursorPosX(popupWidth - 118);
if (ImGui::Button(ok.c_str(), btnSz)) {
ImGui::CloseCurrentPopup();
popupOpen = false;
out = PopupResponse::OK;
}
ImGui::SameLine();
if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button(cancel.c_str(), btnSz)) {
ImGui::CloseCurrentPopup();
popupOpen = false;
out = PopupResponse::Cancel;
}

View File

@ -46,6 +46,13 @@ static ox::Result<ox::Vector<ox::String>> listAllRecursive(ox::FileSystem &fs, o
return std::move(out);
}
[[nodiscard]]
static constexpr bool isParentOf(ox::StringViewCR parent, ox::StringViewCR child) noexcept {
if (endsWith(child, "/")) {
return beginsWith(child, parent);
}
return beginsWith(sfmt("{}/", child), parent);
}
Project::Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir):
m_kctx(ctx),
@ -99,6 +106,9 @@ ox::Error Project::moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcep
}
ox::Error Project::moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept {
if (isParentOf(src, dest)) {
return ox::Error{1, "cannot move parent to a child directory"};
}
OX_REQUIRE(files, listAllRecursive(m_fs, src));
OX_RETURN_ERROR(m_fs.move(src, dest));
fileMoved.emit(src, dest, ox::UUID{});