diff --git a/src/nostalgia/modules/core/include/nostalgia/core/color.hpp b/src/nostalgia/modules/core/include/nostalgia/core/color.hpp index 05974261..ebceb858 100644 --- a/src/nostalgia/modules/core/include/nostalgia/core/color.hpp +++ b/src/nostalgia/modules/core/include/nostalgia/core/color.hpp @@ -87,6 +87,11 @@ constexpr Color32 color32(uint8_t r, uint8_t g, uint8_t b) noexcept { return static_cast(r | (g << 8) | (b << 16)); } +[[nodiscard]] +constexpr Color32 color32(Color16 c) noexcept { + return color32(red32(c), green32(c), blue32(c)); +} + [[nodiscard]] constexpr Color32 color32(float r, float g, float b) noexcept { return static_cast(static_cast(r * 255) | (static_cast(g * 255) << 8) | (static_cast(b * 255) << 16)); diff --git a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp index ac39cf92..d0ab8d6b 100644 --- a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp +++ b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.cpp @@ -12,33 +12,64 @@ namespace nostalgia::core { -template -ox::Error toPngFile( - ox::CStringView const&path, TileSheet::SubSheet const&s, Palette const&pal, int8_t bpp) noexcept { - ox::Vector pixels; - s.readPixelsTo(&pixels, bpp); - const unsigned rows = s.rows == -1 ? - static_cast(pixels.size()) / PixelsPerTile : static_cast(s.rows); - const unsigned cols = s.columns == -1 ? 1 : static_cast(s.columns); - const auto width = cols * TileWidth; - const auto height = rows * TileHeight; - constexpr auto bytesPerPixel = alpha ? 4 : 3; - ox::Vector outData(pixels.size() * bytesPerPixel); - for (auto idx = 0; const auto colorIdx : pixels) { - const auto pt = idxToPt(idx, static_cast(cols)); - const auto i = static_cast(pt.y * static_cast(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; +static ox::Vector normalizePixelSizes( + ox::Vector const&inPixels, + int const bpp) noexcept { + uint_t const bytesPerTile = bpp == 8 ? PixelsPerTile : PixelsPerTile / 2; + ox::Vector outPixels; + if (bytesPerTile == 64) { // 8 BPP + outPixels.resize(inPixels.size()); + for (std::size_t i = 0; i < inPixels.size(); ++i) { + outPixels[i] = inPixels[i]; + } + } else { // 4 BPP + outPixels.resize(inPixels.size() * 2); + for (std::size_t i = 0; i < inPixels.size(); ++i) { + outPixels[i * 2 + 0] = inPixels[i] & 0xF; + outPixels[i * 2 + 1] = inPixels[i] >> 4; } - ++idx; } - constexpr auto fmt = alpha ? LCT_RGBA : LCT_RGB; + return outPixels; +} + +static ox::Vector normalizePixelArrangement( + ox::Vector const&inPixels, + int cols, + int scale) { + auto const scalePt = ox::Point{scale, scale}; + auto const width = cols * TileWidth; + auto const height = static_cast(inPixels.size()) / width; + auto const dstWidth = width * scale; + ox::Vector outPixels(static_cast((width * scale) * (height * scale))); + for (std::size_t dstIdx = 0; dstIdx < outPixels.size(); ++dstIdx) { + auto const dstPt = ox::Point{ + static_cast(dstIdx) % dstWidth, + static_cast(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 &&pixels, + Palette const&pal, + unsigned width, + unsigned height) noexcept { + for (auto &c : pixels) { + c = color32(pal.color(c)) | static_cast(0XFF << 24); + } + constexpr auto fmt = LCT_RGBA; return OxError(static_cast( - lodepng_encode_file(path.c_str(), outData.data(), width, height, fmt, 8))); + lodepng_encode_file( + path.c_str(), + reinterpret_cast(pixels.data()), + width, + height, + fmt, + 8))); } TileSheetEditorImGui::TileSheetEditorImGui(turbine::Context &ctx, ox::CRStringView path): @@ -50,11 +81,12 @@ TileSheetEditorImGui::TileSheetEditorImGui(turbine::Context &ctx, ox::CRStringVi // connect signal/slots undoStack()->changeTriggered.connect(this, &TileSheetEditorImGui::markUnsavedChanges); m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet); + m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubhseetToPng); m_model.paletteChanged.connect(this, &TileSheetEditorImGui::setPaletteSelection); } void TileSheetEditorImGui::exportFile() { - exportSubhseetToPng(); + m_exportMenu.show(); } void TileSheetEditorImGui::cut() { @@ -75,6 +107,7 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) { } if (key == turbine::Key::Escape) { m_subsheetEditor.close(); + m_exportMenu.close(); } auto pal = m_model.pal(); if (pal) { @@ -168,7 +201,7 @@ void TileSheetEditorImGui::draw(turbine::Context&) noexcept { } ImGui::SameLine(); if (ImGui::Button("Export", ImVec2(51, btnHeight))) { - exportSubhseetToPng(); + m_exportMenu.show(); } TileSheet::SubSheetIdx path; static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; @@ -185,6 +218,7 @@ void TileSheetEditorImGui::draw(turbine::Context&) noexcept { } ImGui::EndChild(); m_subsheetEditor.draw(); + m_exportMenu.draw(); } void TileSheetEditorImGui::drawSubsheetSelector(TileSheet::SubSheet *subsheet, TileSheet::SubSheetIdx *path) { @@ -244,26 +278,33 @@ ox::Error TileSheetEditorImGui::saveItem() noexcept { void TileSheetEditorImGui::showSubsheetEditor() noexcept { const auto sheet = m_model.activeSubSheet(); - if (sheet->subsheets.size()) { + if (!sheet->subsheets.empty()) { 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; - } +ox::Error TileSheetEditorImGui::exportSubhseetToPng(int scale) noexcept { + oxRequire(path, studio::saveFile({{"PNG", "png"}})); // subsheet to png - const auto &img = m_model.img(); - const auto &s = *m_model.activeSubSheet(); - const auto &pal = m_model.pal(); - err = toPngFile(path, s, *pal, img.bpp); + auto const&img = m_model.img(); + auto const&s = *m_model.activeSubSheet(); + auto const&pal = m_model.pal(); + 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(width * scale), + static_cast(height * scale)); if (err) { oxErrorf("Tilesheet export failed: {}", toStr(err)); } + return err; } void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept { @@ -420,13 +461,13 @@ void TileSheetEditorImGui::SubSheetEditor::draw() noexcept { ImGui::InputInt("Columns", &m_cols); ImGui::InputInt("Rows", &m_rows); } - if (ImGui::Button("OK")) { + if (ImGui::Button("OK", ImVec2{50, 20})) { ImGui::CloseCurrentPopup(); m_show = false; inputSubmitted.emit(m_name, m_cols, m_rows); } ImGui::SameLine(); - if (ImGui::Button("Cancel")) { + if (ImGui::Button("Cancel", ImVec2{50, 20})) { ImGui::CloseCurrentPopup(); m_show = false; } @@ -438,4 +479,34 @@ void TileSheetEditorImGui::SubSheetEditor::close() noexcept { 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; +} + } diff --git a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp index fe13d7db..5f617702 100644 --- a/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp +++ b/src/nostalgia/modules/core/src/studio/tilesheeteditor/tilesheeteditor-imgui.hpp @@ -27,10 +27,10 @@ class TileSheetEditorImGui: public studio::Editor { private: class SubSheetEditor { - ox::BString<100> m_name; - int m_cols = 0; - int m_rows = 0; - bool m_show = false; + ox::BString<100> m_name; + int m_cols = 0; + int m_rows = 0; + bool m_show = false; public: ox::Signal inputSubmitted; 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 close() noexcept; }; + class ExportMenu { + int m_scale = 0; + bool m_show = false; + public: + ox::Signal inputSubmitted; + constexpr void show() noexcept { + m_show = true; + m_scale = 5; + } + void draw() noexcept; + void close() noexcept; + }; std::size_t m_selectedPaletteIdx = 0; turbine::Context &m_ctx; ox::Vector m_paletteList; SubSheetEditor m_subsheetEditor; + ExportMenu m_exportMenu; glutils::FrameBuffer m_framebuffer; TileSheetEditorView m_view; TileSheetEditorModel &m_model; @@ -81,7 +94,7 @@ class TileSheetEditorImGui: public studio::Editor { private: void showSubsheetEditor() noexcept; - void exportSubhseetToPng() noexcept; + ox::Error exportSubhseetToPng(int scale) noexcept; void drawTileSheet(ox::Vec2 const&fbSize) noexcept; diff --git a/src/nostalgia/studio/CMakeLists.txt b/src/nostalgia/studio/CMakeLists.txt index 0ed4cc9a..14d7b541 100644 --- a/src/nostalgia/studio/CMakeLists.txt +++ b/src/nostalgia/studio/CMakeLists.txt @@ -1,7 +1,7 @@ -add_executable(nostalgia-studio WIN32 MACOSX_BUNDLE) +add_executable(NostalgiaStudio WIN32 MACOSX_BUNDLE) target_link_libraries( - nostalgia-studio + NostalgiaStudio NostalgiaProfile NostalgiaStudioModules NostalgiaKeelModules @@ -11,7 +11,7 @@ target_link_libraries( install( TARGETS - nostalgia-studio + NostalgiaStudio RUNTIME DESTINATION ${NOSTALGIA_DIST_BIN} BUNDLE DESTINATION . @@ -19,11 +19,11 @@ install( if(CMAKE_BUILD_TYPE STREQUAL "Release" AND NOT WIN32) # enable LTO - set_property(TARGET nostalgia-studio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) + set_property(TARGET NostalgiaStudio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) endif() 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() install( diff --git a/src/nostalgia/studio/Info.plist b/src/nostalgia/studio/Info.plist index d2ca8b5c..a8a86835 100644 --- a/src/nostalgia/studio/Info.plist +++ b/src/nostalgia/studio/Info.plist @@ -3,17 +3,23 @@ CFBundleExecutable - Nostalgia Studio + NostalgiaStudio + CFBundleGetInfoString Nostalgia Studio + CFBundleIconFile - + icons/ns_logo128.png + CFBundleIdentifier net.drinkingtea.nostalgia.studio + CFBundlePackageType APPL + CFBundleVersion 0.0.0 + LSMinimumSystemVersion 12.0.0