435 lines
11 KiB
C++
435 lines
11 KiB
C++
/*
|
|
* Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <ox/std/trace.hpp>
|
|
#include <ox/std/string.hpp>
|
|
|
|
#include <nostalgia/core/gfx.hpp>
|
|
#include <nostalgia/geo/bounds.hpp>
|
|
#include <nostalgia/geo/point.hpp>
|
|
#include <nostalgia/geo/vec.hpp>
|
|
#include <nostalgia/studio/studio.hpp>
|
|
|
|
namespace nostalgia::core {
|
|
|
|
class 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;
|
|
geo::Point pt;
|
|
};
|
|
protected:
|
|
ox::Vector<Pixel> m_pixels;
|
|
|
|
public:
|
|
void addPixel(const geo::Point &pt, uint16_t colorIdx) noexcept;
|
|
|
|
[[nodiscard]]
|
|
bool empty() const;
|
|
|
|
void pastePixels(const geo::Point &pt, ox::Vector<int> *tgt, int tgtColumns) const;
|
|
|
|
void clear() noexcept;
|
|
|
|
constexpr const ox::Vector<Pixel> &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,
|
|
UpdateSubSheet = 4,
|
|
Paste = 5,
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
struct DrawCommand: public studio::UndoCommand {
|
|
private:
|
|
struct Change {
|
|
uint32_t idx = 0;
|
|
uint16_t oldPalIdx = 0;
|
|
};
|
|
TileSheet *m_img = nullptr;
|
|
TileSheet::SubSheetIdx m_subSheetIdx;
|
|
ox::Vector<Change, 8> m_changes;
|
|
int m_palIdx = 0;
|
|
|
|
public:
|
|
constexpr DrawCommand(TileSheet *img, const TileSheet::SubSheetIdx &subSheetIdx, std::size_t idx, int palIdx) noexcept {
|
|
m_img = img;
|
|
auto &subsheet = m_img->getSubSheet(subSheetIdx);
|
|
m_subSheetIdx = subSheetIdx;
|
|
m_changes.emplace_back(idx, subsheet.getPixel(m_img->bpp, idx));
|
|
m_palIdx = palIdx;
|
|
}
|
|
|
|
constexpr auto 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 = std::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(idx, subsheet.getPixel(m_img->bpp, idx));
|
|
subsheet.setPixel(m_img->bpp, idx, m_palIdx);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void redo() noexcept final {
|
|
auto &subsheet = m_img->getSubSheet(m_subSheetIdx);
|
|
for (const auto &c : m_changes) {
|
|
subsheet.setPixel(m_img->bpp, c.idx, 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, c.oldPalIdx);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]]
|
|
int commandId() const noexcept final {
|
|
return static_cast<int>(CommandId::Draw);
|
|
}
|
|
|
|
};
|
|
|
|
struct PasteCommand: public studio::UndoCommand {
|
|
private:
|
|
struct Change {
|
|
uint32_t idx = 0;
|
|
uint16_t newPalIdx = 0;
|
|
uint16_t oldPalIdx = 0;
|
|
};
|
|
TileSheet *m_img = nullptr;
|
|
TileSheet::SubSheetIdx m_subSheetIdx;
|
|
ox::Vector<Change> m_changes;
|
|
|
|
public:
|
|
constexpr PasteCommand(TileSheet *img, const TileSheet::SubSheetIdx &subSheetIdx, const geo::Point &dstStart, const geo::Point &dstEnd, const TileSheetClipboard &cb) noexcept {
|
|
m_img = img;
|
|
m_subSheetIdx = subSheetIdx;
|
|
const auto &subsheet = m_img->getSubSheet(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(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, 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, c.oldPalIdx);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]]
|
|
int commandId() const noexcept final {
|
|
return static_cast<int>(CommandId::Paste);
|
|
}
|
|
|
|
};
|
|
|
|
struct AddSubSheetCommand: public studio::UndoCommand {
|
|
private:
|
|
TileSheet *m_img = nullptr;
|
|
TileSheet::SubSheetIdx m_parentIdx;
|
|
ox::Vector<TileSheet::SubSheetIdx, 2> m_addedSheets;
|
|
|
|
public:
|
|
constexpr AddSubSheetCommand(TileSheet *img, const TileSheet::SubSheetIdx &parentIdx) noexcept {
|
|
m_img = img;
|
|
m_parentIdx = 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(0);
|
|
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(ox::sfmt("Subsheet {}", i).c_str(), 1, 1);
|
|
} else {
|
|
parent.subsheets.emplace_back("Subsheet 0", parent.columns, parent.rows, std::move(parent.pixels));
|
|
parent.rows = 0;
|
|
parent.columns = 0;
|
|
parent.subsheets.emplace_back("Subsheet 1", 1, 1);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
};
|
|
|
|
struct RmSubSheetCommand: public studio::UndoCommand {
|
|
private:
|
|
TileSheet *m_img = nullptr;
|
|
TileSheet::SubSheetIdx m_idx;
|
|
TileSheet::SubSheetIdx m_parentIdx;
|
|
TileSheet::SubSheet m_sheet;
|
|
|
|
public:
|
|
constexpr RmSubSheetCommand(TileSheet *img, const TileSheet::SubSheetIdx &idx) noexcept {
|
|
m_img = img;
|
|
m_idx = 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);
|
|
}
|
|
|
|
};
|
|
|
|
struct UpdateSubSheetCommand: public studio::UndoCommand {
|
|
private:
|
|
TileSheet *m_img = nullptr;
|
|
TileSheet::SubSheetIdx m_idx;
|
|
TileSheet::SubSheet m_sheet;
|
|
ox::String m_newName;
|
|
int m_newCols = 0;
|
|
int m_newRows = 0;
|
|
|
|
public:
|
|
constexpr UpdateSubSheetCommand(TileSheet *img, const TileSheet::SubSheetIdx &idx,
|
|
const ox::String &name, int cols, int rows) noexcept {
|
|
m_img = img;
|
|
m_idx = idx;
|
|
m_sheet = img->getSubSheet(idx);
|
|
m_newName = 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);
|
|
}
|
|
|
|
};
|
|
|
|
class TileSheetEditorModel: public ox::SignalHandler {
|
|
|
|
public:
|
|
ox::Signal<ox::Error(const TileSheet::SubSheetIdx&)> activeSubsheetChanged;
|
|
|
|
private:
|
|
TileSheet m_img;
|
|
TileSheet::SubSheetIdx m_activeSubsSheetIdx;
|
|
AssetRef<Palette> m_pal;
|
|
studio::UndoStack m_undoStack;
|
|
DrawCommand *m_ongoingDrawCommand = nullptr;
|
|
bool m_updated = false;
|
|
Context *m_ctx = nullptr;
|
|
ox::String m_path;
|
|
bool m_selectionOngoing = false;
|
|
geo::Point m_selectionOrigin = {-1, -1};
|
|
geo::Bounds m_selectionBounds = {{-1, -1}, {-1, -1}};
|
|
|
|
public:
|
|
TileSheetEditorModel(Context *ctx, const ox::String &path);
|
|
|
|
~TileSheetEditorModel() override = default;
|
|
|
|
void cut();
|
|
|
|
void copy();
|
|
|
|
void paste();
|
|
|
|
[[nodiscard]]
|
|
constexpr const TileSheet &img() const noexcept;
|
|
|
|
[[nodiscard]]
|
|
constexpr TileSheet &img() noexcept;
|
|
|
|
[[nodiscard]]
|
|
constexpr const Palette &pal() const noexcept;
|
|
|
|
void drawCommand(const geo::Point &pt, std::size_t palIdx) noexcept;
|
|
|
|
void endDrawCommand() noexcept;
|
|
|
|
void addSubsheet(const TileSheet::SubSheetIdx &parentIdx) noexcept;
|
|
|
|
void rmSubsheet(const TileSheet::SubSheetIdx &idx) noexcept;
|
|
|
|
ox::Error updateSubsheet(const TileSheet::SubSheetIdx &idx, const ox::String &name, int cols, int rows) noexcept;
|
|
|
|
void setActiveSubsheet(const TileSheet::SubSheetIdx&) noexcept;
|
|
|
|
constexpr const TileSheet::SubSheet *activeSubSheet() const noexcept {
|
|
auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
|
|
return &activeSubSheet;
|
|
}
|
|
|
|
constexpr TileSheet::SubSheet *activeSubSheet() noexcept {
|
|
auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
|
|
return &activeSubSheet;
|
|
}
|
|
|
|
constexpr const TileSheet::SubSheetIdx &activeSubSheetIdx() const noexcept {
|
|
return m_activeSubsSheetIdx;
|
|
}
|
|
|
|
void select(const geo::Point &pt) noexcept;
|
|
|
|
void completeSelection() noexcept;
|
|
|
|
void clearSelection() noexcept;
|
|
|
|
[[nodiscard]]
|
|
bool updated() const noexcept;
|
|
|
|
ox::Error markUpdated(int) noexcept;
|
|
|
|
void ackUpdate() noexcept;
|
|
|
|
ox::Error saveFile() noexcept;
|
|
|
|
constexpr studio::UndoStack *undoStack() noexcept;
|
|
|
|
bool pixelSelected(std::size_t idx) const noexcept;
|
|
|
|
protected:
|
|
void saveItem();
|
|
|
|
void getFillPixels(bool *pixels, const geo::Point &pt, int oldColor) const noexcept;
|
|
|
|
private:
|
|
void pushCommand(studio::UndoCommand *cmd) noexcept;
|
|
|
|
void setPalette();
|
|
|
|
void saveState();
|
|
|
|
void restoreState();
|
|
|
|
[[nodiscard]]
|
|
ox::String paletteName(const ox::String &palettePath) const;
|
|
|
|
[[nodiscard]]
|
|
ox::String palettePath(const ox::String &palettePath) const;
|
|
|
|
};
|
|
|
|
constexpr const TileSheet &TileSheetEditorModel::img() const noexcept {
|
|
return m_img;
|
|
}
|
|
|
|
constexpr TileSheet &TileSheetEditorModel::img() noexcept {
|
|
return m_img;
|
|
}
|
|
|
|
constexpr const Palette &TileSheetEditorModel::pal() const noexcept {
|
|
return *m_pal;
|
|
}
|
|
|
|
constexpr studio::UndoStack *TileSheetEditorModel::undoStack() noexcept {
|
|
return &m_undoStack;
|
|
}
|
|
|
|
}
|