/* * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved. */ #include #include #include #include #include #include "tilesheeteditormodel.hpp" namespace nostalgia::core { class TileSheetClipboard: public ClipboardObject { 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: constexpr void addPixel(const geo::Point &pt, uint16_t colorIdx) noexcept { m_pixels.emplace_back(colorIdx, pt); } [[nodiscard]] 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, Cut = 5, Paste = 6, PaletteChange = 7, }; constexpr bool operator==(CommandId c, int i) noexcept { return static_cast(c) == i; } constexpr bool operator==(int i, CommandId c) noexcept { return static_cast(c) == i; } class 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 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 DrawCommand(TileSheet *img, const TileSheet::SubSheetIdx &subSheetIdx, const ox::Vector &idxList, int palIdx) noexcept { m_img = img; auto &subsheet = m_img->getSubSheet(subSheetIdx); m_subSheetIdx = subSheetIdx; for (const auto idx : idxList) { 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; } constexpr auto append(const ox::Vector &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, 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(CommandId::Draw); } }; template class CutPasteCommand: 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 CutPasteCommand(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(CommandId); } }; class AddSubSheetCommand: public studio::UndoCommand { private: TileSheet *m_img = nullptr; TileSheet::SubSheetIdx m_parentIdx; ox::Vector 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(CommandId::AddSubSheet); } }; class 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(CommandId::RmSubSheet); } }; class 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(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(CommandId::UpdateSubSheet); } }; class PaletteChangeCommand: public studio::UndoCommand { private: TileSheet *m_img = nullptr; ox::FileAddress m_oldPalette; ox::FileAddress m_newPalette; public: constexpr PaletteChangeCommand(TileSheet *img, const ox::String &newPalette) noexcept { m_img = img; m_oldPalette = m_img->defaultPalette; m_newPalette = 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(CommandId::PaletteChange); } }; TileSheetEditorModel::TileSheetEditorModel(Context *ctx, const ox::String &path) { m_ctx = ctx; m_path = path; oxRequireT(img, readObj(ctx, path.c_str())); m_img = *img; oxThrowError(readObj(ctx, m_img.defaultPalette).moveTo(&m_pal)); m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdated); } void TileSheetEditorModel::cut() { TileSheetClipboard blankCb; auto cb = ox::make_unique(); 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 = geo::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 == geo::Point(-1, -1) ? geo::Point(0, 0) : m_selectionOrigin; const auto pt2 = geo::Point(s->columns * TileWidth, s->rows * TileHeight); setClipboardObject(m_ctx, std::move(cb)); pushCommand(new CutPasteCommand(&m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb)); } void TileSheetEditorModel::copy() { auto cb = ox::make_unique();; 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, std::move(cb)); } void TileSheetEditorModel::paste() { auto [cb, err] = getClipboardObject(m_ctx); if (err) { oxLogError(err); oxErrf("Could not read clipboard: {}", toStr(err)); return; } const auto s = activeSubSheet(); const auto pt1 = m_selectionOrigin == geo::Point(-1, -1) ? geo::Point(0, 0) : m_selectionOrigin; const auto pt2 = geo::Point(s->columns * TileWidth, s->rows * TileHeight); pushCommand(new CutPasteCommand(&m_img, m_activeSubsSheetIdx, pt1, pt2, *cb)); } const ox::FileAddress &TileSheetEditorModel::palPath() const noexcept { return m_img.defaultPalette; } ox::Error TileSheetEditorModel::setPalette(const ox::String &path) noexcept { pushCommand(new PaletteChangeCommand(&m_img, path)); return OxError(0); } void TileSheetEditorModel::drawCommand(const geo::Point &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(new DrawCommand(&m_img, m_activeSubsSheetIdx, idx, palIdx)); } } void TileSheetEditorModel::endDrawCommand() noexcept { m_ongoingDrawCommand = nullptr; } void TileSheetEditorModel::addSubsheet(const TileSheet::SubSheetIdx &parentIdx) noexcept { pushCommand(new AddSubSheetCommand(&m_img, parentIdx)); } void TileSheetEditorModel::rmSubsheet(const TileSheet::SubSheetIdx &idx) noexcept { pushCommand(new RmSubSheetCommand(&m_img, idx)); } ox::Error TileSheetEditorModel::updateSubsheet(const TileSheet::SubSheetIdx &idx, const ox::String &name, int cols, int rows) noexcept { pushCommand(new UpdateSubSheetCommand(&m_img, idx, name, cols, rows)); return OxError(0); } void TileSheetEditorModel::setActiveSubsheet(const TileSheet::SubSheetIdx &idx) noexcept { m_activeSubsSheetIdx = idx; this->activeSubsheetChanged.emit(m_activeSubsSheetIdx); } void TileSheetEditorModel::fill(const geo::Point &pt, int palIdx) noexcept { const auto &s = m_img.getSubSheet(m_activeSubsSheetIdx); // build idx list ox::Array 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 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(new DrawCommand(&m_img, m_activeSubsSheetIdx, idxList, palIdx)); } } void TileSheetEditorModel::select(const geo::Point &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::markUpdated(int cmdId) noexcept { m_updated = true; switch (static_cast(cmdId)) { case CommandId::AddSubSheet: case CommandId::RmSubSheet: { // make sure the current active SubSheet is still valid auto idx = m_img.validateSubSheetIdx(m_activeSubsSheetIdx); if (idx != m_activeSubsSheetIdx) { setActiveSubsheet(idx); } break; } case CommandId::Cut: case CommandId::Draw: case CommandId::Paste: case CommandId::UpdateSubSheet: break; case CommandId::PaletteChange: oxReturnError(readObj(m_ctx, m_img.defaultPalette.getPath().value).moveTo(&m_pal)); break; } return OxError(0); } void TileSheetEditorModel::ackUpdate() noexcept { m_updated = false; } ox::Error TileSheetEditorModel::saveFile() noexcept { 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 { 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 { const auto &activeSubSheet = *this->activeSubSheet(); const auto tileIdx = [activeSubSheet](const geo::Point &pt) noexcept { return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile; }; // get points const auto leftPt = pt + geo::Point(-1, 0); const auto rightPt = pt + geo::Point(1, 0); const auto topPt = pt + geo::Point(0, -1); const auto bottomPt = pt + geo::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(cmd); m_updated = true; } }