[nostalgia/core/studio] Restructure core studio for maintainability

This commit is contained in:
Gary Talent 2023-12-10 01:48:24 -06:00
parent d5a1e11ec5
commit 7418a7126a
37 changed files with 1161 additions and 837 deletions

View File

@ -1,17 +1,9 @@
add_library(
NostalgiaCore-Studio
paletteeditor.cpp
tilesheeteditorview.cpp
tilesheeteditormodel.cpp
tilesheetpixelgrid.cpp
tilesheetpixels.cpp
)
add_library(NostalgiaCore-Studio)
add_library(
NostalgiaCore-Studio-ImGui
studiomodule.cpp
paletteeditor-imgui.cpp
tilesheeteditor-imgui.cpp
tilesheeteditor/tilesheeteditor-imgui.cpp
)
target_link_libraries(
@ -24,7 +16,6 @@ target_link_libraries(
NostalgiaCore-Studio-ImGui PUBLIC
NostalgiaCore-Studio
Studio
lodepng
)
install(
@ -34,3 +25,6 @@ install(
LIBRARY DESTINATION
${NOSTALGIA_DIST_MODULE}
)
add_subdirectory(paletteeditor)
add_subdirectory(tilesheeteditor)

View File

@ -0,0 +1,5 @@
target_sources(
NostalgiaCore-Studio PRIVATE
paletteeditor.cpp
paletteeditor-imgui.cpp
)

View File

@ -6,8 +6,8 @@
#include <studio/studio.hpp>
#include "paletteeditor-imgui.hpp"
#include "tilesheeteditor-imgui.hpp"
#include "paletteeditor/paletteeditor-imgui.hpp"
#include "tilesheeteditor/tilesheeteditor-imgui.hpp"
namespace nostalgia::core {

View File

@ -0,0 +1,14 @@
target_sources(
NostalgiaCore-Studio PRIVATE
tilesheeteditorview.cpp
tilesheeteditormodel.cpp
tilesheetpixelgrid.cpp
tilesheetpixels.cpp
)
target_link_libraries(
NostalgiaCore-Studio-ImGui PUBLIC
lodepng
)
add_subdirectory(commands)

View File

@ -0,0 +1,11 @@
target_sources(
NostalgiaCore-Studio PRIVATE
addsubsheetcommand.cpp
cutpastecommand.cpp
deletetilescommand.cpp
drawcommand.cpp
inserttilescommand.cpp
palettechangecommand.cpp
rmsubsheetcommand.cpp
updatesubsheetcommand.cpp
)

View File

@ -0,0 +1,63 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "addsubsheetcommand.hpp"
namespace nostalgia::core {
AddSubSheetCommand::AddSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx parentIdx) noexcept:
m_img(img), m_parentIdx(std::move(parentIdx)) {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (parent.subsheets.size()) {
auto idx = m_parentIdx;
idx.emplace_back(parent.subsheets.size());
m_addedSheets.push_back(idx);
} else {
auto idx = m_parentIdx;
idx.emplace_back(0u);
m_addedSheets.push_back(idx);
*idx.back().value = 1;
m_addedSheets.push_back(idx);
}
}
void AddSubSheetCommand::redo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (m_addedSheets.size() < 2) {
auto i = parent.subsheets.size();
parent.subsheets.emplace_back(m_img.idIt++, ox::sfmt("Subsheet {}", i), 1, 1, m_img.bpp);
} else {
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 0", parent.columns, parent.rows, std::move(parent.pixels));
parent.rows = 0;
parent.columns = 0;
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 1", 1, 1, m_img.bpp);
}
}
void AddSubSheetCommand::undo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (parent.subsheets.size() == 2) {
auto s = parent.subsheets[0];
parent.rows = s.rows;
parent.columns = s.columns;
parent.pixels = std::move(s.pixels);
parent.subsheets.clear();
} else {
for (auto idx = m_addedSheets.rbegin(); idx != m_addedSheets.rend(); ++idx) {
oxLogError(m_img.rmSubSheet(*idx));
}
}
}
int AddSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::AddSubSheet);
}
TileSheet::SubSheetIdx const &AddSubSheetCommand::subsheetIdx() const noexcept {
return m_parentIdx;
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class AddSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_parentIdx;
ox::Vector<TileSheet::SubSheetIdx, 2> m_addedSheets;
public:
AddSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx parentIdx) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/undostack.hpp>
#include <nostalgia/core/tilesheet.hpp>
namespace nostalgia::core {
// Command IDs to use with QUndoCommand::id()
enum class CommandId {
Draw = 1,
AddSubSheet = 2,
RmSubSheet = 3,
DeleteTile = 4,
InsertTile = 4,
UpdateSubSheet = 5,
Cut = 6,
Paste = 7,
PaletteChange = 8,
};
constexpr bool operator==(CommandId c, int i) noexcept {
return static_cast<int>(c) == i;
}
constexpr bool operator==(int i, CommandId c) noexcept {
return static_cast<int>(c) == i;
}
class TileSheetCommand: public studio::UndoCommand {
public:
[[nodiscard]]
virtual TileSheet::SubSheetIdx const&subsheetIdx() const noexcept = 0;
};
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "cutpastecommand.hpp"
namespace nostalgia::core {
TileSheetClipboard::Pixel::Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept {
colorIdx = pColorIdx;
pt = pPt;
}
void TileSheetClipboard::addPixel(const ox::Point &pt, uint16_t colorIdx) noexcept {
m_pixels.emplace_back(colorIdx, pt);
}
const ox::Vector<TileSheetClipboard::Pixel> &TileSheetClipboard::pixels() const noexcept {
return m_pixels;
}
CutPasteCommand::CutPasteCommand(
CommandId commandId,
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Point &dstStart,
const ox::Point &dstEnd,
const TileSheetClipboard &cb) noexcept:
m_commandId(commandId),
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
const auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &p : cb.pixels()) {
const auto dstPt = p.pt + dstStart;
if (dstPt.x <= dstEnd.x && dstPt.y <= dstEnd.y) {
const auto idx = subsheet.idx(dstPt);
m_changes.emplace_back(static_cast<uint32_t>(idx), p.colorIdx, subsheet.getPixel(m_img.bpp, idx));
}
}
}
void CutPasteCommand::redo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.newPalIdx));
}
}
void CutPasteCommand::undo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
}
int CutPasteCommand::commandId() const noexcept {
return static_cast<int>(m_commandId);
}
TileSheet::SubSheetIdx const &CutPasteCommand::subsheetIdx() const noexcept {
return m_subSheetIdx;
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <turbine/clipboard.hpp>
#include "commands.hpp"
namespace nostalgia::core {
class TileSheetClipboard: public turbine::ClipboardObject<TileSheetClipboard> {
public:
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard";
static constexpr auto TypeVersion = 1;
oxModelFriend(TileSheetClipboard);
struct Pixel {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard.Pixel";
static constexpr auto TypeVersion = 1;
uint16_t colorIdx = 0;
ox::Point pt;
Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept;
};
protected:
ox::Vector<Pixel> m_pixels;
public:
void addPixel(ox::Point const&pt, uint16_t colorIdx) noexcept;
[[nodiscard]]
ox::Vector<Pixel> const&pixels() const noexcept;
};
oxModelBegin(TileSheetClipboard::Pixel)
oxModelField(colorIdx)
oxModelField(pt)
oxModelEnd()
oxModelBegin(TileSheetClipboard)
oxModelFieldRename(pixels, m_pixels)
oxModelEnd()
class CutPasteCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t newPalIdx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pNewPalIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
newPalIdx = pNewPalIdx;
oldPalIdx = pOldPalIdx;
}
};
CommandId m_commandId;
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change> m_changes;
public:
CutPasteCommand(
CommandId commandId,
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Point const&dstStart,
ox::Point const&dstEnd,
TileSheetClipboard const&cb) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "deletetilescommand.hpp"
namespace nostalgia::core {
core::DeleteTilesCommand::DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_deletePos = tileIdx * bytesPerTile;
m_deleteSz = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_deleteSz);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + m_deletePos;
const auto sz = m_deleteSz * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void core::DeleteTilesCommand::redo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto srcPos = m_deletePos + m_deleteSz;
const auto src = p.data() + srcPos;
const auto dst1 = p.data() + m_deletePos;
const auto dst2 = p.data() + (p.size() - m_deleteSz);
ox_memmove(dst1, src, p.size() - srcPos);
ox_memset(dst2, 0, m_deleteSz * sizeof(decltype(p[0])));
}
void DeleteTilesCommand::undo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto src = p.data() + m_deletePos;
const auto dst1 = p.data() + m_deletePos + m_deleteSz;
const auto dst2 = src;
const auto sz = p.size() - m_deletePos - m_deleteSz;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
int DeleteTilesCommand::commandId() const noexcept {
return static_cast<int>(CommandId::DeleteTile);
}
TileSheet::SubSheetIdx const &DeleteTilesCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class DeleteTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_deletePos = 0;
std::size_t m_deleteSz = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "drawcommand.hpp"
namespace nostalgia::core {
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
m_palIdx = palIdx;
}
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Vector<std::size_t> &idxList,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto idx : idxList) {
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
}
m_palIdx = palIdx;
}
bool DrawCommand::append(std::size_t idx) noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
if (m_changes.back().value->idx != idx && subsheet.getPixel(m_img.bpp, idx) != m_palIdx) {
// duplicate entries are bad
auto existing = ox::find_if(m_changes.cbegin(), m_changes.cend(), [idx](const auto &c) {
return c.idx == idx;
});
if (existing == m_changes.cend()) {
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
subsheet.setPixel(m_img.bpp, idx, static_cast<uint8_t>(m_palIdx));
return true;
}
}
return false;
}
bool DrawCommand::append(const ox::Vector<std::size_t> &idxList) noexcept {
auto out = false;
for (auto idx : idxList) {
out = append(idx) || out;
}
return out;
}
void DrawCommand::redo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(m_palIdx));
}
}
void DrawCommand::undo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
}
int DrawCommand::commandId() const noexcept {
return static_cast<int>(CommandId::Draw);
}
TileSheet::SubSheetIdx const &DrawCommand::subsheetIdx() const noexcept {
return m_subSheetIdx;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class DrawCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
oldPalIdx = pOldPalIdx;
}
};
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change, 8> m_changes;
int m_palIdx = 0;
public:
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept;
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Vector<std::size_t> &idxList,
int palIdx) noexcept;
bool append(std::size_t idx) noexcept;
bool append(const ox::Vector<std::size_t> &idxList) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "inserttilescommand.hpp"
namespace nostalgia::core {
core::InsertTilesCommand::InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_insertPos = tileIdx * bytesPerTile;
m_insertCnt = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_insertCnt);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + p.size() - m_insertCnt;
const auto sz = m_insertCnt * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void InsertTilesCommand::redo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dstPos = m_insertPos + m_insertCnt;
const auto dst = p.data() + dstPos;
const auto src = p.data() + m_insertPos;
ox_memmove(dst, src, p.size() - dstPos);
ox_memset(src, 0, m_insertCnt * sizeof(decltype(p[0])));
}
void InsertTilesCommand::undo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto srcIdx = m_insertPos + m_insertCnt;
const auto src = p.data() + srcIdx;
const auto dst1 = p.data() + m_insertPos;
const auto dst2 = p.data() + p.size() - m_insertCnt;
const auto sz = p.size() - srcIdx;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
int InsertTilesCommand::commandId() const noexcept {
return static_cast<int>(CommandId::InsertTile);
}
TileSheet::SubSheetIdx const &InsertTilesCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class InsertTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_insertPos = 0;
std::size_t m_insertCnt = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "palettechangecommand.hpp"
namespace nostalgia::core {
core::PaletteChangeCommand::PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::CRStringView newPalette) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_oldPalette = m_img.defaultPalette;
m_newPalette = ox::FileAddress(ox::sfmt<ox::BString<43>>("uuid://{}", newPalette));
}
void PaletteChangeCommand::redo() noexcept {
m_img.defaultPalette = m_newPalette;
}
void PaletteChangeCommand::undo() noexcept {
m_img.defaultPalette = m_oldPalette;
}
int PaletteChangeCommand::commandId() const noexcept {
return static_cast<int>(CommandId::PaletteChange);
}
TileSheet::SubSheetIdx const &PaletteChangeCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class PaletteChangeCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
ox::FileAddress m_oldPalette;
ox::FileAddress m_newPalette;
public:
PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::CRStringView newPalette) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "rmsubsheetcommand.hpp"
namespace nostalgia::core {
core::RmSubSheetCommand::RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_parentIdx = idx;
m_parentIdx.pop_back();
auto &parent = m_img.getSubSheet(m_parentIdx);
m_sheet = parent.subsheets[*idx.back().value];
}
void RmSubSheetCommand::redo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
oxLogError(parent.subsheets.erase(*m_idx.back().value).error);
}
void RmSubSheetCommand::undo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
auto i = *m_idx.back().value;
parent.subsheets.insert(i, m_sheet);
}
int RmSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::RmSubSheet);
}
TileSheet::SubSheetIdx const &RmSubSheetCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class RmSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheetIdx m_parentIdx;
TileSheet::SubSheet m_sheet;
public:
RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "updatesubsheetcommand.hpp"
namespace nostalgia::core {
core::UpdateSubSheetCommand::UpdateSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
ox::String name,
int cols,
int rows) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_sheet = m_img.getSubSheet(m_idx);
m_newName = std::move(name);
m_newCols = cols;
m_newRows = rows;
}
void UpdateSubSheetCommand::redo() noexcept {
auto &sheet = m_img.getSubSheet(m_idx);
sheet.name = m_newName;
sheet.columns = m_newCols;
sheet.rows = m_newRows;
oxLogError(sheet.setPixelCount(m_img.bpp, static_cast<std::size_t>(PixelsPerTile * m_newCols * m_newRows)));
}
void UpdateSubSheetCommand::undo() noexcept {
auto &sheet = m_img.getSubSheet(m_idx);
sheet = m_sheet;
}
int UpdateSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::UpdateSubSheet);
}
TileSheet::SubSheetIdx const &UpdateSubSheetCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class UpdateSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheet m_sheet;
ox::String m_newName;
int m_newCols = 0;
int m_newRows = 0;
public:
UpdateSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
ox::String name,
int cols,
int rows) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,289 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/read.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/memory.hpp>
#include <turbine/clipboard.hpp>
#include <keel/media.hpp>
#include <nostalgia/core/ptidxconv.hpp>
#include "commands/commands.hpp"
#include "commands/addsubsheetcommand.hpp"
#include "commands/cutpastecommand.hpp"
#include "commands/deletetilescommand.hpp"
#include "commands/drawcommand.hpp"
#include "commands/inserttilescommand.hpp"
#include "commands/palettechangecommand.hpp"
#include "commands/rmsubsheetcommand.hpp"
#include "commands/updatesubsheetcommand.hpp"
#include "tilesheeteditormodel.hpp"
namespace nostalgia::core {
const Palette TileSheetEditorModel::s_defaultPalette = {
.colors = ox::Vector<Color16>(128),
};
TileSheetEditorModel::TileSheetEditorModel(turbine::Context &ctx, ox::StringView path):
m_ctx(ctx),
m_path(path),
m_img(*readObj<TileSheet>(keelCtx(m_ctx), m_path).unwrapThrow()),
// ignore failure to load palette
m_pal(readObj<Palette>(keelCtx(m_ctx), m_img.defaultPalette).value) {
m_pal.updated.connect(this, &TileSheetEditorModel::markUpdated);
m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdatedCmdId);
}
void TileSheetEditorModel::cut() {
TileSheetClipboard blankCb;
auto cb = ox::make_unique<TileSheetClipboard>();
const auto s = activeSubSheet();
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
auto pt = ox::Point(x, y);
const auto idx = s->idx(pt);
const auto c = s->getPixel(m_img.bpp, idx);
pt.x -= m_selectionBounds.x;
pt.y -= m_selectionBounds.y;
cb->addPixel(pt, c);
blankCb.addPixel(pt, 0);
}
}
const auto pt1 = m_selectionOrigin == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
turbine::setClipboardObject(m_ctx, std::move(cb));
pushCommand(ox::make<CutPasteCommand>(CommandId::Cut, m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb));
}
void TileSheetEditorModel::copy() {
auto cb = ox::make_unique<TileSheetClipboard>();
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
auto pt = ox::Point(x, y);
const auto s = activeSubSheet();
const auto idx = s->idx(pt);
const auto c = s->getPixel(m_img.bpp, idx);
pt.x -= m_selectionBounds.x;
pt.y -= m_selectionBounds.y;
cb->addPixel(pt, c);
}
}
turbine::setClipboardObject(m_ctx, std::move(cb));
}
void TileSheetEditorModel::paste() {
auto [cb, err] = turbine::getClipboardObject<TileSheetClipboard>(m_ctx);
if (err) {
oxLogError(err);
oxErrf("Could not read clipboard: {}", toStr(err));
return;
}
const auto s = activeSubSheet();
const auto pt1 = m_selectionOrigin == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
pushCommand(ox::make<CutPasteCommand>(CommandId::Paste, m_img, m_activeSubsSheetIdx, pt1, pt2, *cb));
}
ox::StringView TileSheetEditorModel::palPath() const noexcept {
auto [path, err] = m_img.defaultPalette.getPath();
if (err) {
return {};
}
constexpr ox::StringView uuidPrefix = "uuid://";
if (ox::beginsWith(path, uuidPrefix)) {
auto uuid = ox::StringView(path.data() + uuidPrefix.bytes(), path.bytes() - uuidPrefix.bytes());
auto out = keelCtx(m_ctx).uuidToPath.at(uuid);
if (out.error) {
return {};
}
return *out.value;
} else {
return path;
}
}
ox::Error TileSheetEditorModel::setPalette(ox::String const&path) noexcept {
oxRequire(uuid, keelCtx(m_ctx).pathToUuid.at(path));
pushCommand(ox::make<PaletteChangeCommand>(activeSubSheetIdx(), m_img, uuid->toString()));
return {};
}
void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t palIdx) noexcept {
const auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
const auto idx = activeSubSheet.idx(pt);
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
} else if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) {
pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
}
}
void TileSheetEditorModel::endDrawCommand() noexcept {
m_ongoingDrawCommand = nullptr;
}
void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept {
pushCommand(ox::make<AddSubSheetCommand>(m_img, parentIdx));
}
void TileSheetEditorModel::rmSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
pushCommand(ox::make<RmSubSheetCommand>(m_img, idx));
}
void TileSheetEditorModel::insertTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<InsertTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
void TileSheetEditorModel::deleteTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<DeleteTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
ox::Error TileSheetEditorModel::updateSubsheet(TileSheet::SubSheetIdx const&idx, const ox::StringView &name, int cols, int rows) noexcept {
pushCommand(ox::make<UpdateSubSheetCommand>(m_img, idx, ox::String(name), cols, rows));
return {};
}
void TileSheetEditorModel::setActiveSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
m_activeSubsSheetIdx = idx;
this->activeSubsheetChanged.emit(m_activeSubsSheetIdx);
}
void TileSheetEditorModel::fill(ox::Point const&pt, int palIdx) noexcept {
const auto &s = m_img.getSubSheet(m_activeSubsSheetIdx);
// build idx list
ox::Array<bool, PixelsPerTile> updateMap = {};
const auto oldColor = s.getPixel(m_img.bpp, pt);
if (pt.x >= s.columns * TileWidth || pt.y >= s.rows * TileHeight) {
return;
}
getFillPixels(updateMap.data(), pt, oldColor);
ox::Vector<std::size_t> idxList;
auto i = s.idx(pt) / PixelsPerTile * PixelsPerTile;
for (auto u : updateMap) {
if (u) {
idxList.emplace_back(i);
}
++i;
}
// do updates to sheet
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idxList);
} else if (s.getPixel(m_img.bpp, pt) != palIdx) {
pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idxList, palIdx));
}
}
void TileSheetEditorModel::select(ox::Point const&pt) noexcept {
if (!m_selectionOngoing) {
m_selectionOrigin = pt;
m_selectionOngoing = true;
m_selectionBounds = {pt, pt};
m_updated = true;
} else if (m_selectionBounds.pt2() != pt) {
m_selectionBounds = {m_selectionOrigin, pt};
m_updated = true;
}
}
void TileSheetEditorModel::completeSelection() noexcept {
m_selectionOngoing = false;
auto s = activeSubSheet();
auto pt = m_selectionBounds.pt2();
pt.x = ox::min(s->columns * TileWidth, pt.x);
pt.y = ox::min(s->rows * TileHeight, pt.y);
m_selectionBounds.setPt2(pt);
}
void TileSheetEditorModel::clearSelection() noexcept {
m_updated = true;
m_selectionOrigin = {-1, -1};
m_selectionBounds = {{-1, -1}, {-1, -1}};
}
bool TileSheetEditorModel::updated() const noexcept {
return m_updated;
}
ox::Error TileSheetEditorModel::markUpdatedCmdId(const studio::UndoCommand *cmd) noexcept {
m_updated = true;
const auto cmdId = cmd->commandId();
if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
oxReturnError(readObj<Palette>(keelCtx(m_ctx), m_img.defaultPalette).moveTo(&m_pal));
paletteChanged.emit();
}
auto tsCmd = dynamic_cast<const TileSheetCommand*>(cmd);
auto idx = m_img.validateSubSheetIdx(tsCmd->subsheetIdx());
if (idx != m_activeSubsSheetIdx) {
setActiveSubsheet(idx);
}
return {};
}
ox::Error TileSheetEditorModel::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorModel::ackUpdate() noexcept {
m_updated = false;
}
ox::Error TileSheetEditorModel::saveFile() noexcept {
const auto sctx = applicationData<studio::StudioContext>(m_ctx);
return sctx->project->writeObj(m_path, m_img);
}
bool TileSheetEditorModel::pixelSelected(std::size_t idx) const noexcept {
const auto s = activeSubSheet();
const auto pt = idxToPt(static_cast<int>(idx), s->columns);
return m_selectionBounds.contains(pt);
}
void TileSheetEditorModel::getFillPixels(bool *pixels, ox::Point const&pt, int oldColor) const noexcept {
const auto &activeSubSheet = *this->activeSubSheet();
const auto tileIdx = [activeSubSheet](const ox::Point &pt) noexcept {
return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
};
// get points
const auto leftPt = pt + ox::Point(-1, 0);
const auto rightPt = pt + ox::Point(1, 0);
const auto topPt = pt + ox::Point(0, -1);
const auto bottomPt = pt + ox::Point(0, 1);
// calculate indices
const auto idx = ptToIdx(pt, activeSubSheet.columns);
const auto leftIdx = ptToIdx(leftPt, activeSubSheet.columns);
const auto rightIdx = ptToIdx(rightPt, activeSubSheet.columns);
const auto topIdx = ptToIdx(topPt, activeSubSheet.columns);
const auto bottomIdx = ptToIdx(bottomPt, activeSubSheet.columns);
const auto tile = tileIdx(pt);
// mark pixels to update
pixels[idx % PixelsPerTile] = true;
if (!pixels[leftIdx % PixelsPerTile] && tile == tileIdx(leftPt) && activeSubSheet.getPixel(m_img.bpp, leftIdx) == oldColor) {
getFillPixels(pixels, leftPt, oldColor);
}
if (!pixels[rightIdx % PixelsPerTile] && tile == tileIdx(rightPt) && activeSubSheet.getPixel(m_img.bpp, rightIdx) == oldColor) {
getFillPixels(pixels, rightPt, oldColor);
}
if (!pixels[topIdx % PixelsPerTile] && tile == tileIdx(topPt) && activeSubSheet.getPixel(m_img.bpp, topIdx) == oldColor) {
getFillPixels(pixels, topPt, oldColor);
}
if (!pixels[bottomIdx % PixelsPerTile] && tile == tileIdx(bottomPt) && activeSubSheet.getPixel(m_img.bpp, bottomIdx) == oldColor) {
getFillPixels(pixels, bottomPt, oldColor);
}
}
void TileSheetEditorModel::pushCommand(studio::UndoCommand *cmd) noexcept {
m_undoStack.push(cmd);
m_ongoingDrawCommand = dynamic_cast<DrawCommand*>(cmd);
m_updated = true;
}
}

