[nostalgia] Move modules into modules directory
This commit is contained in:
43
src/nostalgia/modules/core/src/studio/CMakeLists.txt
Normal file
43
src/nostalgia/modules/core/src/studio/CMakeLists.txt
Normal 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}
|
||||
)
|
||||
155
src/nostalgia/modules/core/src/studio/paletteeditor-imgui.cpp
Normal file
155
src/nostalgia/modules/core/src/studio/paletteeditor-imgui.cpp
Normal 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 {};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
105
src/nostalgia/modules/core/src/studio/paletteeditor.cpp
Normal file
105
src/nostalgia/modules/core/src/studio/paletteeditor.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
108
src/nostalgia/modules/core/src/studio/paletteeditor.hpp
Normal file
108
src/nostalgia/modules/core/src/studio/paletteeditor.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
||||
49
src/nostalgia/modules/core/src/studio/studiomodule.cpp
Normal file
49
src/nostalgia/modules/core/src/studio/studiomodule.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
436
src/nostalgia/modules/core/src/studio/tilesheeteditor-imgui.cpp
Normal file
436
src/nostalgia/modules/core/src/studio/tilesheeteditor-imgui.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
129
src/nostalgia/modules/core/src/studio/tilesheeteditor-imgui.hpp
Normal file
129
src/nostalgia/modules/core/src/studio/tilesheeteditor-imgui.hpp
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
825
src/nostalgia/modules/core/src/studio/tilesheeteditormodel.cpp
Normal file
825
src/nostalgia/modules/core/src/studio/tilesheeteditormodel.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
158
src/nostalgia/modules/core/src/studio/tilesheeteditormodel.hpp
Normal file
158
src/nostalgia/modules/core/src/studio/tilesheeteditormodel.hpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
132
src/nostalgia/modules/core/src/studio/tilesheeteditorview.cpp
Normal file
132
src/nostalgia/modules/core/src/studio/tilesheeteditorview.cpp
Normal 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 {};
|
||||
}
|
||||
|
||||
}
|
||||
133
src/nostalgia/modules/core/src/studio/tilesheeteditorview.hpp
Normal file
133
src/nostalgia/modules/core/src/studio/tilesheeteditorview.hpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
120
src/nostalgia/modules/core/src/studio/tilesheetpixelgrid.cpp
Normal file
120
src/nostalgia/modules/core/src/studio/tilesheetpixelgrid.cpp
Normal 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};
|
||||
}
|
||||
|
||||
}
|
||||
85
src/nostalgia/modules/core/src/studio/tilesheetpixelgrid.hpp
Normal file
85
src/nostalgia/modules/core/src/studio/tilesheetpixelgrid.hpp
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
128
src/nostalgia/modules/core/src/studio/tilesheetpixels.cpp
Normal file
128
src/nostalgia/modules/core/src/studio/tilesheetpixels.cpp
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
70
src/nostalgia/modules/core/src/studio/tilesheetpixels.hpp
Normal file
70
src/nostalgia/modules/core/src/studio/tilesheetpixels.hpp
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user