From 415c2574bb33db07f5bf61e962c595ad52145f0c Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Thu, 10 Mar 2022 20:42:21 -0600 Subject: [PATCH] [nostalgia/core/studio] Add copy/cut/paste support to TileSheet Editor --- .../core/studio/tilesheeteditor-imgui.cpp | 1 + .../core/studio/tilesheeteditormodel.cpp | 79 +++++++--- .../core/studio/tilesheeteditormodel.hpp | 139 ++++++++++++------ 3 files changed, 154 insertions(+), 65 deletions(-) diff --git a/src/nostalgia/core/studio/tilesheeteditor-imgui.cpp b/src/nostalgia/core/studio/tilesheeteditor-imgui.cpp index 0cc38513..6d4db468 100644 --- a/src/nostalgia/core/studio/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/core/studio/tilesheeteditor-imgui.cpp @@ -59,6 +59,7 @@ void TileSheetEditorImGui::draw(core::Context*) noexcept { const auto btnSz = ImVec2(40, 14); if (ImGui::Selectable("Draw", m_tool == Tool::Draw, 0, btnSz)) { m_tool = Tool::Draw; + model()->clearSelection(); } ImGui::SameLine(); if (ImGui::Selectable("Select", m_tool == Tool::Select, 0, btnSz)) { diff --git a/src/nostalgia/core/studio/tilesheeteditormodel.cpp b/src/nostalgia/core/studio/tilesheeteditormodel.cpp index d5a5fe76..6404673e 100644 --- a/src/nostalgia/core/studio/tilesheeteditormodel.cpp +++ b/src/nostalgia/core/studio/tilesheeteditormodel.cpp @@ -2,14 +2,26 @@ * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved. */ +#include #include +#include #include +#include #include "tilesheeteditormodel.hpp" 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) { m_ctx = ctx; m_path = path; @@ -20,26 +32,59 @@ TileSheetEditorModel::TileSheetEditorModel(Context *ctx, const ox::String &path) } 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() { + 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() { + auto [cb, err] = getClipboardObject(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 { - auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx); + const auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx); const auto idx = ptToIdx(pt, activeSubSheet.columns); if (idx >= activeSubSheet.pixelCnt(m_img.bpp)) { return; } if (m_ongoingDrawCommand) { m_updated = m_updated || m_ongoingDrawCommand->append(idx); - } else { - if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) { - pushCommand(new DrawCommand(&m_img, m_activeSubsSheetIdx, idx, palIdx)); - } + } else if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) { + pushCommand(new DrawCommand(&m_img, m_activeSubsSheetIdx, idx, palIdx)); } } @@ -67,12 +112,12 @@ void TileSheetEditorModel::setActiveSubsheet(const TileSheet::SubSheetIdx &idx) void TileSheetEditorModel::select(const geo::Point &pt) noexcept { if (!m_selectionOngoing) { - m_selectionPt1 = pt; + m_selectionOrigin = pt; m_selectionOngoing = true; - } - if (m_selectionPt2 != pt) { - m_selectionPt2 = pt; - m_selectionBounds = {m_selectionPt1, m_selectionPt2}; + m_selectionBounds = {pt, pt}; + m_updated = true; + } else if (m_selectionBounds.pt2() != pt) { + m_selectionBounds = {m_selectionOrigin, pt}; m_updated = true; } } @@ -83,9 +128,8 @@ void TileSheetEditorModel::completeSelection() noexcept { void TileSheetEditorModel::clearSelection() noexcept { m_updated = true; - m_selectionPt1 = {-1, -1}; - m_selectionPt2 = {-1, -1}; - m_selectionBounds = {m_selectionPt1, m_selectionPt2}; + m_selectionOrigin = {-1, -1}; + m_selectionBounds = {{-1, -1}, {-1, -1}}; } bool TileSheetEditorModel::updated() const noexcept { @@ -105,6 +149,7 @@ ox::Error TileSheetEditorModel::markUpdated(int cmdId) noexcept { break; } case CommandId::Draw: + case CommandId::Paste: case CommandId::UpdateSubSheet: break; } @@ -116,19 +161,19 @@ void TileSheetEditorModel::ackUpdate() noexcept { } ox::Error TileSheetEditorModel::saveFile() noexcept { - auto sctx = applicationData(m_ctx); + const auto sctx = applicationData(m_ctx); oxReturnError(sctx->project->writeObj(m_path, &m_img)); return m_ctx->assetManager.setAsset(m_path, m_img).error; } bool TileSheetEditorModel::pixelSelected(std::size_t idx) const noexcept { - auto s = activeSubSheet(); - auto pt = idxToPt(idx, s->columns); + const auto s = activeSubSheet(); + const auto pt = idxToPt(idx, s->columns); return m_selectionBounds.contains(pt); } 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 { return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile; }; diff --git a/src/nostalgia/core/studio/tilesheeteditormodel.hpp b/src/nostalgia/core/studio/tilesheeteditormodel.hpp index 3db89294..b35836fa 100644 --- a/src/nostalgia/core/studio/tilesheeteditormodel.hpp +++ b/src/nostalgia/core/studio/tilesheeteditormodel.hpp @@ -15,12 +15,54 @@ 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 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 *tgt, int tgtColumns) const; + + void clear() noexcept; + + constexpr const ox::Vector &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 { @@ -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 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(CommandId::Paste); + } + +}; + struct AddSubSheetCommand: public studio::UndoCommand { private: TileSheet *m_img = nullptr; @@ -207,12 +296,7 @@ struct UpdateSubSheetCommand: public studio::UndoCommand { sheet.name = m_newName; sheet.columns = m_newCols; 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(PixelsPerTile * m_newCols * m_newRows))); - oxDebugf("pixel count after: {}", sheet.pixels.size()); } 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 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 *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 { public: @@ -280,11 +325,9 @@ class TileSheetEditorModel: public ox::SignalHandler { bool m_updated = false; Context *m_ctx = nullptr; ox::String m_path; - ox::Vector m_selectedPixels; // pixel idx values bool m_selectionOngoing = false; - geo::Point m_selectionPt1 = {-1, -1}; - geo::Point m_selectionPt2 = {-1, -1}; - geo::Bounds m_selectionBounds; + geo::Point m_selectionOrigin = {-1, -1}; + geo::Bounds m_selectionBounds = {{-1, -1}, {-1, -1}}; public: TileSheetEditorModel(Context *ctx, const ox::String &path);