[nostalgia/core/studio] Add copy/cut/paste support to TileSheet Editor
This commit is contained in:
parent
a6983ce53b
commit
415c2574bb
@ -59,6 +59,7 @@ void TileSheetEditorImGui::draw(core::Context*) noexcept {
|
|||||||
const auto btnSz = ImVec2(40, 14);
|
const auto btnSz = ImVec2(40, 14);
|
||||||
if (ImGui::Selectable("Draw", m_tool == Tool::Draw, 0, btnSz)) {
|
if (ImGui::Selectable("Draw", m_tool == Tool::Draw, 0, btnSz)) {
|
||||||
m_tool = Tool::Draw;
|
m_tool = Tool::Draw;
|
||||||
|
model()->clearSelection();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Selectable("Select", m_tool == Tool::Select, 0, btnSz)) {
|
if (ImGui::Selectable("Select", m_tool == Tool::Select, 0, btnSz)) {
|
||||||
|
@ -2,14 +2,26 @@
|
|||||||
* Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
|
* Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <ox/claw/read.hpp>
|
||||||
#include <ox/claw/write.hpp>
|
#include <ox/claw/write.hpp>
|
||||||
|
|
||||||
|
#include <nostalgia/core/clipboard.hpp>
|
||||||
#include <nostalgia/core/media.hpp>
|
#include <nostalgia/core/media.hpp>
|
||||||
|
#include <ox/std/buffer.hpp>
|
||||||
|
|
||||||
#include "tilesheeteditormodel.hpp"
|
#include "tilesheeteditormodel.hpp"
|
||||||
|
|
||||||
namespace nostalgia::core {
|
namespace nostalgia::core {
|
||||||
|
|
||||||
|
void TileSheetClipboard::addPixel(const geo::Point &pt, uint16_t colorIdx) noexcept {
|
||||||
|
m_pixels.emplace_back(colorIdx, pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TileSheetClipboard::clear() noexcept {
|
||||||
|
m_pixels.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TileSheetEditorModel::TileSheetEditorModel(Context *ctx, const ox::String &path) {
|
TileSheetEditorModel::TileSheetEditorModel(Context *ctx, const ox::String &path) {
|
||||||
m_ctx = ctx;
|
m_ctx = ctx;
|
||||||
m_path = path;
|
m_path = path;
|
||||||
@ -20,27 +32,60 @@ TileSheetEditorModel::TileSheetEditorModel(Context *ctx, const ox::String &path)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TileSheetEditorModel::cut() {
|
void TileSheetEditorModel::cut() {
|
||||||
|
TileSheetClipboard cb;
|
||||||
|
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
|
||||||
|
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
|
||||||
|
auto pt = geo::Point(x, y);
|
||||||
|
const auto s = activeSubSheet();
|
||||||
|
const auto idx = s->idx(pt);
|
||||||
|
const auto c = s->getPixel(m_img.bpp, idx);
|
||||||
|
s->setPixel(m_img.bpp, idx, 0);
|
||||||
|
pt.x -= m_selectionBounds.x;
|
||||||
|
pt.y -= m_selectionBounds.y;
|
||||||
|
cb.addPixel(pt, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setClipboardObject(m_ctx, &cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileSheetEditorModel::copy() {
|
void TileSheetEditorModel::copy() {
|
||||||
|
TileSheetClipboard cb;
|
||||||
|
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
|
||||||
|
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
|
||||||
|
auto pt = geo::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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setClipboardObject(m_ctx, &cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileSheetEditorModel::paste() {
|
void TileSheetEditorModel::paste() {
|
||||||
|
auto [cb, err] = getClipboardObject<TileSheetClipboard>(m_ctx);
|
||||||
|
if (err) {
|
||||||
|
oxLogError(err);
|
||||||
|
oxErrf("Could not read clipboard: {}", toStr(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto pt = m_selectionOrigin == geo::Point(-1, -1) ? geo::Point(0, 0) : m_selectionOrigin;
|
||||||
|
pushCommand(new PasteCommand(&m_img, m_activeSubsSheetIdx, pt, cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileSheetEditorModel::drawCommand(const geo::Point &pt, std::size_t palIdx) noexcept {
|
void TileSheetEditorModel::drawCommand(const geo::Point &pt, std::size_t palIdx) noexcept {
|
||||||
auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
|
const auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
|
||||||
const auto idx = ptToIdx(pt, activeSubSheet.columns);
|
const auto idx = ptToIdx(pt, activeSubSheet.columns);
|
||||||
if (idx >= activeSubSheet.pixelCnt(m_img.bpp)) {
|
if (idx >= activeSubSheet.pixelCnt(m_img.bpp)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_ongoingDrawCommand) {
|
if (m_ongoingDrawCommand) {
|
||||||
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
|
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
|
||||||
} else {
|
} else if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) {
|
||||||
if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) {
|
|
||||||
pushCommand(new DrawCommand(&m_img, m_activeSubsSheetIdx, idx, palIdx));
|
pushCommand(new DrawCommand(&m_img, m_activeSubsSheetIdx, idx, palIdx));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileSheetEditorModel::endDrawCommand() noexcept {
|
void TileSheetEditorModel::endDrawCommand() noexcept {
|
||||||
@ -67,12 +112,12 @@ void TileSheetEditorModel::setActiveSubsheet(const TileSheet::SubSheetIdx &idx)
|
|||||||
|
|
||||||
void TileSheetEditorModel::select(const geo::Point &pt) noexcept {
|
void TileSheetEditorModel::select(const geo::Point &pt) noexcept {
|
||||||
if (!m_selectionOngoing) {
|
if (!m_selectionOngoing) {
|
||||||
m_selectionPt1 = pt;
|
m_selectionOrigin = pt;
|
||||||
m_selectionOngoing = true;
|
m_selectionOngoing = true;
|
||||||
}
|
m_selectionBounds = {pt, pt};
|
||||||
if (m_selectionPt2 != pt) {
|
m_updated = true;
|
||||||
m_selectionPt2 = pt;
|
} else if (m_selectionBounds.pt2() != pt) {
|
||||||
m_selectionBounds = {m_selectionPt1, m_selectionPt2};
|
m_selectionBounds = {m_selectionOrigin, pt};
|
||||||
m_updated = true;
|
m_updated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,9 +128,8 @@ void TileSheetEditorModel::completeSelection() noexcept {
|
|||||||
|
|
||||||
void TileSheetEditorModel::clearSelection() noexcept {
|
void TileSheetEditorModel::clearSelection() noexcept {
|
||||||
m_updated = true;
|
m_updated = true;
|
||||||
m_selectionPt1 = {-1, -1};
|
m_selectionOrigin = {-1, -1};
|
||||||
m_selectionPt2 = {-1, -1};
|
m_selectionBounds = {{-1, -1}, {-1, -1}};
|
||||||
m_selectionBounds = {m_selectionPt1, m_selectionPt2};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TileSheetEditorModel::updated() const noexcept {
|
bool TileSheetEditorModel::updated() const noexcept {
|
||||||
@ -105,6 +149,7 @@ ox::Error TileSheetEditorModel::markUpdated(int cmdId) noexcept {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandId::Draw:
|
case CommandId::Draw:
|
||||||
|
case CommandId::Paste:
|
||||||
case CommandId::UpdateSubSheet:
|
case CommandId::UpdateSubSheet:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -116,19 +161,19 @@ void TileSheetEditorModel::ackUpdate() noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ox::Error TileSheetEditorModel::saveFile() noexcept {
|
ox::Error TileSheetEditorModel::saveFile() noexcept {
|
||||||
auto sctx = applicationData<studio::StudioContext>(m_ctx);
|
const auto sctx = applicationData<studio::StudioContext>(m_ctx);
|
||||||
oxReturnError(sctx->project->writeObj(m_path, &m_img));
|
oxReturnError(sctx->project->writeObj(m_path, &m_img));
|
||||||
return m_ctx->assetManager.setAsset(m_path, m_img).error;
|
return m_ctx->assetManager.setAsset(m_path, m_img).error;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TileSheetEditorModel::pixelSelected(std::size_t idx) const noexcept {
|
bool TileSheetEditorModel::pixelSelected(std::size_t idx) const noexcept {
|
||||||
auto s = activeSubSheet();
|
const auto s = activeSubSheet();
|
||||||
auto pt = idxToPt(idx, s->columns);
|
const auto pt = idxToPt(idx, s->columns);
|
||||||
return m_selectionBounds.contains(pt);
|
return m_selectionBounds.contains(pt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileSheetEditorModel::getFillPixels(bool *pixels, const geo::Point &pt, int oldColor) const noexcept {
|
void TileSheetEditorModel::getFillPixels(bool *pixels, const geo::Point &pt, int oldColor) const noexcept {
|
||||||
auto &activeSubSheet = *this->activeSubSheet();
|
const auto &activeSubSheet = *this->activeSubSheet();
|
||||||
const auto tileIdx = [activeSubSheet](const geo::Point &pt) noexcept {
|
const auto tileIdx = [activeSubSheet](const geo::Point &pt) noexcept {
|
||||||
return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
|
return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
|
||||||
};
|
};
|
||||||
|
@ -15,12 +15,54 @@
|
|||||||
|
|
||||||
namespace nostalgia::core {
|
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()
|
// Command IDs to use with QUndoCommand::id()
|
||||||
enum class CommandId {
|
enum class CommandId {
|
||||||
Draw = 1,
|
Draw = 1,
|
||||||
AddSubSheet = 2,
|
AddSubSheet = 2,
|
||||||
RmSubSheet = 3,
|
RmSubSheet = 3,
|
||||||
UpdateSubSheet = 4,
|
UpdateSubSheet = 4,
|
||||||
|
Paste = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr bool operator==(CommandId c, int i) noexcept {
|
constexpr bool operator==(CommandId c, int i) noexcept {
|
||||||
@ -88,6 +130,53 @@ struct DrawCommand: public studio::UndoCommand {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 &dst, 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 idx = subsheet.idx(p.pt + dst);
|
||||||
|
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) {
|
||||||
|
if (c.idx < subsheet.pixelCnt(m_img->bpp)) {
|
||||||
|
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) {
|
||||||
|
if (c.idx < subsheet.pixelCnt(m_img->bpp)) {
|
||||||
|
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 {
|
struct AddSubSheetCommand: public studio::UndoCommand {
|
||||||
private:
|
private:
|
||||||
TileSheet *m_img = nullptr;
|
TileSheet *m_img = nullptr;
|
||||||
@ -207,12 +296,7 @@ struct UpdateSubSheetCommand: public studio::UndoCommand {
|
|||||||
sheet.name = m_newName;
|
sheet.name = m_newName;
|
||||||
sheet.columns = m_newCols;
|
sheet.columns = m_newCols;
|
||||||
sheet.rows = m_newRows;
|
sheet.rows = m_newRows;
|
||||||
oxDebugf("old cols: {}, old rows: {}", sheet.columns, sheet.rows);
|
|
||||||
oxDebugf("new cols: {}, new rows: {}", m_newCols, m_newRows);
|
|
||||||
oxDebugf("bpp: {}", m_img->bpp);
|
|
||||||
oxDebugf("pixel count before: {}", sheet.pixels.size());
|
|
||||||
oxLogError(sheet.setPixelCount(m_img->bpp, static_cast<std::size_t>(PixelsPerTile * m_newCols * m_newRows)));
|
oxLogError(sheet.setPixelCount(m_img->bpp, static_cast<std::size_t>(PixelsPerTile * m_newCols * m_newRows)));
|
||||||
oxDebugf("pixel count after: {}", sheet.pixels.size());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void undo() noexcept final {
|
void undo() noexcept final {
|
||||||
@ -227,45 +311,6 @@ struct UpdateSubSheetCommand: public studio::UndoCommand {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TileSheetClipboard {
|
|
||||||
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard";
|
|
||||||
static constexpr auto TypeVersion = 1;
|
|
||||||
|
|
||||||
oxModelFriend(TileSheetClipboard);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
ox::Vector<int> m_pixels;
|
|
||||||
geo::Point m_p1;
|
|
||||||
geo::Point m_p2;
|
|
||||||
|
|
||||||
public:
|
|
||||||
void addPixel(int color);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
bool empty() const;
|
|
||||||
|
|
||||||
void pastePixels(const geo::Point &pt, ox::Vector<int> *tgt, int tgtColumns) const;
|
|
||||||
|
|
||||||
void setPoints(const geo::Point &p1, const geo::Point &p2);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
constexpr geo::Point point1() const noexcept {
|
|
||||||
return m_p1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
constexpr geo::Point point2() const noexcept {
|
|
||||||
return m_p2;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
oxModelBegin(TileSheetClipboard)
|
|
||||||
oxModelFieldRename(pixels, m_pixels)
|
|
||||||
oxModelFieldRename(p1, m_p1)
|
|
||||||
oxModelFieldRename(p2, m_p2)
|
|
||||||
oxModelEnd()
|
|
||||||
|
|
||||||
class TileSheetEditorModel: public ox::SignalHandler {
|
class TileSheetEditorModel: public ox::SignalHandler {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@ -280,11 +325,9 @@ class TileSheetEditorModel: public ox::SignalHandler {
|
|||||||
bool m_updated = false;
|
bool m_updated = false;
|
||||||
Context *m_ctx = nullptr;
|
Context *m_ctx = nullptr;
|
||||||
ox::String m_path;
|
ox::String m_path;
|
||||||
ox::Vector<std::size_t> m_selectedPixels; // pixel idx values
|
|
||||||
bool m_selectionOngoing = false;
|
bool m_selectionOngoing = false;
|
||||||
geo::Point m_selectionPt1 = {-1, -1};
|
geo::Point m_selectionOrigin = {-1, -1};
|
||||||
geo::Point m_selectionPt2 = {-1, -1};
|
geo::Bounds m_selectionBounds = {{-1, -1}, {-1, -1}};
|
||||||
geo::Bounds m_selectionBounds;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TileSheetEditorModel(Context *ctx, const ox::String &path);
|
TileSheetEditorModel(Context *ctx, const ox::String &path);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user