[nostalgia] Move modules into modules directory

This commit is contained in:
2023-06-17 17:22:39 -05:00
parent dd54e7363f
commit ecde759bec
73 changed files with 75 additions and 63 deletions

View File

@@ -0,0 +1,43 @@
add_library(
NostalgiaCore-Studio
paletteeditor.cpp
tilesheeteditorview.cpp
tilesheeteditormodel.cpp
tilesheetpixelgrid.cpp
tilesheetpixels.cpp
)
add_library(
NostalgiaCore-Studio-ImGui OBJECT
studiomodule.cpp
paletteeditor-imgui.cpp
tilesheeteditor-imgui.cpp
)
if(NOT MSVC)
target_compile_options(NostalgiaCore-Studio PRIVATE -Wsign-conversion)
target_compile_options(NostalgiaCore-Studio-ImGui PRIVATE -Wsign-conversion)
endif()
target_link_libraries(
NostalgiaCore-Studio PUBLIC
NostalgiaCore
Studio
)
target_link_libraries(
NostalgiaCore-Studio-ImGui PUBLIC
NostalgiaCore-Studio
Studio
lodepng
)
#target_compile_definitions(NostalgiaCore-Studio PRIVATE QT_QML_DEBUG)
install(
TARGETS
NostalgiaCore-Studio-ImGui
NostalgiaCore-Studio
LIBRARY DESTINATION
${NOSTALGIA_DIST_MODULE}
)

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <ox/std/memory.hpp>
#include <keel/media.hpp>
#include <nostalgia/core/gfx.hpp>
#include "paletteeditor.hpp"
#include "paletteeditor-imgui.hpp"
namespace nostalgia::core {
ox::Result<PaletteEditorImGui*> PaletteEditorImGui::make(turbine::Context *ctx, ox::CRStringView path) noexcept {
ox::UniquePtr<PaletteEditorImGui> out;
try {
out = ox::UniquePtr<PaletteEditorImGui>(new PaletteEditorImGui);
} catch (...) {
return OxError(1);
}
out->m_ctx = ctx;
out->m_itemPath = path;
const auto lastSlash = std::find(out->m_itemPath.rbegin(), out->m_itemPath.rend(), '/').offset();
out->m_itemName = out->m_itemPath.substr(lastSlash + 1);
oxRequire(pal, keel::readObj<Palette>(out->m_ctx, ox::FileAddress(out->m_itemPath.c_str())));
out->m_pal = *pal;
return out.release();
}
const ox::String &PaletteEditorImGui::itemName() const noexcept {
return m_itemPath;
}
const ox::String &PaletteEditorImGui::itemDisplayName() const noexcept {
return m_itemName;
}
void PaletteEditorImGui::draw(turbine::Context*) noexcept {
static constexpr auto flags = ImGuiTableFlags_RowBg;
const auto paneSize = ImGui::GetContentRegionAvail();
ImGui::BeginChild("PaletteEditor");
{
ImGui::BeginChild("Colors", ImVec2(paneSize.x - 200, paneSize.y), false);
{
const auto colorsSz = ImGui::GetContentRegionAvail();
static constexpr auto toolbarHeight = 40;
ImGui::BeginChild("Toolbar", ImVec2(colorsSz.x, toolbarHeight), true);
{
const auto sz = ImVec2(70, 24);
if (ImGui::Button("Add", sz)) {
const auto colorSz = static_cast<int>(m_pal.colors.size());
constexpr Color16 c = 0;
undoStack()->push(ox::make<AddColorCommand>(&m_pal, c, colorSz));
}
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedRow >= m_pal.colors.size());
{
if (ImGui::Button("Remove", sz)) {
undoStack()->push(ox::make<RemoveColorCommand>(&m_pal, m_pal.colors[static_cast<std::size_t>(m_selectedRow)], static_cast<int>(m_selectedRow)));
m_selectedRow = ox::min(m_pal.colors.size() - 1, m_selectedRow);
}
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedRow <= 0);
{
if (ImGui::Button("Move Up", sz)) {
undoStack()->push(ox::make<MoveColorCommand>(&m_pal, m_selectedRow, -1));
--m_selectedRow;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedRow >= m_pal.colors.size() - 1);
{
if (ImGui::Button("Move Down", sz)) {
undoStack()->push(ox::make<MoveColorCommand>(&m_pal, m_selectedRow, 1));
++m_selectedRow;
}
}
ImGui::EndDisabled();
}
ImGui::EndDisabled();
}
ImGui::EndChild();
ImGui::BeginTable("Colors", 5, flags, ImVec2(colorsSz.x, colorsSz.y - (toolbarHeight + 5)));
{
ImGui::TableSetupColumn("Idx", ImGuiTableColumnFlags_WidthFixed, 25);
ImGui::TableSetupColumn("Red", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Green", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Blue", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Color Preview", ImGuiTableColumnFlags_NoHide);
ImGui::TableHeadersRow();
for (auto i = 0u; const auto c : m_pal.colors) {
ImGui::PushID(static_cast<int>(i));
ImGui::TableNextRow();
// Color No.
ImGui::TableNextColumn();
ImGui::Text("%d", i);
// Red
ImGui::TableNextColumn();
ImGui::Text("%d", red16(c));
// Green
ImGui::TableNextColumn();
ImGui::Text("%d", green16(c));
// Blue
ImGui::TableNextColumn();
ImGui::Text("%d", blue16(c));
// ColorPreview
ImGui::TableNextColumn();
const auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
if (ImGui::Selectable("##ColorRow", i == m_selectedRow, ImGuiSelectableFlags_SpanAllColumns)) {
m_selectedRow = i;
}
ImGui::PopID();
++i;
}
}
ImGui::EndTable();
}
ImGui::EndChild();
if (m_selectedRow < m_pal.colors.size()) {
ImGui::SameLine();
ImGui::BeginChild("ColorEditor", ImVec2(200, paneSize.y), true);
{
const auto c = m_pal.colors[m_selectedRow];
int r = red16(c);
int g = green16(c);
int b = blue16(c);
int a = alpha16(c);
ImGui::InputInt("Red", &r, 1, 5);
ImGui::InputInt("Green", &g, 1, 5);
ImGui::InputInt("Blue", &b, 1, 5);
const auto newColor = color16(r, g, b, a);
if (c != newColor) {
undoStack()->push(ox::make<UpdateColorCommand>(&m_pal, static_cast<int>(m_selectedRow), c, newColor));
}
}
ImGui::EndChild();
}
}
ImGui::EndChild();
}
ox::Error PaletteEditorImGui::saveItem() noexcept {
const auto sctx = applicationData<studio::StudioContext>(*m_ctx);
oxReturnError(sctx->project->writeObj(m_itemPath, &m_pal));
oxReturnError(m_ctx->assetManager.setAsset(m_itemPath, m_pal));
return {};
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
namespace nostalgia::core {
class PaletteEditorImGui: public studio::Editor {
private:
turbine::Context *m_ctx = nullptr;
ox::String m_itemName;
ox::String m_itemPath;
Palette m_pal;
std::size_t m_selectedRow = 0;
PaletteEditorImGui() noexcept = default;
public:
static ox::Result<PaletteEditorImGui*> make(turbine::Context *ctx, ox::CRStringView path) noexcept;
/**
* Returns the name of item being edited.
*/
const ox::String &itemName() const noexcept final;
const ox::String &itemDisplayName() const noexcept final;
void draw(turbine::Context*) noexcept final;
protected:
ox::Error saveItem() noexcept final;
};
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "paletteeditor.hpp"
namespace nostalgia::core {
AddColorCommand::AddColorCommand(Palette *pal, Color16 color, int idx) noexcept {
m_pal = pal;
m_color = color;
m_idx = idx;
}
int AddColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::AddColor);
}
void AddColorCommand::redo() noexcept {
m_pal->colors.insert(static_cast<std::size_t>(m_idx), m_color);
}
void AddColorCommand::undo() noexcept {
oxIgnoreError(m_pal->colors.erase(static_cast<std::size_t>(m_idx)));
}
RemoveColorCommand::RemoveColorCommand(Palette *pal, Color16 color, int idx) noexcept {
m_pal = pal;
m_color = color;
m_idx = idx;
}
int RemoveColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::RemoveColor);
}
void RemoveColorCommand::redo() noexcept {
oxIgnoreError(m_pal->colors.erase(static_cast<std::size_t>(m_idx)));
}
void RemoveColorCommand::undo() noexcept {
m_pal->colors.insert(static_cast<std::size_t>(m_idx), m_color);
}
UpdateColorCommand::UpdateColorCommand(Palette *pal, int idx, Color16 oldColor, Color16 newColor) noexcept {
m_pal = pal;
m_idx = idx;
m_oldColor = oldColor;
m_newColor = newColor;
//setObsolete(m_oldColor == m_newColor);
}
bool UpdateColorCommand::mergeWith(const UndoCommand *cmd) noexcept {
if (cmd->commandId() != static_cast<int>(PaletteEditorCommandId::UpdateColor)) {
return false;
}
auto ucCmd = static_cast<const UpdateColorCommand*>(cmd);
if (m_idx != ucCmd->m_idx) {
return false;
}
m_newColor = ucCmd->m_newColor;
return true;
}
[[nodiscard]]
int UpdateColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::UpdateColor);
}
void UpdateColorCommand::redo() noexcept {
m_pal->colors[static_cast<std::size_t>(m_idx)] = m_newColor;
}
void UpdateColorCommand::undo() noexcept {
m_pal->colors[static_cast<std::size_t>(m_idx)] = m_oldColor;
}
MoveColorCommand::MoveColorCommand(Palette *pal, std::size_t idx, int offset) noexcept {
m_pal = pal;
m_idx = idx;
m_offset = offset;
}
int MoveColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::MoveColor);
}
void MoveColorCommand::redo() noexcept {
moveColor(static_cast<int>(m_idx), m_offset);
}
void MoveColorCommand::undo() noexcept {
moveColor(static_cast<int>(m_idx) + m_offset, -m_offset);
}
void MoveColorCommand::moveColor(int idx, int offset) noexcept {
const auto c = m_pal->colors[static_cast<std::size_t>(idx)];
oxIgnoreError(m_pal->colors.erase(static_cast<std::size_t>(idx)));
m_pal->colors.insert(static_cast<std::size_t>(idx + offset), c);
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
namespace nostalgia::core {
enum class PaletteEditorCommandId {
AddColor,
RemoveColor,
UpdateColor,
MoveColor,
};
class AddColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
Color16 m_color = 0;
int m_idx = -1;
public:
AddColorCommand(Palette *pal, Color16 color, int idx) noexcept;
~AddColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
void redo() noexcept override;
void undo() noexcept override;
};
class RemoveColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
Color16 m_color = 0;
int m_idx = -1;
public:
RemoveColorCommand(Palette *pal, Color16 color, int idx) noexcept;
~RemoveColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
void redo() noexcept override;
void undo() noexcept override;
};
class UpdateColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
Color16 m_oldColor = 0;
Color16 m_newColor = 0;
int m_idx = -1;
public:
UpdateColorCommand(Palette *pal, int idx, Color16 oldColor, Color16 newColor) noexcept;
~UpdateColorCommand() noexcept override = default;
[[nodiscard]]
bool mergeWith(const UndoCommand *cmd) noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
void redo() noexcept final;
void undo() noexcept final;
};
class MoveColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
std::size_t m_idx = 0;
int m_offset = 0;
public:
MoveColorCommand(Palette *pal, std::size_t idx, int offset) noexcept;
~MoveColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
public:
void redo() noexcept override;
void undo() noexcept override;
private:
void moveColor(int idx, int offset) noexcept;
};
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/memory.hpp>
#include <studio/studio.hpp>
#include "paletteeditor-imgui.hpp"
#include "tilesheeteditor-imgui.hpp"
namespace nostalgia::core {
class StudioModule: public studio::Module {
ox::Vector<studio::EditorMaker> editors(turbine::Context *ctx) const noexcept final {
return {
{
{FileExt_ng},
[ctx](ox::CRStringView path) -> ox::Result<studio::BaseEditor*> {
try {
return ox::make<TileSheetEditorImGui>(ctx, path);
} catch (const ox::Exception &ex) {
return ex.toError();
}
}
},
{
{FileExt_npal},
[ctx](ox::CRStringView path) -> ox::Result<studio::BaseEditor*> {
return PaletteEditorImGui::make(ctx, path);
}
}
};
}
ox::Vector<ox::UniquePtr<studio::ItemMaker>> itemMakers(turbine::Context*) const noexcept final {
ox::Vector<ox::UniquePtr<studio::ItemMaker>> out;
out.emplace_back(ox::make<studio::ItemMakerT<core::TileSheet>>("Tile Sheet", "TileSheets", "ng"));
out.emplace_back(ox::make<studio::ItemMakerT<core::Palette>>("Palette", "Palettes", "npal"));
return out;
}
};
static StudioModule mod;
const studio::Module *studioModule() noexcept {
return &mod;
}
}

