[studio,nostalgia/gfx] Add system for adding sub-commands in Modules, add export-tilesheet command
All checks were successful
Build / build (push) Successful in 1m16s
All checks were successful
Build / build (push) Successful in 1m16s
This commit is contained in:
@ -428,6 +428,14 @@ TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubShe
|
||||
|
||||
ox::Result<TileSheet::SubSheetIdx> getSubSheetIdx(TileSheet const &ts, SubSheetId pId) noexcept;
|
||||
|
||||
ox::Result<TileSheet::SubSheet*> getSubSheet(
|
||||
ox::SpanView<ox::StringView> const &idx,
|
||||
TileSheet &ts) noexcept;
|
||||
|
||||
ox::Result<TileSheet::SubSheet const*> getSubSheet(
|
||||
ox::SpanView<ox::StringView> const &idx,
|
||||
TileSheet const &ts) noexcept;
|
||||
|
||||
[[nodiscard]]
|
||||
TileSheet::SubSheet &getSubSheet(
|
||||
ox::SpanView<uint32_t> const&idx,
|
||||
|
@ -26,3 +26,4 @@ install(
|
||||
|
||||
add_subdirectory(paletteeditor)
|
||||
add_subdirectory(tilesheeteditor)
|
||||
add_subdirectory(subcommands)
|
||||
|
@ -8,10 +8,25 @@
|
||||
|
||||
#include "paletteeditor/paletteeditor-imgui.hpp"
|
||||
#include "tilesheeteditor/tilesheeteditor-imgui.hpp"
|
||||
#include "subcommands/export-tilesheet/export-tilesheet.hpp"
|
||||
|
||||
namespace nostalgia::gfx {
|
||||
|
||||
static class: public studio::Module {
|
||||
static struct: studio::Module {
|
||||
|
||||
ox::String id() const noexcept final {
|
||||
return ox::String{"net.drinkingtea.nostalgia.gfx"};
|
||||
}
|
||||
|
||||
ox::Vector<studio::Command> commands() const final {
|
||||
return {
|
||||
{
|
||||
"export-tilesheet",
|
||||
cmdExportTilesheet,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ox::Vector<studio::EditorMaker> editors(studio::Context &ctx) const noexcept final {
|
||||
return {
|
||||
studio::editorMaker<TileSheetEditorImGui>(ctx, {FileExt_ng, FileExt_nts}),
|
||||
@ -28,6 +43,7 @@ static class: public studio::Module {
|
||||
}, ox::ClawFormat::Organic));
|
||||
return out;
|
||||
}
|
||||
|
||||
} const mod;
|
||||
|
||||
studio::Module const *studioModule() noexcept {
|
||||
|
@ -0,0 +1,10 @@
|
||||
|
||||
target_sources(
|
||||
NostalgiaGfx-Studio PRIVATE
|
||||
export-tilesheet/export-tilesheet.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
NostalgiaGfx-Studio PUBLIC
|
||||
OxClArgs
|
||||
)
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
|
||||
*/
|
||||
|
||||
#include <lodepng.h>
|
||||
|
||||
#include <ox/clargs/clargs.hpp>
|
||||
#include <ox/std/trace.hpp>
|
||||
|
||||
#include <studio/project.hpp>
|
||||
|
||||
#include <nostalgia/gfx/palette.hpp>
|
||||
#include <nostalgia/gfx/tilesheet.hpp>
|
||||
|
||||
#include "export-tilesheet.hpp"
|
||||
|
||||
#include "studio/context.hpp"
|
||||
|
||||
|
||||
namespace nostalgia::gfx {
|
||||
|
||||
static ox::Vector<uint32_t> normalizePixelSizes(
|
||||
ox::Vector<uint8_t> const &inPixels) noexcept {
|
||||
ox::Vector<uint32_t> outPixels;
|
||||
outPixels.reserve(inPixels.size());
|
||||
outPixels.resize(inPixels.size());
|
||||
for (size_t i{}; i < inPixels.size(); ++i) {
|
||||
outPixels[i] = inPixels[i];
|
||||
}
|
||||
return outPixels;
|
||||
}
|
||||
|
||||
static ox::Vector<uint32_t> normalizePixelArrangement(
|
||||
ox::Vector<uint32_t> const &inPixels,
|
||||
int const cols,
|
||||
int const 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 (size_t dstIdx{}; 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::CStringViewCR path,
|
||||
ox::Vector<uint32_t> &&pixels,
|
||||
Palette const &pal,
|
||||
size_t const page,
|
||||
unsigned const width,
|
||||
unsigned const height) noexcept {
|
||||
for (auto &c : pixels) {
|
||||
c = color32(color(pal, page, c)) | static_cast<Color32>(0XFF << 24);
|
||||
}
|
||||
constexpr auto fmt = LCT_RGBA;
|
||||
return ox::Error(static_cast<ox::ErrorCode>(
|
||||
lodepng_encode_file(
|
||||
path.c_str(),
|
||||
reinterpret_cast<uint8_t const*>(pixels.data()),
|
||||
width,
|
||||
height,
|
||||
fmt,
|
||||
8)));
|
||||
}
|
||||
|
||||
ox::Error exportSubsheetToPng(
|
||||
TileSheet const &src,
|
||||
Palette const &pal,
|
||||
ox::StringViewCR subsheetPath,
|
||||
size_t const palPage,
|
||||
ox::CStringViewCR dstPath,
|
||||
int const scale) noexcept {
|
||||
// subsheet to png
|
||||
auto const [s, ssErr] = getSubSheet(
|
||||
ox::split(subsheetPath, '.'), src);
|
||||
if (ssErr) {
|
||||
return ox::Error{6, "failed to find SubSheet"};
|
||||
}
|
||||
auto const width = s->columns * TileWidth;
|
||||
auto const height = s->rows * TileHeight;
|
||||
auto const err = toPngFile(
|
||||
dstPath,
|
||||
normalizePixelArrangement(
|
||||
normalizePixelSizes(s->pixels),
|
||||
s->columns,
|
||||
scale),
|
||||
pal,
|
||||
palPage,
|
||||
static_cast<unsigned>(width * scale),
|
||||
static_cast<unsigned>(height * scale));
|
||||
if (err) {
|
||||
oxErrorf("TileSheet export failed: {}", toStr(err));
|
||||
return ox::Error{7, "TileSheet export failed"};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ox::Error cmdExportTilesheet(studio::Project &project, ox::SpanView<ox::CString> const args) noexcept {
|
||||
// parse args
|
||||
ox::ClArgs const clargs{args};
|
||||
bool showUsage = true;
|
||||
OX_DEFER [&showUsage] {
|
||||
if (showUsage) {
|
||||
oxErr("usage: export-tilesheet "
|
||||
"-src-path <path> "
|
||||
"-dst-path <path> "
|
||||
"[-pal-path <path>] "
|
||||
"[-subsheet-path <path>] "
|
||||
"[-scale <int>]\n");
|
||||
}
|
||||
};
|
||||
OX_REQUIRE(srcPath, clargs.getString("src-path").transformError(1, "no src path specified"));
|
||||
OX_REQUIRE(dstPath, clargs.getString("dst-path").transformError(2, "no dst path specified"));
|
||||
auto const palPath = clargs.getString("pal-path", "");
|
||||
auto const subsheetPath = clargs.getString("subsheet-path").or_value({});
|
||||
auto const palPage = static_cast<size_t>(clargs.getInt("pal-page", 0));
|
||||
auto const scale = clargs.getInt("scale", 1);
|
||||
showUsage = false;
|
||||
// load objects
|
||||
auto &kctx = project.kctx();
|
||||
OX_REQUIRE(ts, keel::readObj<TileSheet>(kctx, srcPath).transformError(4, "could not load TileSheet"));
|
||||
OX_REQUIRE(pal, keel::readObj<Palette>(kctx, palPath.len() ? palPath : ts->defaultPalette)
|
||||
.transformError(5, "could not load Palette"));
|
||||
// export to the destination file
|
||||
return exportSubsheetToPng(
|
||||
*ts,
|
||||
*pal,
|
||||
subsheetPath,
|
||||
palPage,
|
||||
dstPath,
|
||||
scale);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ox/std/error.hpp>
|
||||
|
||||
namespace nostalgia::gfx {
|
||||
|
||||
ox::Error exportSubsheetToPng(
|
||||
TileSheet const &src,
|
||||
Palette const &pal,
|
||||
ox::StringViewCR subsheetPath,
|
||||
size_t palPage,
|
||||
ox::CStringViewCR dstPath,
|
||||
int scale) noexcept;
|
||||
|
||||
ox::Error cmdExportTilesheet(studio::Project& project, ox::SpanView<ox::CString> args) noexcept;
|
||||
|
||||
}
|
@ -211,6 +211,41 @@ ox::Result<TileSheet::SubSheetIdx> getSubSheetIdx(TileSheet const &ts, SubSheetI
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
template<typename SubSheet>
|
||||
static ox::Result<SubSheet*> getSubSheet(
|
||||
ox::SpanView<ox::StringView> const &idx,
|
||||
std::size_t const idxIt,
|
||||
SubSheet &pSubSheet) noexcept {
|
||||
if (idxIt == idx.size()) {
|
||||
return &pSubSheet;
|
||||
}
|
||||
auto const ¤tIdx = idx[idxIt];
|
||||
auto const next = ox::find_if(
|
||||
pSubSheet.subsheets.begin(),
|
||||
pSubSheet.subsheets.end(),
|
||||
[¤tIdx](TileSheet::SubSheet const &ss) {
|
||||
return ss.name == currentIdx;
|
||||
});
|
||||
if (next == pSubSheet.subsheets.end()) {
|
||||
return ox::Error{1, "SubSheet not found"};
|
||||
}
|
||||
return getSubSheet(idx, idxIt + 1, *next);
|
||||
}
|
||||
|
||||
ox::Result<TileSheet::SubSheet const*> getSubSheet(
|
||||
ox::SpanView<ox::StringView> const &idx,
|
||||
TileSheet const &ts) noexcept {
|
||||
return getSubSheet<TileSheet::SubSheet const>(idx, 1, ts.subsheet);
|
||||
}
|
||||
|
||||
ox::Result<TileSheet::SubSheet*> getSubSheet(
|
||||
ox::SpanView<ox::StringView> const &idx,
|
||||
TileSheet &ts) noexcept {
|
||||
return getSubSheet<TileSheet::SubSheet>(idx, 1, ts.subsheet);
|
||||
}
|
||||
|
||||
|
||||
#if defined(__GNUC__) && __GNUC__ >= 13
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdangling-reference"
|
||||
|
@ -10,6 +10,10 @@ namespace nostalgia::sound {
|
||||
|
||||
static struct: studio::Module {
|
||||
|
||||
ox::String id() const noexcept final {
|
||||
return ox::String{"net.drinkingtea.nostalgia.sound"};
|
||||
}
|
||||
|
||||
ox::Vector<studio::EditorMaker> editors(studio::Context&) const noexcept final {
|
||||
return {
|
||||
};
|
||||
@ -22,7 +26,7 @@ static struct: studio::Module {
|
||||
|
||||
} const mod;
|
||||
|
||||
const studio::Module *studioModule() noexcept {
|
||||
studio::Module const *studioModule() noexcept {
|
||||
return &mod;
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,9 @@
|
||||
|
||||
namespace studio {
|
||||
|
||||
[[nodiscard]]
|
||||
ox::Vector<Module const*> const &modules() noexcept;
|
||||
|
||||
void registerModule(Module const*) noexcept;
|
||||
|
||||
}
|
||||
|
@ -2,15 +2,18 @@
|
||||
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
|
||||
*/
|
||||
|
||||
#include <complex>
|
||||
#include <ctime>
|
||||
|
||||
#include <ox/logconn/logconn.hpp>
|
||||
#include <ox/std/trace.hpp>
|
||||
#include <ox/std/uuid.hpp>
|
||||
#include <keel/media.hpp>
|
||||
#include <keel/keel.hpp>
|
||||
#include <turbine/turbine.hpp>
|
||||
|
||||
#include <studio/context.hpp>
|
||||
#include <studioapp/studioapp.hpp>
|
||||
|
||||
#include "studioui.hpp"
|
||||
|
||||
namespace studio {
|
||||
@ -40,7 +43,7 @@ static void mouseButtonEventHandler(turbine::Context &ctx, int const btn, bool c
|
||||
[[nodiscard]]
|
||||
ox::Vector<ox::SpanView<uint8_t>> WindowIcons() noexcept;
|
||||
|
||||
static ox::Error runApp(
|
||||
static ox::Error runStudio(
|
||||
ox::StringViewCR appName,
|
||||
ox::StringViewCR projectDataDir,
|
||||
ox::UPtr<ox::FileSystem> &&fs) noexcept {
|
||||
@ -61,17 +64,46 @@ static ox::Error runApp(
|
||||
static ox::Error run(
|
||||
ox::StringViewCR appName,
|
||||
ox::StringViewCR projectDataDir,
|
||||
ox::SpanView<ox::CString>) {
|
||||
ox::SpanView<ox::CString> const &args) {
|
||||
// seed UUID generator
|
||||
auto const time = std::time(nullptr);
|
||||
ox::UUID::seedGenerator({
|
||||
static_cast<uint64_t>(time),
|
||||
static_cast<uint64_t>(time << 1)
|
||||
});
|
||||
// run app
|
||||
auto const err = runApp(appName, projectDataDir, ox::UPtr<ox::FileSystem>{});
|
||||
oxAssert(err, "Something went wrong...");
|
||||
return err;
|
||||
if (args.size() > 1 && ox::StringView{args[1]} == "cmd") {
|
||||
if (args.size() < 5) {
|
||||
return ox::Error{1, "insufficient arguments for sub-command"};
|
||||
}
|
||||
auto constexpr numCmdArgs = 5;
|
||||
ox::StringView const projectDir = args[2];
|
||||
ox::StringView const moduleId = args[3];
|
||||
ox::StringView const subCmd = args[4];
|
||||
for (auto const m : modules()) {
|
||||
if (m->id() == moduleId) {
|
||||
for (auto const &c : m->commands()) {
|
||||
if (c.name == subCmd) {
|
||||
auto kctx = keel::init(
|
||||
ox::make_unique<ox::PassThroughFS>(projectDir),
|
||||
c.name);
|
||||
if (kctx.error) {
|
||||
return ox::Error{2, "failed to load project directory"};
|
||||
}
|
||||
Project project{*kctx.value, projectDir, projectDataDir};
|
||||
return c.func(project, args + numCmdArgs);
|
||||
}
|
||||
}
|
||||
return ox::Error{1, "command not found"};
|
||||
}
|
||||
}
|
||||
return ox::Error{1, "module not found"};
|
||||
} else {
|
||||
// run app
|
||||
auto const err = runStudio(appName, projectDataDir, ox::UPtr<ox::FileSystem>{});
|
||||
oxAssert(err, "Something went wrong...");
|
||||
return err;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -37,11 +37,15 @@ static bool shutdownHandler(turbine::Context &ctx) {
|
||||
namespace ig {
|
||||
extern bool s_mainWinHasFocus;
|
||||
}
|
||||
static ox::Vector<Module const*> modules;
|
||||
static ox::Vector<Module const*> g_modules;
|
||||
|
||||
ox::Vector<Module const*> const &modules() noexcept {
|
||||
return g_modules;
|
||||
}
|
||||
|
||||
void registerModule(Module const*mod) noexcept {
|
||||
if (mod) {
|
||||
modules.emplace_back(mod);
|
||||
g_modules.emplace_back(mod);
|
||||
}
|
||||
}
|
||||
|
||||
@ -443,7 +447,7 @@ void StudioUI::loadModule(Module const &mod) noexcept {
|
||||
}
|
||||
|
||||
void StudioUI::loadModules() noexcept {
|
||||
for (auto const mod : modules) {
|
||||
for (auto const mod : g_modules) {
|
||||
loadModule(*mod);
|
||||
}
|
||||
}
|
||||
|
@ -23,15 +23,34 @@ struct EditorMaker {
|
||||
Func make;
|
||||
};
|
||||
|
||||
struct Command {
|
||||
ox::String name;
|
||||
std::function<ox::Error(Project&, ox::SpanView<ox::CString>)> func;
|
||||
Command(
|
||||
ox::StringParam name,
|
||||
std::function<ox::Error(Project&, ox::SpanView<ox::CString>)> func) noexcept:
|
||||
name(std::move(name)),
|
||||
func(std::move(func)) {}
|
||||
};
|
||||
|
||||
class Module {
|
||||
public:
|
||||
virtual ~Module() noexcept = default;
|
||||
|
||||
virtual ox::Vector<EditorMaker> editors(studio::Context &ctx) const;
|
||||
[[nodiscard]]
|
||||
virtual ox::String id() const noexcept = 0;
|
||||
|
||||
virtual ox::Vector<ox::UPtr<ItemMaker>> itemMakers(studio::Context&) const;
|
||||
[[nodiscard]]
|
||||
virtual ox::Vector<Command> commands() const;
|
||||
|
||||
virtual ox::Vector<ox::UPtr<ItemTemplate>> itemTemplates(studio::Context&) const;
|
||||
[[nodiscard]]
|
||||
virtual ox::Vector<EditorMaker> editors(Context &ctx) const;
|
||||
|
||||
[[nodiscard]]
|
||||
virtual ox::Vector<ox::UPtr<ItemMaker>> itemMakers(Context&) const;
|
||||
|
||||
[[nodiscard]]
|
||||
virtual ox::Vector<ox::UPtr<ItemTemplate>> itemTemplates(Context&) const;
|
||||
|
||||
};
|
||||
|
||||
|
@ -59,7 +59,7 @@ class Project: public ox::SignalHandler {
|
||||
ox::HashMap<ox::String, ox::Vector<ox::String>> m_fileExtFileMap;
|
||||
|
||||
public:
|
||||
explicit Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir);
|
||||
explicit Project(keel::Context &ctx, ox::StringParam path, ox::StringViewCR projectDataDir);
|
||||
|
||||
ox::Error create() noexcept;
|
||||
|
||||
@ -101,6 +101,16 @@ class Project: public ox::SignalHandler {
|
||||
|
||||
ox::Error deleteItem(ox::StringViewCR path) noexcept;
|
||||
|
||||
[[nodiscard]]
|
||||
keel::Context &kctx() noexcept {
|
||||
return m_kctx;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
keel::Context const &kctx() const noexcept {
|
||||
return m_kctx;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool exists(ox::StringViewCR path) const noexcept;
|
||||
|
||||
|
@ -6,6 +6,10 @@
|
||||
|
||||
namespace studio {
|
||||
|
||||
ox::Vector<Command> Module::commands() const {
|
||||
return {};
|
||||
}
|
||||
|
||||
ox::Vector<EditorMaker> Module::editors(Context&) const {
|
||||
return {};
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ static constexpr bool isParentOf(ox::StringViewCR parent, ox::StringViewCR child
|
||||
return beginsWith(sfmt("{}/", child), parent);
|
||||
}
|
||||
|
||||
Project::Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir):
|
||||
Project::Project(keel::Context &ctx, ox::StringParam path, ox::StringViewCR projectDataDir):
|
||||
m_kctx(ctx),
|
||||
m_path(std::move(path)),
|
||||
m_projectDataDir(projectDataDir),
|
||||
|
Reference in New Issue
Block a user