View File

@ -1,824 +0,0 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/read.hpp>
#include <ox/std/algorithm.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/memory.hpp>
#include <turbine/clipboard.hpp>
#include <keel/media.hpp>
#include <nostalgia/core/ptidxconv.hpp>
#include "tilesheeteditormodel.hpp"
namespace nostalgia::core {
const Palette TileSheetEditorModel::s_defaultPalette = {
.colors = ox::Vector<Color16>(128),
};
class TileSheetClipboard: public turbine::ClipboardObject<TileSheetClipboard> {
public:
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard";
static constexpr auto TypeVersion = 1;
oxModelFriend(TileSheetClipboard);
struct Pixel {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard.Pixel";
static constexpr auto TypeVersion = 1;
uint16_t colorIdx = 0;
ox::Point pt;
constexpr Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept {
colorIdx = pColorIdx;
pt = pPt;
}
};
protected:
ox::Vector<Pixel> m_pixels;
public:
constexpr void addPixel(ox::Point const&pt, uint16_t colorIdx) noexcept {
m_pixels.emplace_back(colorIdx, pt);
}
[[nodiscard]]
constexpr ox::Vector<Pixel> const&pixels() const noexcept {
return m_pixels;
}
};
oxModelBegin(TileSheetClipboard::Pixel)
oxModelField(colorIdx)
oxModelField(pt)
oxModelEnd()
oxModelBegin(TileSheetClipboard)
oxModelFieldRename(pixels, m_pixels)
oxModelEnd()
// Command IDs to use with QUndoCommand::id()
enum class CommandId {
Draw = 1,
AddSubSheet = 2,
RmSubSheet = 3,
DeleteTile = 4,
InsertTile = 4,
UpdateSubSheet = 5,
Cut = 6,
Paste = 7,
PaletteChange = 8,
};
constexpr bool operator==(CommandId c, int i) noexcept {
return static_cast<int>(c) == i;
}
constexpr bool operator==(int i, CommandId c) noexcept {
return static_cast<int>(c) == i;
}
class TileSheetCommand: public studio::UndoCommand {
public:
[[nodiscard]]
virtual TileSheet::SubSheetIdx const&subsheetIdx() const noexcept = 0;
};
class DrawCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
oldPalIdx = pOldPalIdx;
}
};
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change, 8> m_changes;
int m_palIdx = 0;
public:
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
m_palIdx = palIdx;
}
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Vector<std::size_t> &idxList,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto idx : idxList) {
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
}
m_palIdx = palIdx;
}
bool append(std::size_t idx) noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
if (m_changes.back().value->idx != idx && subsheet.getPixel(m_img.bpp, idx) != m_palIdx) {
// duplicate entries are bad
auto existing = ox::find_if(m_changes.cbegin(), m_changes.cend(), [idx](const auto &c) {
return c.idx == idx;
});
if (existing == m_changes.cend()) {
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
subsheet.setPixel(m_img.bpp, idx, static_cast<uint8_t>(m_palIdx));
return true;
}
}
return false;
}
constexpr auto append(const ox::Vector<std::size_t> &idxList) noexcept {
auto out = false;
for (auto idx : idxList) {
out = append(idx) || out;
}
return out;
}
void redo() noexcept final {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(m_palIdx));
}
}
void undo() noexcept final {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::Draw);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_subSheetIdx;
}
};
template<CommandId CommandId>
class CutPasteCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t newPalIdx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pNewPalIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
newPalIdx = pNewPalIdx;
oldPalIdx = pOldPalIdx;
}
};
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change> m_changes;
public:
constexpr CutPasteCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Point const&dstStart,
ox::Point const&dstEnd,
TileSheetClipboard const&cb) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
const auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &p : cb.pixels()) {
const auto dstPt = p.pt + dstStart;
if (dstPt.x <= dstEnd.x && dstPt.y <= dstEnd.y) {
const auto idx = subsheet.idx(dstPt);
m_changes.emplace_back(static_cast<uint32_t>(idx), p.colorIdx, subsheet.getPixel(m_img.bpp, idx));
}
}
}
void redo() noexcept final {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.newPalIdx));
}
}
void undo() noexcept final {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_subSheetIdx;
}
};
class AddSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_parentIdx;
ox::Vector<TileSheet::SubSheetIdx, 2> m_addedSheets;
public:
AddSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx parentIdx) noexcept:
m_img(img), m_parentIdx(std::move(parentIdx)) {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (parent.subsheets.size()) {
auto idx = m_parentIdx;
idx.emplace_back(parent.subsheets.size());
m_addedSheets.push_back(idx);
} else {
auto idx = m_parentIdx;
idx.emplace_back(0u);
m_addedSheets.push_back(idx);
*idx.back().value = 1;
m_addedSheets.push_back(idx);
}
}
void redo() noexcept final {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (m_addedSheets.size() < 2) {
auto i = parent.subsheets.size();
parent.subsheets.emplace_back(m_img.idIt++, ox::sfmt("Subsheet {}", i), 1, 1, m_img.bpp);
} else {
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 0", parent.columns, parent.rows, std::move(parent.pixels));
parent.rows = 0;
parent.columns = 0;
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 1", 1, 1, m_img.bpp);
}
}
void undo() noexcept final {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (parent.subsheets.size() == 2) {
auto s = parent.subsheets[0];
parent.rows = s.rows;
parent.columns = s.columns;
parent.pixels = std::move(s.pixels);
parent.subsheets.clear();
} else {
for (auto idx = m_addedSheets.rbegin(); idx != m_addedSheets.rend(); ++idx) {
oxLogError(m_img.rmSubSheet(*idx));
}
}
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::AddSubSheet);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_parentIdx;
}
};
class RmSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheetIdx m_parentIdx;
TileSheet::SubSheet m_sheet;
public:
RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_parentIdx = idx;
m_parentIdx.pop_back();
auto &parent = m_img.getSubSheet(m_parentIdx);
m_sheet = parent.subsheets[*idx.back().value];
}
void redo() noexcept final {
auto &parent = m_img.getSubSheet(m_parentIdx);
oxLogError(parent.subsheets.erase(*m_idx.back().value).error);
}
void undo() noexcept final {
auto &parent = m_img.getSubSheet(m_parentIdx);
auto i = *m_idx.back().value;
parent.subsheets.insert(i, m_sheet);
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::RmSubSheet);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_idx;
}
};
class InsertTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_insertPos = 0;
std::size_t m_insertCnt = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_insertPos = tileIdx * bytesPerTile;
m_insertCnt = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_insertCnt);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + p.size() - m_insertCnt;
const auto sz = m_insertCnt * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void redo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dstPos = m_insertPos + m_insertCnt;
const auto dst = p.data() + dstPos;
const auto src = p.data() + m_insertPos;
ox_memmove(dst, src, p.size() - dstPos);
ox_memset(src, 0, m_insertCnt * sizeof(decltype(p[0])));
}
void undo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto srcIdx = m_insertPos + m_insertCnt;
const auto src = p.data() + srcIdx;
const auto dst1 = p.data() + m_insertPos;
const auto dst2 = p.data() + p.size() - m_insertCnt;
const auto sz = p.size() - srcIdx;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::InsertTile);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_idx;
}
};
class DeleteTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_deletePos = 0;
std::size_t m_deleteSz = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_deletePos = tileIdx * bytesPerTile;
m_deleteSz = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_deleteSz);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + m_deletePos;
const auto sz = m_deleteSz * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void redo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto srcPos = m_deletePos + m_deleteSz;
const auto src = p.data() + srcPos;
const auto dst1 = p.data() + m_deletePos;
const auto dst2 = p.data() + (p.size() - m_deleteSz);
ox_memmove(dst1, src, p.size() - srcPos);
ox_memset(dst2, 0, m_deleteSz * sizeof(decltype(p[0])));
}
void undo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto src = p.data() + m_deletePos;
const auto dst1 = p.data() + m_deletePos + m_deleteSz;
const auto dst2 = src;
const auto sz = p.size() - m_deletePos - m_deleteSz;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::DeleteTile);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_idx;
}
};
class UpdateSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheet m_sheet;
ox::String m_newName;
int m_newCols = 0;
int m_newRows = 0;
public:
UpdateSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
ox::String name,
int cols,
int rows) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_sheet = m_img.getSubSheet(m_idx);
m_newName = std::move(name);
m_newCols = cols;
m_newRows = rows;
}
void redo() noexcept final {
auto &sheet = m_img.getSubSheet(m_idx);
sheet.name = m_newName;
sheet.columns = m_newCols;
sheet.rows = m_newRows;
oxLogError(sheet.setPixelCount(m_img.bpp, static_cast<std::size_t>(PixelsPerTile * m_newCols * m_newRows)));
}
void undo() noexcept final {
auto &sheet = m_img.getSubSheet(m_idx);
sheet = m_sheet;
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::UpdateSubSheet);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_idx;
}
};
class PaletteChangeCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
ox::FileAddress m_oldPalette;
ox::FileAddress m_newPalette;
public:
PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::CRStringView newPalette) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_oldPalette = m_img.defaultPalette;
m_newPalette = ox::FileAddress(ox::sfmt<ox::BString<43>>("uuid://{}", newPalette));
}
void redo() noexcept final {
m_img.defaultPalette = m_newPalette;
}
void undo() noexcept final {
m_img.defaultPalette = m_oldPalette;
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::PaletteChange);
}
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override {
return m_idx;
}
};
TileSheetEditorModel::TileSheetEditorModel(turbine::Context &ctx, ox::StringView path):
m_ctx(ctx),
m_path(path),
m_img(*readObj<TileSheet>(keelCtx(m_ctx), m_path).unwrapThrow()),
// ignore failure to load palette
m_pal(readObj<Palette>(keelCtx(m_ctx), m_img.defaultPalette).value) {
m_pal.updated.connect(this, &TileSheetEditorModel::markUpdated);
m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdatedCmdId);
}
void TileSheetEditorModel::cut() {
TileSheetClipboard blankCb;
auto cb = ox::make_unique<TileSheetClipboard>();
const auto s = activeSubSheet();
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
auto pt = ox::Point(x, y);
const auto idx = s->idx(pt);
const auto c = s->getPixel(m_img.bpp, idx);
pt.x -= m_selectionBounds.x;
pt.y -= m_selectionBounds.y;
cb->addPixel(pt, c);
blankCb.addPixel(pt, 0);
}
}
const auto pt1 = m_selectionOrigin == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
turbine::setClipboardObject(m_ctx, std::move(cb));
pushCommand(ox::make<CutPasteCommand<CommandId::Cut>>(m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb));
}
void TileSheetEditorModel::copy() {
auto cb = ox::make_unique<TileSheetClipboard>();
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
auto pt = ox::Point(x, y);
const auto s = activeSubSheet();
const auto idx = s->idx(pt);
const auto c = s->getPixel(m_img.bpp, idx);
pt.x -= m_selectionBounds.x;
pt.y -= m_selectionBounds.y;
cb->addPixel(pt, c);
}
}
turbine::setClipboardObject(m_ctx, std::move(cb));
}
void TileSheetEditorModel::paste() {
auto [cb, err] = turbine::getClipboardObject<TileSheetClipboard>(m_ctx);
if (err) {
oxLogError(err);
oxErrf("Could not read clipboard: {}", toStr(err));
return;
}
const auto s = activeSubSheet();
const auto pt1 = m_selectionOrigin == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
pushCommand(ox::make<CutPasteCommand<CommandId::Paste>>(m_img, m_activeSubsSheetIdx, pt1, pt2, *cb));
}
ox::StringView TileSheetEditorModel::palPath() const noexcept {
auto [path, err] = m_img.defaultPalette.getPath();
if (err) {
return {};
}
constexpr ox::StringView uuidPrefix = "uuid://";
if (ox::beginsWith(path, uuidPrefix)) {
auto uuid = ox::StringView(path.data() + uuidPrefix.bytes(), path.bytes() - uuidPrefix.bytes());
auto out = keelCtx(m_ctx).uuidToPath.at(uuid);
if (out.error) {
return {};
}
return *out.value;
} else {
return path;
}
}
ox::Error TileSheetEditorModel::setPalette(ox::String const&path) noexcept {
oxRequire(uuid, keelCtx(m_ctx).pathToUuid.at(path));
pushCommand(ox::make<PaletteChangeCommand>(activeSubSheetIdx(), m_img, uuid->toString()));
return {};
}
void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t palIdx) noexcept {
const auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
const auto idx = activeSubSheet.idx(pt);
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
} else if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) {
pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
}
}
void TileSheetEditorModel::endDrawCommand() noexcept {
m_ongoingDrawCommand = nullptr;
}
void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept {
pushCommand(ox::make<AddSubSheetCommand>(m_img, parentIdx));
}
void TileSheetEditorModel::rmSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
pushCommand(ox::make<RmSubSheetCommand>(m_img, idx));
}
void TileSheetEditorModel::insertTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<InsertTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
void TileSheetEditorModel::deleteTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<DeleteTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
ox::Error TileSheetEditorModel::updateSubsheet(TileSheet::SubSheetIdx const&idx, const ox::StringView &name, int cols, int rows) noexcept {
pushCommand(ox::make<UpdateSubSheetCommand>(m_img, idx, ox::String(name), cols, rows));
return {};
}
void TileSheetEditorModel::setActiveSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
m_activeSubsSheetIdx = idx;
this->activeSubsheetChanged.emit(m_activeSubsSheetIdx);
}
void TileSheetEditorModel::fill(ox::Point const&pt, int palIdx) noexcept {
const auto &s = m_img.getSubSheet(m_activeSubsSheetIdx);
// build idx list
ox::Array<bool, PixelsPerTile> updateMap = {};
const auto oldColor = s.getPixel(m_img.bpp, pt);
if (pt.x >= s.columns * TileWidth || pt.y >= s.rows * TileHeight) {
return;
}
getFillPixels(updateMap.data(), pt, oldColor);
ox::Vector<std::size_t> idxList;
auto i = s.idx(pt) / PixelsPerTile * PixelsPerTile;
for (auto u : updateMap) {
if (u) {
idxList.emplace_back(i);
}
++i;
}
// do updates to sheet
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idxList);
} else if (s.getPixel(m_img.bpp, pt) != palIdx) {
pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idxList, palIdx));
}
}
void TileSheetEditorModel::select(ox::Point const&pt) noexcept {
if (!m_selectionOngoing) {
m_selectionOrigin = pt;
m_selectionOngoing = true;
m_selectionBounds = {pt, pt};
m_updated = true;
} else if (m_selectionBounds.pt2() != pt) {
m_selectionBounds = {m_selectionOrigin, pt};
m_updated = true;
}
}
void TileSheetEditorModel::completeSelection() noexcept {
m_selectionOngoing = false;
auto s = activeSubSheet();
auto pt = m_selectionBounds.pt2();
pt.x = ox::min(s->columns * TileWidth, pt.x);
pt.y = ox::min(s->rows * TileHeight, pt.y);
m_selectionBounds.setPt2(pt);
}
void TileSheetEditorModel::clearSelection() noexcept {
m_updated = true;
m_selectionOrigin = {-1, -1};
m_selectionBounds = {{-1, -1}, {-1, -1}};
}
bool TileSheetEditorModel::updated() const noexcept {
return m_updated;
}
ox::Error TileSheetEditorModel::markUpdatedCmdId(const studio::UndoCommand *cmd) noexcept {
m_updated = true;
const auto cmdId = cmd->commandId();
if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
oxReturnError(readObj<Palette>(keelCtx(m_ctx), m_img.defaultPalette).moveTo(&m_pal));
paletteChanged.emit();
}
auto tsCmd = dynamic_cast<const TileSheetCommand*>(cmd);
auto idx = m_img.validateSubSheetIdx(tsCmd->subsheetIdx());
if (idx != m_activeSubsSheetIdx) {
setActiveSubsheet(idx);
}
return {};
}
ox::Error TileSheetEditorModel::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorModel::ackUpdate() noexcept {
m_updated = false;
}
ox::Error TileSheetEditorModel::saveFile() noexcept {
const auto sctx = applicationData<studio::StudioContext>(m_ctx);
return sctx->project->writeObj(m_path, m_img);
}
bool TileSheetEditorModel::pixelSelected(std::size_t idx) const noexcept {
const auto s = activeSubSheet();
const auto pt = idxToPt(static_cast<int>(idx), s->columns);
return m_selectionBounds.contains(pt);
}
void TileSheetEditorModel::getFillPixels(bool *pixels, ox::Point const&pt, int oldColor) const noexcept {
const auto &activeSubSheet = *this->activeSubSheet();
const auto tileIdx = [activeSubSheet](const ox::Point &pt) noexcept {
return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
};
// get points
const auto leftPt = pt + ox::Point(-1, 0);
const auto rightPt = pt + ox::Point(1, 0);
const auto topPt = pt + ox::Point(0, -1);
const auto bottomPt = pt + ox::Point(0, 1);
// calculate indices
const auto idx = ptToIdx(pt, activeSubSheet.columns);
const auto leftIdx = ptToIdx(leftPt, activeSubSheet.columns);
const auto rightIdx = ptToIdx(rightPt, activeSubSheet.columns);
const auto topIdx = ptToIdx(topPt, activeSubSheet.columns);
const auto bottomIdx = ptToIdx(bottomPt, activeSubSheet.columns);
const auto tile = tileIdx(pt);
// mark pixels to update
pixels[idx % PixelsPerTile] = true;
if (!pixels[leftIdx % PixelsPerTile] && tile == tileIdx(leftPt) && activeSubSheet.getPixel(m_img.bpp, leftIdx) == oldColor) {
getFillPixels(pixels, leftPt, oldColor);
}
if (!pixels[rightIdx % PixelsPerTile] && tile == tileIdx(rightPt) && activeSubSheet.getPixel(m_img.bpp, rightIdx) == oldColor) {
getFillPixels(pixels, rightPt, oldColor);
}
if (!pixels[topIdx % PixelsPerTile] && tile == tileIdx(topPt) && activeSubSheet.getPixel(m_img.bpp, topIdx) == oldColor) {
getFillPixels(pixels, topPt, oldColor);
}
if (!pixels[bottomIdx % PixelsPerTile] && tile == tileIdx(bottomPt) && activeSubSheet.getPixel(m_img.bpp, bottomIdx) == oldColor) {
getFillPixels(pixels, bottomPt, oldColor);
}
}
void TileSheetEditorModel::pushCommand(studio::UndoCommand *cmd) noexcept {
m_undoStack.push(cmd);
m_ongoingDrawCommand = dynamic_cast<DrawCommand*>(cmd);
m_updated = true;
}
}