View File

@@ -0,0 +1,436 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <lodepng.h>
#include <ox/std/point.hpp>
#include <keel/media.hpp>
#include "tilesheeteditor-imgui.hpp"
namespace nostalgia::core {
template<bool alpha = false>
ox::Error toPngFile(const ox::String &path, const TileSheet::SubSheet &s, const Palette &pal, int8_t bpp) noexcept {
ox::Vector<uint8_t> pixels;
s.readPixelsTo(&pixels, bpp);
const unsigned rows = s.rows == -1 ? static_cast<unsigned>(pixels.size()) / PixelsPerTile : static_cast<unsigned>(s.rows);
const unsigned cols = s.columns == -1 ? 1 : static_cast<unsigned>(s.columns);
const auto width = cols * TileWidth;
const auto height = rows * TileHeight;
constexpr auto bytesPerPixel = alpha ? 4 : 3;
ox::Vector<unsigned char> outData(pixels.size() * bytesPerPixel);
for (auto idx = 0; const auto colorIdx : pixels) {
const auto pt = idxToPt(idx, static_cast<int>(cols));
const auto i = static_cast<unsigned>(pt.y * static_cast<int>(width) + pt.x) * bytesPerPixel;
const auto c = pal.colors[colorIdx];
outData[i + 0] = red32(c);
outData[i + 1] = green32(c);
outData[i + 2] = blue32(c);
if constexpr(alpha) {
outData[i + 3] = colorIdx ? 255 : 0;
}
++idx;
}
constexpr auto fmt = alpha ? LCT_RGBA : LCT_RGB;
return OxError(static_cast<ox::ErrorCode>(lodepng_encode_file(path.c_str(), outData.data(), width, height, fmt, 8)));
}
TileSheetEditorImGui::TileSheetEditorImGui(turbine::Context *ctx, ox::CRStringView path): m_tileSheetEditor(ctx, path) {
m_ctx = ctx;
m_itemPath = path;
const auto lastSlash = ox::find(m_itemPath.rbegin(), m_itemPath.rend(), '/').offset();
m_itemName = m_itemPath.substr(lastSlash + 1);
// init palette idx
const auto &palPath = model()->palPath();
auto sctx = applicationData<studio::StudioContext>(*m_ctx);
const auto &palList = sctx->project->fileList(core::FileExt_npal);
for (std::size_t i = 0; const auto &pal : palList) {
if (palPath == pal) {
m_selectedPaletteIdx = i;
break;
}
++i;
}
// connect signal/slots
undoStack()->changeTriggered.connect(this, &TileSheetEditorImGui::markUnsavedChanges);
m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet);
}
const ox::String &TileSheetEditorImGui::itemName() const noexcept {
return m_itemPath;
}
const ox::String &TileSheetEditorImGui::itemDisplayName() const noexcept {
return m_itemName;
}
void TileSheetEditorImGui::exportFile() {
exportSubhseetToPng();
}
void TileSheetEditorImGui::cut() {
model()->cut();
}
void TileSheetEditorImGui::copy() {
model()->copy();
}
void TileSheetEditorImGui::paste() {
model()->paste();
}
void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) {
if (!down) {
return;
}
if (key == turbine::Key::Escape) {
m_subsheetEditor.close();
}
auto pal = model()->pal();
if (pal) {
const auto colorCnt = pal->colors.size();
if (key == turbine::Key::Alpha_D) {
m_tool = Tool::Draw;
model()->clearSelection();
} else if (key == turbine::Key::Alpha_S) {
m_tool = Tool::Select;
} else if (key == turbine::Key::Alpha_F) {
m_tool = Tool::Fill;
model()->clearSelection();
} else if (key >= turbine::Key::Num_1 && key <= turbine::Key::Num_9 && key <= turbine::Key::Num_0 + colorCnt) {
auto idx = ox::min<std::size_t>(static_cast<uint32_t>(key - turbine::Key::Num_1), colorCnt - 1);
m_tileSheetEditor.setPalIdx(idx);
} else if (key == turbine::Key::Num_0 && colorCnt >= 10) {
auto idx = ox::min<std::size_t>(static_cast<uint32_t>(key - turbine::Key::Num_1 + 9), colorCnt - 1);
m_tileSheetEditor.setPalIdx(idx);
}
}
}
void TileSheetEditorImGui::draw(turbine::Context*) noexcept {
const auto paneSize = ImGui::GetContentRegionAvail();
const auto tileSheetParentSize = ImVec2(paneSize.x - m_palViewWidth, paneSize.y);
const auto fbSize = ox::Vec2(tileSheetParentSize.x - 16, tileSheetParentSize.y - 16);
ImGui::BeginChild("TileSheetView", tileSheetParentSize, true);
{
drawTileSheet(fbSize);
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("Controls", ImVec2(m_palViewWidth - 8, paneSize.y), true);
{
ImGui::BeginChild("ToolBox", ImVec2(m_palViewWidth - 24, 30), true);
{
const auto btnSz = ImVec2(45, 14);
if (ImGui::Selectable("Select", m_tool == Tool::Select, 0, btnSz)) {
m_tool = Tool::Select;
}
ImGui::SameLine();
if (ImGui::Selectable("Draw", m_tool == Tool::Draw, 0, btnSz)) {
m_tool = Tool::Draw;
model()->clearSelection();
}
ImGui::SameLine();
if (ImGui::Selectable("Fill", m_tool == Tool::Fill, 0, btnSz)) {
m_tool = Tool::Fill;
model()->clearSelection();
}
}
ImGui::EndChild();
const auto ySize = paneSize.y - 36;
// draw palette/color picker
ImGui::BeginChild("Palette", ImVec2(m_palViewWidth - 24, ySize / 2.07f), true);
{
drawPaletteSelector();
}
ImGui::EndChild();
ImGui::BeginChild("SubSheets", ImVec2(m_palViewWidth - 24, ySize / 2.03f), true);
{
static constexpr auto btnHeight = 18;
const auto btnSize = ImVec2(18, btnHeight);
if (ImGui::Button("+", btnSize)) {
auto insertOnIdx = model()->activeSubSheetIdx();
const auto &parent = *model()->activeSubSheet();
model()->addSubsheet(insertOnIdx);
insertOnIdx.emplace_back(parent.subsheets.size() - 1);
model()->setActiveSubsheet(insertOnIdx);
}
ImGui::SameLine();
if (ImGui::Button("-", btnSize)) {
const auto &activeSubsheetIdx = model()->activeSubSheetIdx();
if (activeSubsheetIdx.size() > 0) {
model()->rmSubsheet(activeSubsheetIdx);
}
}
ImGui::SameLine();
if (ImGui::Button("Edit", ImVec2(51, btnHeight))) {
showSubsheetEditor();
}
ImGui::SameLine();
if (ImGui::Button("Export", ImVec2(51, btnHeight))) {
exportSubhseetToPng();
}
TileSheet::SubSheetIdx path;
static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
if (ImGui::BeginTable("Subsheets", 3, flags)) {
ImGui::TableSetupColumn("Subsheet", ImGuiTableColumnFlags_NoHide);
ImGui::TableSetupColumn("Columns", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableHeadersRow();
drawSubsheetSelector(&m_tileSheetEditor.img().subsheet, &path);
ImGui::EndTable();
}
}
ImGui::EndChild();
}
ImGui::EndChild();
m_subsheetEditor.draw();
}
void TileSheetEditorImGui::drawSubsheetSelector(TileSheet::SubSheet *subsheet, TileSheet::SubSheetIdx *path) {
ImGui::TableNextRow(0, 5);
using Str = ox::BasicString<100>;
auto pathStr = ox::join<Str>("##", *path).value;
auto lbl = ox::sfmt<Str>("{}##{}", subsheet->name, pathStr);
const auto rowSelected = *path == model()->activeSubSheetIdx();
const auto flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| ImGuiTreeNodeFlags_DefaultOpen
| (subsheet->subsheets.empty() ? ImGuiTreeNodeFlags_Leaf : 0)
| (rowSelected ? ImGuiTreeNodeFlags_Selected : 0);
ImGui::TableNextColumn();
const auto open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
if (ImGui::IsItemClicked()) {
model()->setActiveSubsheet(*path);
}
if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) {
showSubsheetEditor();
}
if (subsheet->subsheets.empty()) {
ImGui::TableNextColumn();
ImGui::Text("%d", subsheet->columns);
ImGui::TableNextColumn();
ImGui::Text("%d", subsheet->rows);
} else {
ImGui::TableNextColumn();
ImGui::Text("--");
ImGui::TableNextColumn();
ImGui::Text("--");
}
if (open) {
for (auto i = 0ul; auto &child : subsheet->subsheets) {
path->push_back(i);
ImGui::PushID(static_cast<int>(i));
drawSubsheetSelector(&child, path);
ImGui::PopID();
path->pop_back();
++i;
}
ImGui::TreePop();
}
}
studio::UndoStack *TileSheetEditorImGui::undoStack() noexcept {
return model()->undoStack();
}
[[nodiscard]]
ox::Vec2 TileSheetEditorImGui::clickPos(const ImVec2 &winPos, ox::Vec2 clickPos) noexcept {
clickPos.x -= winPos.x + 10;
clickPos.y -= winPos.y + 10;
return clickPos;
}
ox::Error TileSheetEditorImGui::saveItem() noexcept {
return model()->saveFile();
}
void TileSheetEditorImGui::showSubsheetEditor() noexcept {
const auto sheet = model()->activeSubSheet();
if (sheet->subsheets.size()) {
m_subsheetEditor.show(sheet->name, -1, -1);
} else {
m_subsheetEditor.show(sheet->name, sheet->columns, sheet->rows);
}
}
void TileSheetEditorImGui::exportSubhseetToPng() noexcept {
auto [path, err] = studio::saveFile({{"PNG", "png"}});
if (err) {
return;
}
// subsheet to png
const auto &img = model()->img();
const auto &s = *model()->activeSubSheet();
const auto &pal = model()->pal();
err = toPngFile(path, s, *pal, img.bpp);
if (err) {
oxErrorf("Tilesheet export failed: {}", toStr(err));
}
}
void TileSheetEditorImGui::drawTileSheet(const ox::Vec2 &fbSize) noexcept {
const auto winPos = ImGui::GetWindowPos();
const auto fbSizei = ox::Size(static_cast<int>(fbSize.x), static_cast<int>(fbSize.y));
if (m_framebuffer.width != fbSizei.width || m_framebuffer.height != fbSizei.height) {
glutils::resizeInitFrameBuffer(&m_framebuffer, fbSizei.width, fbSizei.height);
m_tileSheetEditor.resizeView(fbSize);
} else if (m_tileSheetEditor.updated()) {
m_tileSheetEditor.ackUpdate();
}
glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
// clear screen and draw
glViewport(0, 0, fbSizei.width, fbSizei.height);
m_tileSheetEditor.draw();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const uintptr_t buffId = m_framebuffer.color.id;
ImGui::Image(
reinterpret_cast<void*>(buffId),
static_cast<ImVec2>(fbSize),
ImVec2(0, 1),
ImVec2(1, 0));
// handle input, this must come after drawing
const auto &io = ImGui::GetIO();
const auto mousePos = ox::Vec2(io.MousePos);
if (ImGui::IsItemHovered()) {
const auto wheel = io.MouseWheel;
const auto wheelh = io.MouseWheelH;
if (wheel != 0) {
const auto zoomMod = ox::defines::OS == ox::OS::Darwin ?
io.KeySuper : turbine::buttonDown(*m_ctx, turbine::Key::Mod_Ctrl);
m_tileSheetEditor.scrollV(fbSize, wheel, zoomMod);
}
if (wheelh != 0) {
m_tileSheetEditor.scrollH(fbSize, wheelh);
}
if (io.MouseDown[0] && m_prevMouseDownPos != mousePos) {
m_prevMouseDownPos = mousePos;
switch (m_tool) {
case Tool::Draw:
m_tileSheetEditor.clickDraw(fbSize, clickPos(winPos, mousePos));
break;
case Tool::Fill:
m_tileSheetEditor.clickFill(fbSize, clickPos(winPos, mousePos));
break;
case Tool::Select:
m_tileSheetEditor.clickSelect(fbSize, clickPos(winPos, mousePos));
break;
case Tool::None:
break;
}
}
}
if (ImGui::BeginPopupContextItem("TileMenu", ImGuiPopupFlags_MouseButtonRight)) {
const auto popupPos = ox::Vec2(ImGui::GetWindowPos());
if (ImGui::MenuItem("Insert Tile")) {
m_tileSheetEditor.insertTile(fbSize, clickPos(winPos, popupPos));
}
if (ImGui::MenuItem("Delete Tile")) {
m_tileSheetEditor.deleteTile(fbSize, clickPos(winPos, popupPos));
}
ImGui::EndPopup();
}
if (io.MouseReleased[0]) {
m_prevMouseDownPos = {-1, -1};
m_tileSheetEditor.releaseMouseButton();
}
}
void TileSheetEditorImGui::drawPaletteSelector() noexcept {
auto sctx = applicationData<studio::StudioContext>(*m_ctx);
const auto &files = sctx->project->fileList(core::FileExt_npal);
const auto first = m_selectedPaletteIdx < files.size() ?
files[m_selectedPaletteIdx].c_str() : "";
if (ImGui::BeginCombo("Palette", first, 0)) {
for (auto n = 0u; n < files.size(); n++) {
const auto selected = (m_selectedPaletteIdx == n);
if (ImGui::Selectable(files[n].c_str(), selected) && m_selectedPaletteIdx != n) {
m_selectedPaletteIdx = n;
oxLogError(model()->setPalette(files[n]));
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
// header
if (ImGui::BeginTable("PaletteTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("No.", 0, 0.45f);
ImGui::TableSetupColumn("", 0, 0.22f);
ImGui::TableSetupColumn("Color16", 0, 3);
ImGui::TableHeadersRow();
if (auto pal = m_tileSheetEditor.pal()) {
for (auto i = 0u; auto c: pal->colors) {
ImGui::PushID(static_cast<int>(i));
// Column: color idx
ImGui::TableNextColumn();
const auto label = ox::BString<8>() + (i + 1);
const auto rowSelected = i == m_tileSheetEditor.palIdx();
if (ImGui::Selectable(label.c_str(), rowSelected, ImGuiSelectableFlags_SpanAllColumns)) {
m_tileSheetEditor.setPalIdx(i);
}
// Column: color RGB
ImGui::TableNextColumn();
auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
ImGui::TableNextColumn();
ImGui::Text("(%02d, %02d, %02d)", red16(c), green16(c), blue16(c));
ImGui::TableNextRow();
ImGui::PopID();
++i;
}
}
ImGui::EndTable();
}
}
ox::Error TileSheetEditorImGui::updateActiveSubsheet(const ox::String &name, int cols, int rows) noexcept {
return model()->updateSubsheet(model()->activeSubSheetIdx(), name, cols, rows);
}
ox::Error TileSheetEditorImGui::markUnsavedChanges(const studio::UndoCommand*) noexcept {
setUnsavedChanges(true);
return {};
}
void TileSheetEditorImGui::SubSheetEditor::draw() noexcept {
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
constexpr auto popupName = "Edit Subsheet";
if (!m_show) {
return;
}
ImGui::OpenPopup(popupName);
const auto modSize = m_cols > 0;
const auto popupHeight = modSize ? 125.f : 80.f;
ImGui::SetNextWindowSize(ImVec2(235, popupHeight));
if (ImGui::BeginPopupModal(popupName, &m_show, modalFlags)) {
ImGui::InputText("Name", m_name.data(), m_name.cap());
if (modSize) {
ImGui::InputInt("Columns", &m_cols);
ImGui::InputInt("Rows", &m_rows);
}
if (ImGui::Button("OK")) {
ImGui::CloseCurrentPopup();
m_show = false;
inputSubmitted.emit(m_name, m_cols, m_rows);
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
m_show = false;
}
ImGui::EndPopup();
}
}
void TileSheetEditorImGui::SubSheetEditor::close() noexcept {
m_show = false;
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/model/def.hpp>
#include <ox/std/vec.hpp>
#include <glutils/glutils.hpp>
#include <studio/editor.hpp>
#include "tilesheetpixelgrid.hpp"
#include "tilesheetpixels.hpp"
#include "tilesheeteditorview.hpp"
namespace nostalgia::core {
enum class Tool {
None,
Draw,
Fill,
Select,
};
class TileSheetEditorImGui: public studio::BaseEditor {
private:
class SubSheetEditor {
ox::BString<100> m_name;
int m_cols = 0;
int m_rows = 0;
bool m_show = false;
public:
ox::Signal<ox::Error(const ox::StringView &name, int cols, int rows)> inputSubmitted;
constexpr void show(const ox::String &name, int cols, int rows) noexcept {
m_show = true;
m_name = name.c_str();
m_cols = cols;
m_rows = rows;
}
void draw() noexcept;
void close() noexcept;
};
std::size_t m_selectedPaletteIdx = 0;
turbine::Context *m_ctx = nullptr;
ox::Vector<ox::String> m_paletteList;
SubSheetEditor m_subsheetEditor;
ox::String m_itemPath;
ox::String m_itemName;
glutils::FrameBuffer m_framebuffer;
TileSheetEditorView m_tileSheetEditor;
float m_palViewWidth = 300;
ox::Vec2 m_prevMouseDownPos;
Tool m_tool = Tool::Draw;
public:
TileSheetEditorImGui(turbine::Context *ctx, ox::CRStringView path);
~TileSheetEditorImGui() override = default;
const ox::String &itemName() const noexcept override;
const ox::String &itemDisplayName() const noexcept override;
void exportFile() override;
void cut() override;
void copy() override;
void paste() override;
void keyStateChanged(turbine::Key key, bool down) override;
void draw(turbine::Context*) noexcept override;
void drawSubsheetSelector(TileSheet::SubSheet*, TileSheet::SubSheetIdx *path);
studio::UndoStack *undoStack() noexcept final;
[[nodiscard]]
static ox::Vec2 clickPos(const ImVec2 &winPos, ox::Vec2 clickPos) noexcept;
protected:
ox::Error saveItem() noexcept override;
private:
void showSubsheetEditor() noexcept;
void exportSubhseetToPng() noexcept;
[[nodiscard]]
constexpr auto model() const noexcept {
return m_tileSheetEditor.model();
}
[[nodiscard]]
constexpr auto model() noexcept {
return m_tileSheetEditor.model();
}
void setPalette();
void saveState();
void restoreState();
[[nodiscard]]
ox::String paletteName(const ox::String &palettePath) const;
[[nodiscard]]
ox::String palettePath(const ox::String &palettePath) const;
void drawTileSheet(const ox::Vec2 &fbSize) noexcept;
void drawPaletteSelector() noexcept;
ox::Error updateActiveSubsheet(const ox::String &name, int cols, int rows) noexcept;
// slots
private:
ox::Error updateAfterClicked() noexcept;
ox::Error markUnsavedChanges(const studio::UndoCommand*) noexcept;
};
}

View File

@@ -0,0 +1,825 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/read.hpp>
#include <ox/std/algorithm.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/memory.hpp>
#include <turbine/clipboard.hpp>
#include <keel/media.hpp>
#include "tilesheeteditormodel.hpp"
namespace nostalgia::core {
const Palette TileSheetEditorModel::s_defaultPalette = {
.colors = ox::Vector<Color16>(128),
};
class TileSheetClipboard: public turbine::ClipboardObject<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;
ox::Point pt;
constexpr Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept {
colorIdx = pColorIdx;
pt = pPt;
}
};
protected:
ox::Vector<Pixel> m_pixels;
public:
constexpr void addPixel(const ox::Point &pt, uint16_t colorIdx) noexcept {
m_pixels.emplace_back(colorIdx, pt);
}
[[nodiscard]]
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,
DeleteTile = 4,
InsertTile = 4,
UpdateSubSheet = 5,
Cut = 6,
Paste = 7,
PaletteChange = 8,
};
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;
}
class TileSheetCommand: public studio::UndoCommand {
public:
[[nodiscard]]
virtual const TileSheet::SubSheetIdx &subsheetIdx() const noexcept = 0;
};
class DrawCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
oldPalIdx = pOldPalIdx;
}
};
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change, 8> m_changes;
int m_palIdx = 0;
public:
constexpr DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
m_palIdx = palIdx;
}
constexpr DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Vector<std::size_t> &idxList,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto idx : idxList) {
m_changes.emplace_back(static_cast<uint32_t>(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 = ox::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(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
subsheet.setPixel(m_img.bpp, idx, static_cast<uint8_t>(m_palIdx));
return true;
}
}
return false;
}
constexpr auto append(const ox::Vector<std::size_t> &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, static_cast<uint8_t>(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, static_cast<uint8_t>(c.oldPalIdx));
}
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::Draw);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_subSheetIdx;
}
};
template<CommandId CommandId>
class CutPasteCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t newPalIdx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pNewPalIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
newPalIdx = pNewPalIdx;
oldPalIdx = pOldPalIdx;
}
};
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change> m_changes;
public:
constexpr CutPasteCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Point &dstStart,
const ox::Point &dstEnd,
const TileSheetClipboard &cb) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
const auto &subsheet = m_img.getSubSheet(m_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(static_cast<uint32_t>(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, static_cast<uint8_t>(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, static_cast<uint8_t>(c.oldPalIdx));
}
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_subSheetIdx;
}
};
class AddSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_parentIdx;
ox::Vector<TileSheet::SubSheetIdx, 2> m_addedSheets;
public:
constexpr AddSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx parentIdx) noexcept:
m_img(img),
m_parentIdx(std::move(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(0u);
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(m_img.idIt++, ox::sfmt("Subsheet {}", i), 1, 1, m_img.bpp);
} else {
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 0", parent.columns, parent.rows, std::move(parent.pixels));
parent.rows = 0;
parent.columns = 0;
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 1", 1, 1, m_img.bpp);
}
}
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);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_parentIdx;
}
};
class RmSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheetIdx m_parentIdx;
TileSheet::SubSheet m_sheet;
public:
constexpr RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept:
m_img(img),
m_idx(std::move(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);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_idx;
}
};
class InsertTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_insertPos = 0;
std::size_t m_insertCnt = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
constexpr InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_insertPos = tileIdx * bytesPerTile;
m_insertCnt = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_insertCnt);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + p.size() - m_insertCnt;
const auto sz = m_insertCnt * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void redo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dstPos = m_insertPos + m_insertCnt;
const auto dst = p.data() + dstPos;
const auto src = p.data() + m_insertPos;
ox_memmove(dst, src, p.size() - dstPos);
ox_memset(src, 0, m_insertCnt * sizeof(decltype(p[0])));
}
void undo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto srcIdx = m_insertPos + m_insertCnt;
const auto src = p.data() + srcIdx;
const auto dst1 = p.data() + m_insertPos;
const auto dst2 = p.data() + p.size() - m_insertCnt;
const auto sz = p.size() - srcIdx;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::InsertTile);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_idx;
}
};
class DeleteTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_deletePos = 0;
std::size_t m_deleteSz = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
constexpr DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_deletePos = tileIdx * bytesPerTile;
m_deleteSz = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_deleteSz);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + m_deletePos;
const auto sz = m_deleteSz * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void redo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto srcPos = m_deletePos + m_deleteSz;
const auto src = p.data() + srcPos;
const auto dst1 = p.data() + m_deletePos;
const auto dst2 = p.data() + (p.size() - m_deleteSz);
ox_memmove(dst1, src, p.size() - srcPos);
ox_memset(dst2, 0, m_deleteSz * sizeof(decltype(p[0])));
}
void undo() noexcept final {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto src = p.data() + m_deletePos;
const auto dst1 = p.data() + m_deletePos + m_deleteSz;
const auto dst2 = src;
const auto sz = p.size() - m_deletePos - m_deleteSz;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
[[nodiscard]]
int commandId() const noexcept final {
return static_cast<int>(CommandId::DeleteTile);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_idx;
}
};
class UpdateSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
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,
TileSheet::SubSheetIdx idx,
const ox::String &name,
int cols,
int rows) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_sheet = m_img.getSubSheet(m_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);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_idx;
}
};
class PaletteChangeCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
ox::FileAddress m_oldPalette;
ox::FileAddress m_newPalette;
public:
PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::CRStringView newPalette) noexcept:
m_img(img),
m_idx(std::move(idx)) {
m_oldPalette = m_img.defaultPalette;
m_newPalette = ox::FileAddress(ox::sfmt<ox::BString<43>>("uuid://{}", 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<int>(CommandId::PaletteChange);
}
[[nodiscard]]
const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
return m_idx;
}
};
TileSheetEditorModel::TileSheetEditorModel(turbine::Context *ctx, ox::String path):
m_ctx(ctx),
m_path(std::move(path)) {
oxRequireT(img, readObj<TileSheet>(ctx, m_path));
m_img = *img;
if (m_img.defaultPalette) {
oxThrowError(readObj<Palette>(ctx, m_img.defaultPalette).moveTo(&m_pal));
}
m_pal.updated.connect(this, &TileSheetEditorModel::markUpdated);
m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdatedCmdId);
}
void TileSheetEditorModel::cut() {
TileSheetClipboard blankCb;
auto cb = ox::make_unique<TileSheetClipboard>();
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 = ox::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 == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
turbine::setClipboardObject(*m_ctx, std::move(cb));
pushCommand(ox::make<CutPasteCommand<CommandId::Cut>>(m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb));
}
void TileSheetEditorModel::copy() {
auto cb = ox::make_unique<TileSheetClipboard>();
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
auto pt = ox::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);
}
}
turbine::setClipboardObject(*m_ctx, std::move(cb));
}
void TileSheetEditorModel::paste() {
auto [cb, err] = turbine::getClipboardObject<TileSheetClipboard>(*m_ctx);
if (err) {
oxLogError(err);
oxErrf("Could not read clipboard: {}", toStr(err));
return;
}
const auto s = activeSubSheet();
const auto pt1 = m_selectionOrigin == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
pushCommand(ox::make<CutPasteCommand<CommandId::Paste>>(m_img, m_activeSubsSheetIdx, pt1, pt2, *cb));
}
ox::StringView TileSheetEditorModel::palPath() const noexcept {
auto [path, err] = m_img.defaultPalette.getPath();
if (err) {
return {};
}
constexpr ox::StringView uuidPrefix = "uuid://";
if (ox::beginsWith(path, uuidPrefix)) {
auto uuid = ox::StringView(path + uuidPrefix.bytes(), ox_strlen(path) - uuidPrefix.bytes());
auto out = m_ctx->uuidToPath.at(uuid);
if (out.error) {
return {};
}
return *out.value;
} else {
return path;
}
}
ox::Error TileSheetEditorModel::setPalette(const ox::String &path) noexcept {
oxRequire(uuid, m_ctx->pathToUuid.at(path));
pushCommand(ox::make<PaletteChangeCommand>(activeSubSheetIdx(), m_img, uuid->toString()));
return {};
}
void TileSheetEditorModel::drawCommand(const ox::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(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
}
}
void TileSheetEditorModel::endDrawCommand() noexcept {
m_ongoingDrawCommand = nullptr;
}
void TileSheetEditorModel::addSubsheet(const TileSheet::SubSheetIdx &parentIdx) noexcept {
pushCommand(ox::make<AddSubSheetCommand>(m_img, parentIdx));
}
void TileSheetEditorModel::rmSubsheet(const TileSheet::SubSheetIdx &idx) noexcept {
pushCommand(ox::make<RmSubSheetCommand>(m_img, idx));
}
void TileSheetEditorModel::insertTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<InsertTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
void TileSheetEditorModel::deleteTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<DeleteTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
ox::Error TileSheetEditorModel::updateSubsheet(const TileSheet::SubSheetIdx &idx, const ox::String &name, int cols, int rows) noexcept {
pushCommand(ox::make<UpdateSubSheetCommand>(m_img, idx, name, cols, rows));
return {};
}
void TileSheetEditorModel::setActiveSubsheet(const TileSheet::SubSheetIdx &idx) noexcept {
m_activeSubsSheetIdx = idx;
this->activeSubsheetChanged.emit(m_activeSubsSheetIdx);
}
void TileSheetEditorModel::fill(const ox::Point &pt, int palIdx) noexcept {
const auto &s = m_img.getSubSheet(m_activeSubsSheetIdx);
// build idx list
ox::Array<bool, PixelsPerTile> 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<std::size_t> 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(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idxList, palIdx));
}
}
void TileSheetEditorModel::select(const ox::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::markUpdatedCmdId(const studio::UndoCommand *cmd) noexcept {
m_updated = true;
const auto cmdId = cmd->commandId();
if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
oxReturnError(readObj<Palette>(m_ctx, ox::StringView(m_img.defaultPalette.getPath().value)).moveTo(&m_pal));
}
auto tsCmd = dynamic_cast<const TileSheetCommand*>(cmd);
auto idx = m_img.validateSubSheetIdx(tsCmd->subsheetIdx());
if (idx != m_activeSubsSheetIdx) {
setActiveSubsheet(idx);
}
return {};
}
ox::Error TileSheetEditorModel::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorModel::ackUpdate() noexcept {
m_updated = false;
}
ox::Error TileSheetEditorModel::saveFile() noexcept {
const auto sctx = applicationData<studio::StudioContext>(*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(static_cast<int>(idx), s->columns);
return m_selectionBounds.contains(pt);
}
void TileSheetEditorModel::getFillPixels(bool *pixels, const ox::Point &pt, int oldColor) const noexcept {
const auto &activeSubSheet = *this->activeSubSheet();
const auto tileIdx = [activeSubSheet](const ox::Point &pt) noexcept {
return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
};
// get points
const auto leftPt = pt + ox::Point(-1, 0);
const auto rightPt = pt + ox::Point(1, 0);
const auto topPt = pt + ox::Point(0, -1);
const auto bottomPt = pt + ox::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<DrawCommand*>(cmd);
m_updated = true;
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/bounds.hpp>
#include <ox/std/point.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/string.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
namespace nostalgia::core {
class TileSheetEditorModel: public ox::SignalHandler {
public:
ox::Signal<ox::Error(const TileSheet::SubSheetIdx&)> activeSubsheetChanged;
private:
static const Palette s_defaultPalette;
TileSheet m_img;
TileSheet::SubSheetIdx m_activeSubsSheetIdx;
keel::AssetRef<Palette> m_pal;
studio::UndoStack m_undoStack;
class DrawCommand *m_ongoingDrawCommand = nullptr;
bool m_updated = false;
turbine::Context *m_ctx = nullptr;
ox::String m_path;
bool m_selectionOngoing = false;
ox::Point m_selectionOrigin = {-1, -1};
ox::Bounds m_selectionBounds = {{-1, -1}, {-1, -1}};
public:
TileSheetEditorModel(turbine::Context *ctx, 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;
[[nodiscard]]
ox::StringView palPath() const noexcept;
ox::Error setPalette(const ox::String &path) noexcept;
void drawCommand(const ox::Point &pt, std::size_t palIdx) noexcept;
void endDrawCommand() noexcept;
void addSubsheet(const TileSheet::SubSheetIdx &parentIdx) noexcept;
void rmSubsheet(const TileSheet::SubSheetIdx &idx) noexcept;
void insertTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept;
void deleteTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept;
ox::Error updateSubsheet(const TileSheet::SubSheetIdx &idx, const ox::String &name, int cols, int rows) noexcept;
void setActiveSubsheet(const TileSheet::SubSheetIdx&) noexcept;
[[nodiscard]]
constexpr const TileSheet::SubSheet *activeSubSheet() const noexcept {
auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
return &activeSubSheet;
}
[[nodiscard]]
constexpr TileSheet::SubSheet *activeSubSheet() noexcept {
auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
return &activeSubSheet;
}
[[nodiscard]]
constexpr const TileSheet::SubSheetIdx &activeSubSheetIdx() const noexcept {
return m_activeSubsSheetIdx;
}
void fill(const ox::Point &pt, int palIdx) noexcept;
void select(const ox::Point &pt) noexcept;
void completeSelection() noexcept;
void clearSelection() noexcept;
[[nodiscard]]
bool updated() const noexcept;
ox::Error markUpdatedCmdId(const studio::UndoCommand *cmd) noexcept;
ox::Error markUpdated() noexcept;
void ackUpdate() noexcept;
ox::Error saveFile() noexcept;
[[nodiscard]]
constexpr studio::UndoStack *undoStack() noexcept;
bool pixelSelected(std::size_t idx) const noexcept;
protected:
void getFillPixels(bool *pixels, const ox::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 {
if (m_pal) {
return m_pal.get();
}
return &s_defaultPalette;
}
constexpr studio::UndoStack *TileSheetEditorModel::undoStack() noexcept {
return &m_undoStack;
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/point.hpp>
#include <keel/media.hpp>
#include <nostalgia/core/consts.hpp>
#include "tilesheeteditorview.hpp"
namespace nostalgia::core {
TileSheetEditorView::TileSheetEditorView(turbine::Context *ctx, ox::CRStringView path):
m_model(ctx, path), m_pixelsDrawer(&m_model) {
// build shaders
oxThrowError(m_pixelsDrawer.buildShader());
oxThrowError(m_pixelGridDrawer.buildShader());
m_model.activeSubsheetChanged.connect(this, &TileSheetEditorView::setActiveSubsheet);
}
void TileSheetEditorView::draw() noexcept {
constexpr Color32 bgColor = 0x717d7e;
glClearColor(redf(bgColor), greenf(bgColor), bluef(bgColor), 1.f);
glClear(GL_COLOR_BUFFER_BIT);
m_pixelsDrawer.draw(updated(), m_scrollOffset);
m_pixelGridDrawer.draw(updated(), m_scrollOffset);
}
void TileSheetEditorView::scrollV(const ox::Vec2 &paneSz, float wheel, bool zoomMod) noexcept {
const auto pixelSize = m_pixelsDrawer.pixelSize(paneSz);
const ImVec2 sheetSize(pixelSize.x * static_cast<float>(m_model.activeSubSheet()->columns) * TileWidth,
pixelSize.y * static_cast<float>(m_model.activeSubSheet()->rows) * TileHeight);
if (zoomMod) {
m_pixelSizeMod = ox::clamp(m_pixelSizeMod + wheel * 0.02f, 0.55f, 2.f);
m_pixelsDrawer.setPixelSizeMod(m_pixelSizeMod);
m_pixelGridDrawer.setPixelSizeMod(m_pixelSizeMod);
m_updated = true;
} else {
m_scrollOffset.y -= wheel * 0.1f;
}
// adjust scroll offset in both cases because the image can be zoomed
// or scrolled off screen
m_scrollOffset.y = ox::clamp(m_scrollOffset.y, 0.f, sheetSize.y / 2);
}
void TileSheetEditorView::scrollH(const ox::Vec2 &paneSz, float wheelh) noexcept {
const auto pixelSize = m_pixelsDrawer.pixelSize(paneSz);
const ImVec2 sheetSize(pixelSize.x * static_cast<float>(m_model.activeSubSheet()->columns) * TileWidth,
pixelSize.y * static_cast<float>(m_model.activeSubSheet()->rows) * TileHeight);
m_scrollOffset.x += wheelh * 0.1f;
m_scrollOffset.x = ox::clamp(m_scrollOffset.x, -(sheetSize.x / 2), 0.f);
}
void TileSheetEditorView::insertTile(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
const auto s = m_model.activeSubSheet();
const auto tileIdx = ptToIdx(pt, s->columns) / PixelsPerTile;
m_model.insertTiles(m_model.activeSubSheetIdx(), tileIdx, 1);
}
void TileSheetEditorView::deleteTile(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
const auto s = m_model.activeSubSheet();
const auto tileIdx = ptToIdx(pt, s->columns) / PixelsPerTile;
m_model.deleteTiles(m_model.activeSubSheetIdx(), tileIdx, 1);
}
void TileSheetEditorView::clickDraw(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
m_model.drawCommand(pt, m_palIdx);
}
void TileSheetEditorView::clickSelect(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
m_model.select(pt);
}
void TileSheetEditorView::clickFill(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
m_model.fill(pt, static_cast<int>(m_palIdx));
}
void TileSheetEditorView::releaseMouseButton() noexcept {
m_model.endDrawCommand();
m_model.completeSelection();
}
void TileSheetEditorView::resizeView(const ox::Vec2 &sz) noexcept {
m_viewSize = sz;
initView();
}
bool TileSheetEditorView::updated() const noexcept {
return m_updated || m_model.updated();
}
ox::Error TileSheetEditorView::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorView::ackUpdate() noexcept {
m_updated = false;
m_pixelsDrawer.update(m_viewSize);
m_pixelGridDrawer.update(m_viewSize, *m_model.activeSubSheet());
m_model.ackUpdate();
}
void TileSheetEditorView::initView() noexcept {
m_pixelsDrawer.initBufferSet(m_viewSize);
m_pixelGridDrawer.initBufferSet(m_viewSize, *m_model.activeSubSheet());
}
ox::Point TileSheetEditorView::clickPoint(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) const noexcept {
auto [x, y] = clickPos;
const auto pixDrawSz = m_pixelsDrawer.pixelSize(paneSize);
x /= paneSize.x;
y /= paneSize.y;
x += -m_scrollOffset.x / 2;
y += m_scrollOffset.y / 2;
x /= pixDrawSz.x;
y /= pixDrawSz.y;
return {static_cast<int>(x * 2), static_cast<int>(y * 2)};
}
ox::Error TileSheetEditorView::setActiveSubsheet(const TileSheet::SubSheetIdx&) noexcept {
initView();
return {};
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vec.hpp>
#include <ox/model/def.hpp>
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
#include "tilesheeteditormodel.hpp"
#include "tilesheetpixelgrid.hpp"
#include "tilesheetpixels.hpp"
namespace nostalgia::core {
enum class TileSheetTool: int {
Select,
Draw,
Fill,
};
[[nodiscard]]
constexpr auto toString(TileSheetTool t) noexcept {
switch (t) {
case TileSheetTool::Select:
return "Select";
case TileSheetTool::Draw:
return "Draw";
case TileSheetTool::Fill:
return "Fill";
}
return "";
}
class TileSheetEditorView: public ox::SignalHandler {
private:
TileSheetEditorModel m_model;
TileSheetGrid m_pixelGridDrawer;
TileSheetPixels m_pixelsDrawer;
ox::Vec2 m_viewSize;
float m_pixelSizeMod = 1;
bool m_updated = false;
ox::Vec2 m_scrollOffset;
std::size_t m_palIdx = 0;
public:
TileSheetEditorView(turbine::Context *ctx, ox::CRStringView path);
~TileSheetEditorView() override = default;
void draw() noexcept;
void insertTile(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept;
void deleteTile(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept;
void clickDraw(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept;
void clickSelect(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept;
void clickFill(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) noexcept;
void releaseMouseButton() noexcept;
void scrollV(const ox::Vec2 &paneSz, float wheel, bool zoomMod) noexcept;
void scrollH(const ox::Vec2 &paneSz, float wheel) noexcept;
void resizeView(const ox::Vec2 &sz) noexcept;
[[nodiscard]]
constexpr const TileSheet &img() const noexcept;
[[nodiscard]]
constexpr TileSheet &img() noexcept;
[[nodiscard]]
constexpr const Palette *pal() const noexcept;
[[nodiscard]]
constexpr auto *model() noexcept {
return &m_model;
}
[[nodiscard]]
constexpr auto *model() const noexcept {
return &m_model;
}
constexpr auto setPalIdx(auto palIdx) noexcept {
m_palIdx = palIdx;
}
[[nodiscard]]
constexpr auto palIdx() const noexcept {
return m_palIdx;
}
[[nodiscard]]
bool updated() const noexcept;
ox::Error markUpdated() noexcept;
void ackUpdate() noexcept;
private:
void initView() noexcept;
ox::Point clickPoint(const ox::Vec2 &paneSize, const ox::Vec2 &clickPos) const noexcept;
ox::Error setActiveSubsheet(const TileSheet::SubSheetIdx &idx) noexcept;
};
constexpr const TileSheet &TileSheetEditorView::img() const noexcept {
return m_model.img();
}
constexpr TileSheet &TileSheetEditorView::img() noexcept {
return m_model.img();
}
constexpr const Palette *TileSheetEditorView::pal() const noexcept {
return m_model.pal();
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/write.hpp>
#include <nostalgia/core/consts.hpp>
#include "tilesheetpixelgrid.hpp"
namespace nostalgia::core {
void TileSheetGrid::setPixelSizeMod(float sm) noexcept {
m_pixelSizeMod = sm;
}
ox::Error TileSheetGrid::buildShader() noexcept {
const auto pixelLineVshad = ox::sfmt(VShad, glutils::GlslVersion);
const auto pixelLineFshad = ox::sfmt(FShad, glutils::GlslVersion);
const auto pixelLineGshad = ox::sfmt(GShad, glutils::GlslVersion);
return glutils::buildShaderProgram(pixelLineVshad, pixelLineFshad, pixelLineGshad).moveTo(&m_shader);
}
void TileSheetGrid::draw(bool update, const ox::Vec2 &scroll) noexcept {
glLineWidth(3 * m_pixelSizeMod * 0.5f);
glUseProgram(m_shader);
glBindVertexArray(m_bufferSet.vao);
if (update) {
glutils::sendVbo(m_bufferSet);
}
const auto uniformScroll = glGetUniformLocation(m_shader, "gScroll");
glUniform2f(uniformScroll, scroll.x, scroll.y);
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(m_bufferSet.vertices.size() / VertexVboRowLength));
glBindVertexArray(0);
glUseProgram(0);
}
void TileSheetGrid::initBufferSet(const ox::Vec2 &paneSize, const TileSheet::SubSheet &subsheet) noexcept {
// vao
m_bufferSet.vao = glutils::generateVertexArrayObject();
glBindVertexArray(m_bufferSet.vao);
// vbo
m_bufferSet.vbo = glutils::generateBuffer();
setBufferObjects(paneSize, subsheet);
glutils::sendVbo(m_bufferSet);
// vbo layout
const auto pt1Attr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPt1"));
glEnableVertexAttribArray(pt1Attr);
glVertexAttribPointer(pt1Attr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float), nullptr);
const auto pt2Attr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPt2"));
glEnableVertexAttribArray(pt2Attr);
glVertexAttribPointer(pt2Attr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
const auto colorAttr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vColor"));
glEnableVertexAttribArray(colorAttr);
glVertexAttribPointer(colorAttr, 3, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
reinterpret_cast<void*>(4 * sizeof(float)));
}
void TileSheetGrid::update(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept {
glBindVertexArray(m_bufferSet.vao);
setBufferObjects(paneSize, subsheet);
glutils::sendVbo(m_bufferSet);
glutils::sendEbo(m_bufferSet);
}
void TileSheetGrid::setBufferObject(ox::Point pt1, ox::Point pt2, Color32 c, float *vbo, const ox::Vec2 &pixSize) noexcept {
const auto x1 = static_cast<float>(pt1.x) * pixSize.x - 1.f;
const auto y1 = 1.f - static_cast<float>(pt1.y) * pixSize.y;
const auto x2 = static_cast<float>(pt2.x) * pixSize.x - 1.f;
const auto y2 = 1.f - static_cast<float>(pt2.y) * pixSize.y;
// don't worry, this memcpy gets optimized to something much more ideal
const ox::Array<float, VertexVboLength> vertices = {x1, y1, x2, y2, redf(c), greenf(c), bluef(c)};
memcpy(vbo, vertices.data(), sizeof(vertices));
}
void TileSheetGrid::setBufferObjects(const ox::Vec2 &paneSize, const TileSheet::SubSheet &subsheet) noexcept {
const auto pixSize = pixelSize(paneSize);
const auto set = [&](std::size_t i, ox::Point pt1, ox::Point pt2, Color32 c) {
const auto vbo = &m_bufferSet.vertices[i * VertexVboLength];
setBufferObject(pt1, pt2, c, vbo, pixSize);
};
// set buffer length
const auto width = subsheet.columns * TileWidth;
const auto height = subsheet.rows * TileHeight;
const auto tileCnt = static_cast<unsigned>(subsheet.columns + subsheet.rows);
const auto pixelCnt = static_cast<unsigned>(width + height);
m_bufferSet.vertices.resize(static_cast<std::size_t>(tileCnt + pixelCnt + 4) * VertexVboLength);
// set buffer
std::size_t i = 0;
// pixel outlines
constexpr auto pixOutlineColor = color32(0.4431f, 0.4901f, 0.4941f);
for (auto x = 0; x < subsheet.columns * TileWidth + 1; ++x) {
set(i, {x, 0}, {x, subsheet.rows * TileHeight}, pixOutlineColor);
++i;
}
for (auto y = 0; y < subsheet.rows * TileHeight + 1; ++y) {
set(i, {0, y}, {subsheet.columns * TileWidth, y}, pixOutlineColor);
++i;
}
// tile outlines
constexpr auto tileOutlineColor = color32(0.f, 0.f, 0.f);
for (auto x = 0; x < subsheet.columns * TileWidth + 1; x += TileWidth) {
set(i, {x, 0}, {x, subsheet.rows * TileHeight}, tileOutlineColor);
++i;
}
for (auto y = 0; y < subsheet.rows * TileHeight + 1; y += TileHeight) {
set(i, {0, y}, {subsheet.columns * TileWidth, y}, tileOutlineColor);
++i;
}
}
ox::Vec2 TileSheetGrid::pixelSize(const ox::Vec2 &paneSize) const noexcept {
const auto [sw, sh] = paneSize;
constexpr float ymod = 0.35f / 10.0f;
const auto xmod = ymod * sh / sw;
return {xmod * m_pixelSizeMod, ymod * m_pixelSizeMod};
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
namespace nostalgia::core {
class TileSheetGrid {
private:
static constexpr auto VertexVboRows = 1;
static constexpr auto VertexVboRowLength = 7;
static constexpr auto VertexVboLength = VertexVboRows * VertexVboRowLength;
static constexpr auto VShad = R"glsl(
{}
in vec2 vPt1;
in vec2 vPt2;
in vec3 vColor;
out vec2 gPt2;
out vec3 gColor;
void main() {
gColor = vColor;
gl_Position = vec4(vPt1, 0.0, 1.0);
gPt2 = vPt2;
})glsl";
static constexpr auto FShad = R"glsl(
{}
in vec3 fColor;
out vec4 outColor;
void main() {
outColor = vec4(fColor, 1);
//outColor = vec4(0.4431, 0.4901, 0.4941, 1.0);
})glsl";
static constexpr auto GShad = R"glsl(
{}
layout(points) in;
layout(line_strip, max_vertices = 2) out;
in vec3 gColor[];
in vec2 gPt2[];
out vec3 fColor;
uniform vec2 gScroll;
void main() {
fColor = gColor[0];
gl_Position = gl_in[0].gl_Position + vec4(gScroll, 0, 0);
EmitVertex();
gl_Position = vec4(gPt2[0] + gScroll, 0, 1);
EmitVertex();
EndPrimitive();
})glsl";
glutils::GLProgram m_shader;
glutils::BufferSet m_bufferSet;
float m_pixelSizeMod = 1;
public:
void setPixelSizeMod(float sm) noexcept;
ox::Error buildShader() noexcept;
void draw(bool update, const ox::Vec2 &scroll) noexcept;
void initBufferSet(const ox::Vec2 &paneSize, TileSheet::SubSheet const&subsheet) noexcept;
void update(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept;
private:
static void setBufferObject(ox::Point pt1, ox::Point pt2, Color32 c, float *vbo, const ox::Vec2 &pixSize) noexcept;
void setBufferObjects(const ox::Vec2 &paneSize, const TileSheet::SubSheet &subsheet) noexcept;
[[nodiscard]]
ox::Vec2 pixelSize(const ox::Vec2 &paneSize) const noexcept;
};
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/consts.hpp>
#include <nostalgia/core/ptidxconv.hpp>
#include "tilesheeteditormodel.hpp"
#include "tilesheetpixels.hpp"
namespace nostalgia::core {
TileSheetPixels::TileSheetPixels(TileSheetEditorModel *model) noexcept: m_model(model) {
}
void TileSheetPixels::setPixelSizeMod(float sm) noexcept {
m_pixelSizeMod = sm;
}
ox::Error TileSheetPixels::buildShader() noexcept {
const auto Vshad = ox::sfmt(VShad, glutils::GlslVersion);
const auto Fshad = ox::sfmt(FShad, glutils::GlslVersion);
return glutils::buildShaderProgram(Vshad, Fshad).moveTo(&m_shader);
}
void TileSheetPixels::draw(bool update, const ox::Vec2 &scroll) noexcept {
glUseProgram(m_shader);
glBindVertexArray(m_bufferSet.vao);
if (update) {
glutils::sendVbo(m_bufferSet);
}
const auto uniformScroll = glGetUniformLocation(m_shader, "vScroll");
glUniform2f(uniformScroll, scroll.x, scroll.y);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(m_bufferSet.elements.size()), GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
glUseProgram(0);
}
void TileSheetPixels::initBufferSet(ox::Vec2 const&paneSize) noexcept {
m_bufferSet.vao = glutils::generateVertexArrayObject();
m_bufferSet.vbo = glutils::generateBuffer();
m_bufferSet.ebo = glutils::generateBuffer();
update(paneSize);
// vbo layout
const auto posAttr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPosition"));
glEnableVertexAttribArray(posAttr);
glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float), nullptr);
const auto colorAttr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vColor"));
glEnableVertexAttribArray(colorAttr);
glVertexAttribPointer(colorAttr, 3, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
}
void TileSheetPixels::update(ox::Vec2 const&paneSize) noexcept {
glBindVertexArray(m_bufferSet.vao);
setBufferObjects(paneSize);
glutils::sendVbo(m_bufferSet);
glutils::sendEbo(m_bufferSet);
}
ox::Vec2 TileSheetPixels::pixelSize(const ox::Vec2 &paneSize) const noexcept {
const auto [sw, sh] = paneSize;
constexpr float ymod = 0.35f / 10.0f;
const auto xmod = ymod * sh / sw;
return {xmod * m_pixelSizeMod, ymod * m_pixelSizeMod};
}
void TileSheetPixels::setPixelBufferObject(
ox::Vec2 const&paneSize,
unsigned vertexRow,
float x, float y,
Color16 color,
float *vbo,
GLuint *ebo) const noexcept {
const auto [xmod, ymod] = pixelSize(paneSize);
x *= xmod;
y *= -ymod;
x -= 1.0f;
y += 1.0f - ymod;
const auto r = redf(color), g = greenf(color), b = bluef(color);
// don't worry, these memcpys gets optimized to something much more ideal
const ox::Array<float, VertexVboLength> vertices = {
x, y, r, g, b, // bottom left
x + xmod, y, r, g, b, // bottom right
x + xmod, y + ymod, r, g, b, // top right
x, y + ymod, r, g, b, // top left
};
memcpy(vbo, vertices.data(), sizeof(vertices));
const ox::Array<GLuint, VertexEboLength> elms = {
vertexRow + 0, vertexRow + 1, vertexRow + 2,
vertexRow + 2, vertexRow + 3, vertexRow + 0,
};
memcpy(ebo, elms.data(), sizeof(elms));
}
void TileSheetPixels::setBufferObjects(const ox::Vec2 &paneSize) noexcept {
// set buffer lengths
const auto subSheet = m_model->activeSubSheet();
const auto pal = m_model->pal();
const auto width = subSheet->columns * TileWidth;
const auto height = subSheet->rows * TileHeight;
const auto pixels = static_cast<unsigned>(width * height);
m_bufferSet.vertices.resize(pixels * VertexVboLength);
m_bufferSet.elements.resize(pixels * VertexEboLength);
// set pixels
subSheet->walkPixels(m_model->img().bpp, [&](std::size_t i, uint8_t p) {
auto color = pal->color(p);
const auto pt = idxToPt(static_cast<int>(i), subSheet->columns);
const auto fx = static_cast<float>(pt.x);
const auto fy = static_cast<float>(pt.y);
const auto vbo = &m_bufferSet.vertices[i * VertexVboLength];
const auto ebo = &m_bufferSet.elements[i * VertexEboLength];
if (i * VertexVboLength + VertexVboLength > m_bufferSet.vertices.size()) {
return;
}
if (i * VertexEboLength + VertexEboLength > m_bufferSet.elements.size()) {
return;
}
if (m_model->pixelSelected(i)) {
const auto r = red16(color) / 2;
const auto g = (green16(color) + 20) / 2;
const auto b = (blue16(color) + 31) / 2;
color = color16(r, g, b);
}
setPixelBufferObject(paneSize, static_cast<unsigned>(i * VertexVboRows), fx, fy, color, vbo, ebo);
});
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vec.hpp>
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
namespace nostalgia::core {
class TileSheetPixels {
private:
static constexpr auto VertexVboRows = 4;
static constexpr auto VertexVboRowLength = 5;
static constexpr auto VertexVboLength = VertexVboRows * VertexVboRowLength;
static constexpr auto VertexEboLength = 6;
static constexpr auto VShad = R"(
{}
in vec2 vPosition;
in vec3 vColor;
out vec3 fColor;
uniform vec2 vScroll;
void main() {
gl_Position = vec4(vPosition + vScroll, 0.0, 1.0);
fColor = vColor;
})";
static constexpr auto FShad = R"(
{}
in vec3 fColor;
out vec4 outColor;
void main() {
//outColor = vec4(0.0, 0.7, 1.0, 1.0);
outColor = vec4(fColor, 1.0);
})";
float m_pixelSizeMod = 1;
glutils::GLProgram m_shader;
glutils::BufferSet m_bufferSet;
const class TileSheetEditorModel *m_model = nullptr;
public:
explicit TileSheetPixels(class TileSheetEditorModel *model) noexcept;
void setPixelSizeMod(float sm) noexcept;
ox::Error buildShader() noexcept;
void draw(bool update, const ox::Vec2 &scroll) noexcept;
void initBufferSet(ox::Vec2 const&paneSize) noexcept;
void update(ox::Vec2 const&paneSize) noexcept;
[[nodiscard]]
ox::Vec2 pixelSize(const ox::Vec2 &paneSize) const noexcept;
private:
void setPixelBufferObject(const ox::Vec2 &paneS, unsigned vertexRow, float x, float y, Color16 color, float *vbo, GLuint *ebo) const noexcept;
void setBufferObjects(const ox::Vec2 &paneS) noexcept;
};
}