[nostalgia/core/studio] Add scale option to TileSheet export

This commit is contained in:
Gary Talent 2023-12-16 02:58:54 -06:00
parent 9b11fa4e91
commit 56f59b29fe
5 changed files with 145 additions and 50 deletions

View File

@ -87,6 +87,11 @@ constexpr Color32 color32(uint8_t r, uint8_t g, uint8_t b) noexcept {
return static_cast<Color32>(r | (g << 8) | (b << 16)); return static_cast<Color32>(r | (g << 8) | (b << 16));
} }
[[nodiscard]]
constexpr Color32 color32(Color16 c) noexcept {
return color32(red32(c), green32(c), blue32(c));
}
[[nodiscard]] [[nodiscard]]
constexpr Color32 color32(float r, float g, float b) noexcept { constexpr Color32 color32(float r, float g, float b) noexcept {
return static_cast<Color32>(static_cast<uint8_t>(r * 255) | (static_cast<uint8_t>(g * 255) << 8) | (static_cast<uint8_t>(b * 255) << 16)); return static_cast<Color32>(static_cast<uint8_t>(r * 255) | (static_cast<uint8_t>(g * 255) << 8) | (static_cast<uint8_t>(b * 255) << 16));

View File

@ -12,33 +12,64 @@
namespace nostalgia::core { namespace nostalgia::core {
template<bool alpha = false> static ox::Vector<uint32_t> normalizePixelSizes(
ox::Error toPngFile( ox::Vector<uint8_t> const&inPixels,
ox::CStringView const&path, TileSheet::SubSheet const&s, Palette const&pal, int8_t bpp) noexcept { int const bpp) noexcept {
ox::Vector<uint8_t> pixels; uint_t const bytesPerTile = bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
s.readPixelsTo(&pixels, bpp); ox::Vector<uint32_t> outPixels;
const unsigned rows = s.rows == -1 ? if (bytesPerTile == 64) { // 8 BPP
static_cast<unsigned>(pixels.size()) / PixelsPerTile : static_cast<unsigned>(s.rows); outPixels.resize(inPixels.size());
const unsigned cols = s.columns == -1 ? 1 : static_cast<unsigned>(s.columns); for (std::size_t i = 0; i < inPixels.size(); ++i) {
const auto width = cols * TileWidth; outPixels[i] = inPixels[i];
const auto height = rows * TileHeight; }
constexpr auto bytesPerPixel = alpha ? 4 : 3; } else { // 4 BPP
ox::Vector<unsigned char> outData(pixels.size() * bytesPerPixel); outPixels.resize(inPixels.size() * 2);
for (auto idx = 0; const auto colorIdx : pixels) { for (std::size_t i = 0; i < inPixels.size(); ++i) {
const auto pt = idxToPt(idx, static_cast<int>(cols)); outPixels[i * 2 + 0] = inPixels[i] & 0xF;
const auto i = static_cast<unsigned>(pt.y * static_cast<int>(width) + pt.x) * bytesPerPixel; outPixels[i * 2 + 1] = inPixels[i] >> 4;
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 outPixels;
}
static ox::Vector<uint32_t> normalizePixelArrangement(
ox::Vector<uint32_t> const&inPixels,
int cols,
int scale) {
auto const scalePt = ox::Point{scale, scale};
auto const width = cols * TileWidth;
auto const height = static_cast<int>(inPixels.size()) / width;
auto const dstWidth = width * scale;
ox::Vector<uint32_t> outPixels(static_cast<size_t>((width * scale) * (height * scale)));
for (std::size_t dstIdx = 0; dstIdx < outPixels.size(); ++dstIdx) {
auto const dstPt = ox::Point{
static_cast<int>(dstIdx) % dstWidth,
static_cast<int>(dstIdx) / dstWidth};
auto const srcPt = dstPt / scalePt;
auto const srcIdx = ptToIdx(srcPt, cols);
outPixels[dstIdx] = inPixels[srcIdx];
}
return outPixels;
}
static ox::Error toPngFile(
ox::CStringView const&path,
ox::Vector<uint32_t> &&pixels,
Palette const&pal,
unsigned width,
unsigned height) noexcept {
for (auto &c : pixels) {
c = color32(pal.color(c)) | static_cast<Color32>(0XFF << 24);
}
constexpr auto fmt = LCT_RGBA;
return OxError(static_cast<ox::ErrorCode>( return OxError(static_cast<ox::ErrorCode>(
lodepng_encode_file(path.c_str(), outData.data(), width, height, fmt, 8))); lodepng_encode_file(
path.c_str(),
reinterpret_cast<uint8_t const*>(pixels.data()),
width,
height,
fmt,
8)));
} }
TileSheetEditorImGui::TileSheetEditorImGui(turbine::Context &ctx, ox::CRStringView path): TileSheetEditorImGui::TileSheetEditorImGui(turbine::Context &ctx, ox::CRStringView path):
@ -50,11 +81,12 @@ TileSheetEditorImGui::TileSheetEditorImGui(turbine::Context &ctx, ox::CRStringVi
// connect signal/slots // connect signal/slots
undoStack()->changeTriggered.connect(this, &TileSheetEditorImGui::markUnsavedChanges); undoStack()->changeTriggered.connect(this, &TileSheetEditorImGui::markUnsavedChanges);
m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet); m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet);
m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubhseetToPng);
m_model.paletteChanged.connect(this, &TileSheetEditorImGui::setPaletteSelection); m_model.paletteChanged.connect(this, &TileSheetEditorImGui::setPaletteSelection);
} }
void TileSheetEditorImGui::exportFile() { void TileSheetEditorImGui::exportFile() {
exportSubhseetToPng(); m_exportMenu.show();
} }
void TileSheetEditorImGui::cut() { void TileSheetEditorImGui::cut() {
@ -75,6 +107,7 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) {
} }
if (key == turbine::Key::Escape) { if (key == turbine::Key::Escape) {
m_subsheetEditor.close(); m_subsheetEditor.close();
m_exportMenu.close();
} }
auto pal = m_model.pal(); auto pal = m_model.pal();
if (pal) { if (pal) {
@ -168,7 +201,7 @@ void TileSheetEditorImGui::draw(turbine::Context&) noexcept {
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Export", ImVec2(51, btnHeight))) { if (ImGui::Button("Export", ImVec2(51, btnHeight))) {
exportSubhseetToPng(); m_exportMenu.show();
} }
TileSheet::SubSheetIdx path; TileSheet::SubSheetIdx path;
static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
@ -185,6 +218,7 @@ void TileSheetEditorImGui::draw(turbine::Context&) noexcept {
} }
ImGui::EndChild(); ImGui::EndChild();
m_subsheetEditor.draw(); m_subsheetEditor.draw();
m_exportMenu.draw();
} }
void TileSheetEditorImGui::drawSubsheetSelector(TileSheet::SubSheet *subsheet, TileSheet::SubSheetIdx *path) { void TileSheetEditorImGui::drawSubsheetSelector(TileSheet::SubSheet *subsheet, TileSheet::SubSheetIdx *path) {
@ -244,26 +278,33 @@ ox::Error TileSheetEditorImGui::saveItem() noexcept {
void TileSheetEditorImGui::showSubsheetEditor() noexcept { void TileSheetEditorImGui::showSubsheetEditor() noexcept {
const auto sheet = m_model.activeSubSheet(); const auto sheet = m_model.activeSubSheet();
if (sheet->subsheets.size()) { if (!sheet->subsheets.empty()) {
m_subsheetEditor.show(sheet->name, -1, -1); m_subsheetEditor.show(sheet->name, -1, -1);
} else { } else {
m_subsheetEditor.show(sheet->name, sheet->columns, sheet->rows); m_subsheetEditor.show(sheet->name, sheet->columns, sheet->rows);
} }
} }
void TileSheetEditorImGui::exportSubhseetToPng() noexcept { ox::Error TileSheetEditorImGui::exportSubhseetToPng(int scale) noexcept {
auto [path, err] = studio::saveFile({{"PNG", "png"}}); oxRequire(path, studio::saveFile({{"PNG", "png"}}));
if (err) {
return;
}
// subsheet to png // subsheet to png
const auto &img = m_model.img(); auto const&img = m_model.img();
const auto &s = *m_model.activeSubSheet(); auto const&s = *m_model.activeSubSheet();
const auto &pal = m_model.pal(); auto const&pal = m_model.pal();
err = toPngFile(path, s, *pal, img.bpp); auto const width = s.columns * TileWidth;
auto const height = s.rows * TileHeight;
auto pixels = normalizePixelSizes(s.pixels, img.bpp);
pixels = normalizePixelArrangement(pixels, s.columns, scale);
auto const err = toPngFile(
path,
std::move(pixels),
*pal,
static_cast<unsigned>(width * scale),
static_cast<unsigned>(height * scale));
if (err) { if (err) {
oxErrorf("Tilesheet export failed: {}", toStr(err)); oxErrorf("Tilesheet export failed: {}", toStr(err));
} }
return err;
} }
void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept { void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept {
@ -420,13 +461,13 @@ void TileSheetEditorImGui::SubSheetEditor::draw() noexcept {
ImGui::InputInt("Columns", &m_cols); ImGui::InputInt("Columns", &m_cols);
ImGui::InputInt("Rows", &m_rows); ImGui::InputInt("Rows", &m_rows);
} }
if (ImGui::Button("OK")) { if (ImGui::Button("OK", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
m_show = false; m_show = false;
inputSubmitted.emit(m_name, m_cols, m_rows); inputSubmitted.emit(m_name, m_cols, m_rows);
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Cancel")) { if (ImGui::Button("Cancel", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup(); ImGui::CloseCurrentPopup();
m_show = false; m_show = false;
} }
@ -438,4 +479,34 @@ void TileSheetEditorImGui::SubSheetEditor::close() noexcept {
m_show = false; m_show = false;
} }
void TileSheetEditorImGui::ExportMenu::draw() noexcept {
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
constexpr auto popupName = "Export Tile Sheet";
if (!m_show) {
return;
}
ImGui::OpenPopup(popupName);
constexpr auto popupHeight = 80.f;
ImGui::SetNextWindowSize(ImVec2(235, popupHeight));
if (ImGui::BeginPopupModal(popupName, &m_show, modalFlags)) {
ImGui::InputInt("Scale", &m_scale);
m_scale = ox::clamp(m_scale, 1, 20);
if (ImGui::Button("OK", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup();
m_show = false;
inputSubmitted.emit(m_scale);
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup();
m_show = false;
}
ImGui::EndPopup();
}
}
void TileSheetEditorImGui::ExportMenu::close() noexcept {
m_show = false;
}
} }

View File

@ -27,10 +27,10 @@ class TileSheetEditorImGui: public studio::Editor {
private: private:
class SubSheetEditor { class SubSheetEditor {
ox::BString<100> m_name; ox::BString<100> m_name;
int m_cols = 0; int m_cols = 0;
int m_rows = 0; int m_rows = 0;
bool m_show = false; bool m_show = false;
public: public:
ox::Signal<ox::Error(ox::StringView const&name, int cols, int rows)> inputSubmitted; ox::Signal<ox::Error(ox::StringView const&name, int cols, int rows)> inputSubmitted;
constexpr void show(ox::StringView const&name, int cols, int rows) noexcept { constexpr void show(ox::StringView const&name, int cols, int rows) noexcept {
@ -42,10 +42,23 @@ class TileSheetEditorImGui: public studio::Editor {
void draw() noexcept; void draw() noexcept;
void close() noexcept; void close() noexcept;
}; };
class ExportMenu {
int m_scale = 0;
bool m_show = false;
public:
ox::Signal<ox::Error(int scale)> inputSubmitted;
constexpr void show() noexcept {
m_show = true;
m_scale = 5;
}
void draw() noexcept;
void close() noexcept;
};
std::size_t m_selectedPaletteIdx = 0; std::size_t m_selectedPaletteIdx = 0;
turbine::Context &m_ctx; turbine::Context &m_ctx;
ox::Vector<ox::String> m_paletteList; ox::Vector<ox::String> m_paletteList;
SubSheetEditor m_subsheetEditor; SubSheetEditor m_subsheetEditor;
ExportMenu m_exportMenu;
glutils::FrameBuffer m_framebuffer; glutils::FrameBuffer m_framebuffer;
TileSheetEditorView m_view; TileSheetEditorView m_view;
TileSheetEditorModel &m_model; TileSheetEditorModel &m_model;
@ -81,7 +94,7 @@ class TileSheetEditorImGui: public studio::Editor {
private: private:
void showSubsheetEditor() noexcept; void showSubsheetEditor() noexcept;
void exportSubhseetToPng() noexcept; ox::Error exportSubhseetToPng(int scale) noexcept;
void drawTileSheet(ox::Vec2 const&fbSize) noexcept; void drawTileSheet(ox::Vec2 const&fbSize) noexcept;

View File

@ -1,7 +1,7 @@
add_executable(nostalgia-studio WIN32 MACOSX_BUNDLE) add_executable(NostalgiaStudio WIN32 MACOSX_BUNDLE)
target_link_libraries( target_link_libraries(
nostalgia-studio NostalgiaStudio
NostalgiaProfile NostalgiaProfile
NostalgiaStudioModules NostalgiaStudioModules
NostalgiaKeelModules NostalgiaKeelModules
@ -11,7 +11,7 @@ target_link_libraries(
install( install(
TARGETS TARGETS
nostalgia-studio NostalgiaStudio
RUNTIME DESTINATION RUNTIME DESTINATION
${NOSTALGIA_DIST_BIN} ${NOSTALGIA_DIST_BIN}
BUNDLE DESTINATION . BUNDLE DESTINATION .
@ -19,11 +19,11 @@ install(
if(CMAKE_BUILD_TYPE STREQUAL "Release" AND NOT WIN32) if(CMAKE_BUILD_TYPE STREQUAL "Release" AND NOT WIN32)
# enable LTO # enable LTO
set_property(TARGET nostalgia-studio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) set_property(TARGET NostalgiaStudio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
endif() endif()
if(APPLE) if(APPLE)
set_target_properties(nostalgia-studio PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist) set_target_properties(NostalgiaStudio PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
endif() endif()
install( install(

View File

@ -3,17 +3,23 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Nostalgia Studio</string> <string>NostalgiaStudio</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Nostalgia Studio</string> <string>Nostalgia Studio</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string></string> <string>icons/ns_logo128.png</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>net.drinkingtea.nostalgia.studio</string> <string>net.drinkingtea.nostalgia.studio</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.0.0</string> <string>0.0.0</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0.0</string> <string>12.0.0</string>