[nostalgia/core/studio] Add scale option to TileSheet export
This commit is contained in:
parent
9b11fa4e91
commit
56f59b29fe
@ -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));
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user