|
|
|
@ -12,33 +12,64 @@
|
|
|
|
|
|
|
|
|
|
namespace nostalgia::core {
|
|
|
|
|
|
|
|
|
|
template<bool alpha = false>
|
|
|
|
|
ox::Error toPngFile(
|
|
|
|
|
ox::CStringView const&path, TileSheet::SubSheet const&s, Palette const&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;
|
|
|
|
|
static ox::Vector<uint32_t> normalizePixelSizes(
|
|
|
|
|
ox::Vector<uint8_t> const&inPixels,
|
|
|
|
|
int const bpp) noexcept {
|
|
|
|
|
uint_t const bytesPerTile = bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
|
|
|
|
|
ox::Vector<uint32_t> outPixels;
|
|
|
|
|
if (bytesPerTile == 64) { // 8 BPP
|
|
|
|
|
outPixels.resize(inPixels.size());
|
|
|
|
|
for (std::size_t i = 0; i < inPixels.size(); ++i) {
|
|
|
|
|
outPixels[i] = inPixels[i];
|
|
|
|
|
}
|
|
|
|
|
++idx;
|
|
|
|
|
} 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;
|
|
|
|
|
}
|
|
|
|
|
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>(
|
|
|
|
|
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):
|
|
|
|
@ -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<unsigned>(width * scale),
|
|
|
|
|
static_cast<unsigned>(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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|