/* * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved. */ #pragma once #include #include #include #include #include #include #include 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 { return static_cast(c) == i; } constexpr bool operator==(int i, CommandId c) noexcept { return static_cast(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 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(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 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(CommandId::Paste); } }; struct 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); } }; 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(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(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 TileSheetEditorModel: public ox::SignalHandler { public: ox::Signal activeSubsheetChanged; private: TileSheet m_img; TileSheet::SubSheetIdx m_activeSubsSheetIdx; AssetRef 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; } }