From f5f4c3d782d3a0901035128777f32715a53d60ba Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 09:34:19 -0700 Subject: [PATCH] refactor(editor): extract simple texture helpers into cli_texture_helpers.cpp Moves the two basic texture-helper handlers (--gen-texture, --add-texture-to-zone) out of main.cpp into a new cli_texture_helpers.{hpp,cpp} module. The first synthesizes solid hex / checker / grid PNG placeholders; the second imports an existing PNG into a zone with optional rename and idempotent re-run support (skip if bytes already match). Both complement the procedural pattern generators in cli_gen_texture (which handles the 43 named patterns). main.cpp shrinks by 196 lines (2,022 to 1,826) and finally drops below 2K lines. --- CMakeLists.txt | 1 + tools/editor/cli_texture_helpers.cpp | 241 +++++++++++++++++++++++++++ tools/editor/cli_texture_helpers.hpp | 18 ++ tools/editor/main.cpp | 204 +---------------------- 4 files changed, 264 insertions(+), 200 deletions(-) create mode 100644 tools/editor/cli_texture_helpers.cpp create mode 100644 tools/editor/cli_texture_helpers.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bf07278..180ba03a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1356,6 +1356,7 @@ add_executable(wowee_editor tools/editor/cli_for_each.cpp tools/editor/cli_check.cpp tools/editor/cli_introspect.cpp + tools/editor/cli_texture_helpers.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_texture_helpers.cpp b/tools/editor/cli_texture_helpers.cpp new file mode 100644 index 00000000..cbafdd47 --- /dev/null +++ b/tools/editor/cli_texture_helpers.cpp @@ -0,0 +1,241 @@ +#include "cli_texture_helpers.hpp" +#include "stb_image_write.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleGenTexture(int& i, int argc, char** argv) { + // Synthesize a placeholder PNG texture. Lets users add a + // working texture to their project without an external + // image editor — useful for prototyping new meshes, + // filling out a zone before art is final, or generating + // test fixtures. + // + // : + // "RRGGBB" or "RGB" hex (case-insensitive) → solid color + // "checker" → 32x32 black/white checkerboard + // "grid" → black background with white 1-px grid every 16 + std::string outPath = argv[++i]; + std::string spec = argv[++i]; + int W = 256, H = 256; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { W = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { H = std::stoi(argv[++i]); } catch (...) {} + } + if (W < 1 || H < 1 || W > 8192 || H > 8192) { + std::fprintf(stderr, + "gen-texture: invalid size %dx%d (must be 1..8192)\n", W, H); + return 1; + } + std::vector pixels(static_cast(W) * H * 3, 0); + std::string lower = spec; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (lower == "checker") { + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + bool dark = ((x / 32) + (y / 32)) & 1; + uint8_t v = dark ? 16 : 240; + size_t i2 = (static_cast(y) * W + x) * 3; + pixels[i2 + 0] = v; + pixels[i2 + 1] = v; + pixels[i2 + 2] = v; + } + } + } else if (lower == "grid") { + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + bool line = (x % 16 == 0) || (y % 16 == 0); + uint8_t v = line ? 240 : 32; + size_t i2 = (static_cast(y) * W + x) * 3; + pixels[i2 + 0] = v; + pixels[i2 + 1] = v; + pixels[i2 + 2] = v; + } + } + } else { + // Hex color. Accept "RGB" (3 chars) or "RRGGBB" (6 chars), + // optional leading '#'. + std::string hex = lower; + if (!hex.empty() && hex[0] == '#') hex.erase(0, 1); + auto fromHex = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + c - 'a'; + return -1; + }; + uint8_t r = 0, g = 0, b = 0; + if (hex.size() == 6) { + int hi, lo; + if ((hi = fromHex(hex[0])) < 0) goto bad_color; + if ((lo = fromHex(hex[1])) < 0) goto bad_color; + r = static_cast((hi << 4) | lo); + if ((hi = fromHex(hex[2])) < 0) goto bad_color; + if ((lo = fromHex(hex[3])) < 0) goto bad_color; + g = static_cast((hi << 4) | lo); + if ((hi = fromHex(hex[4])) < 0) goto bad_color; + if ((lo = fromHex(hex[5])) < 0) goto bad_color; + b = static_cast((hi << 4) | lo); + } else if (hex.size() == 3) { + int v0, v1, v2; + if ((v0 = fromHex(hex[0])) < 0) goto bad_color; + if ((v1 = fromHex(hex[1])) < 0) goto bad_color; + if ((v2 = fromHex(hex[2])) < 0) goto bad_color; + r = static_cast((v0 << 4) | v0); + g = static_cast((v1 << 4) | v1); + b = static_cast((v2 << 4) | v2); + } else { + goto bad_color; + } + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + size_t i2 = (static_cast(y) * W + x) * 3; + pixels[i2 + 0] = r; + pixels[i2 + 1] = g; + pixels[i2 + 2] = b; + } + } + goto color_ok; + bad_color: + std::fprintf(stderr, + "gen-texture: '%s' is not a valid hex color or 'checker'/'grid'\n", + spec.c_str()); + return 1; + color_ok: ; + } + if (!stbi_write_png(outPath.c_str(), W, H, 3, + pixels.data(), W * 3)) { + std::fprintf(stderr, + "gen-texture: stbi_write_png failed for %s\n", outPath.c_str()); + return 1; + } + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" size : %dx%d\n", W, H); + std::printf(" spec : %s\n", spec.c_str()); + return 0; +} + +int handleAddTextureToZone(int& i, int argc, char** argv) { + // Import an existing PNG into a zone directory. Useful + // for the "I have an artist-painted texture, get it into + // my project" workflow — complements --gen-texture + // (procedural placeholder) and --convert-blp-png (legacy + // BLP migration). + // + // Optional argument lets the user store the + // PNG under a project-specific name (e.g., a generic + // "stone.png" downloaded from a tileset becomes + // "courtyard_floor.png" in the zone). + // + // Refuses to overwrite an existing destination unless the + // source and destination are byte-identical (idempotent + // re-runs are safe). + std::string zoneDir = argv[++i]; + std::string srcPng = argv[++i]; + std::string renameTo; + if (i + 1 < argc && argv[i + 1][0] != '-') renameTo = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir) || !fs::is_directory(zoneDir)) { + std::fprintf(stderr, + "add-texture-to-zone: %s is not a directory\n", + zoneDir.c_str()); + return 1; + } + if (!fs::exists(srcPng) || !fs::is_regular_file(srcPng)) { + std::fprintf(stderr, + "add-texture-to-zone: %s is not a file\n", + srcPng.c_str()); + return 1; + } + // Sanity-check: must end in .png (any case) so users + // don't accidentally drop a .blp/.tga and get surprised + // when nothing renders. + std::string srcExt = fs::path(srcPng).extension().string(); + std::transform(srcExt.begin(), srcExt.end(), srcExt.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (srcExt != ".png") { + std::fprintf(stderr, + "add-texture-to-zone: %s is not a .png " + "(use --convert-blp-png for .blp first)\n", + srcPng.c_str()); + return 1; + } + std::string destLeaf = renameTo.empty() + ? fs::path(srcPng).filename().string() + : renameTo; + // If the rename arg lacks an extension, append .png so + // common typos ("stone" -> "stone.png") just work. + if (fs::path(destLeaf).extension().string().empty()) { + destLeaf += ".png"; + } + std::string destPath = zoneDir + "/" + destLeaf; + std::error_code ec; + if (fs::exists(destPath)) { + // Allow re-running if the bytes already match — makes + // makefile-driven workflows idempotent. + if (fs::file_size(srcPng, ec) == fs::file_size(destPath, ec)) { + std::ifstream a(srcPng, std::ios::binary); + std::ifstream b(destPath, std::ios::binary); + std::stringstream sa, sb; + sa << a.rdbuf(); sb << b.rdbuf(); + if (sa.str() == sb.str()) { + std::printf("Already present: %s (no-op)\n", + destPath.c_str()); + return 0; + } + } + std::fprintf(stderr, + "add-texture-to-zone: %s already exists with different " + "content (delete it first if intentional)\n", + destPath.c_str()); + return 1; + } + fs::copy_file(srcPng, destPath, ec); + if (ec) { + std::fprintf(stderr, + "add-texture-to-zone: copy failed (%s)\n", + ec.message().c_str()); + return 1; + } + uint64_t bytes = fs::file_size(destPath, ec); + std::printf("Imported %s -> %s\n", + srcPng.c_str(), destPath.c_str()); + std::printf(" bytes : %llu\n", + static_cast(bytes)); + std::printf(" next : --add-texture-to-mesh %s\n", + destPath.c_str()); + return 0; +} + + +} // namespace + +bool handleTextureHelpers(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-texture") == 0 && i + 2 < argc) { + outRc = handleGenTexture(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--add-texture-to-zone") == 0 && i + 2 < argc) { + outRc = handleAddTextureToZone(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_texture_helpers.hpp b/tools/editor/cli_texture_helpers.hpp new file mode 100644 index 00000000..eca9390b --- /dev/null +++ b/tools/editor/cli_texture_helpers.hpp @@ -0,0 +1,18 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the simple texture-helper handlers — placeholder +// generation and PNG import workflows that complement the +// procedural pattern generators in cli_gen_texture. +// --gen-texture solid hex / checker / grid PNG +// --add-texture-to-zone import an existing PNG into a zone +// +// Returns true if matched; outRc holds the exit code. +bool handleTextureHelpers(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 24ca947a..340fc54d 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -57,6 +57,7 @@ #include "cli_for_each.hpp" #include "cli_check.hpp" #include "cli_introspect.hpp" +#include "cli_texture_helpers.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -569,6 +570,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleIntrospect(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleTextureHelpers(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1497,206 +1501,6 @@ int main(int argc, char* argv[]) { outPath.c_str(), col.triangles.size(), col.walkableCount(), col.steepCount()); return 0; - } else if (std::strcmp(argv[i], "--gen-texture") == 0 && i + 2 < argc) { - // Synthesize a placeholder PNG texture. Lets users add a - // working texture to their project without an external - // image editor — useful for prototyping new meshes, - // filling out a zone before art is final, or generating - // test fixtures. - // - // : - // "RRGGBB" or "RGB" hex (case-insensitive) → solid color - // "checker" → 32x32 black/white checkerboard - // "grid" → black background with white 1-px grid every 16 - std::string outPath = argv[++i]; - std::string spec = argv[++i]; - int W = 256, H = 256; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { W = std::stoi(argv[++i]); } catch (...) {} - } - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { H = std::stoi(argv[++i]); } catch (...) {} - } - if (W < 1 || H < 1 || W > 8192 || H > 8192) { - std::fprintf(stderr, - "gen-texture: invalid size %dx%d (must be 1..8192)\n", W, H); - return 1; - } - std::vector pixels(static_cast(W) * H * 3, 0); - std::string lower = spec; - std::transform(lower.begin(), lower.end(), lower.begin(), - [](unsigned char c) { return std::tolower(c); }); - if (lower == "checker") { - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - bool dark = ((x / 32) + (y / 32)) & 1; - uint8_t v = dark ? 16 : 240; - size_t i2 = (static_cast(y) * W + x) * 3; - pixels[i2 + 0] = v; - pixels[i2 + 1] = v; - pixels[i2 + 2] = v; - } - } - } else if (lower == "grid") { - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - bool line = (x % 16 == 0) || (y % 16 == 0); - uint8_t v = line ? 240 : 32; - size_t i2 = (static_cast(y) * W + x) * 3; - pixels[i2 + 0] = v; - pixels[i2 + 1] = v; - pixels[i2 + 2] = v; - } - } - } else { - // Hex color. Accept "RGB" (3 chars) or "RRGGBB" (6 chars), - // optional leading '#'. - std::string hex = lower; - if (!hex.empty() && hex[0] == '#') hex.erase(0, 1); - auto fromHex = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return 10 + c - 'a'; - return -1; - }; - uint8_t r = 0, g = 0, b = 0; - if (hex.size() == 6) { - int hi, lo; - if ((hi = fromHex(hex[0])) < 0) goto bad_color; - if ((lo = fromHex(hex[1])) < 0) goto bad_color; - r = static_cast((hi << 4) | lo); - if ((hi = fromHex(hex[2])) < 0) goto bad_color; - if ((lo = fromHex(hex[3])) < 0) goto bad_color; - g = static_cast((hi << 4) | lo); - if ((hi = fromHex(hex[4])) < 0) goto bad_color; - if ((lo = fromHex(hex[5])) < 0) goto bad_color; - b = static_cast((hi << 4) | lo); - } else if (hex.size() == 3) { - int v0, v1, v2; - if ((v0 = fromHex(hex[0])) < 0) goto bad_color; - if ((v1 = fromHex(hex[1])) < 0) goto bad_color; - if ((v2 = fromHex(hex[2])) < 0) goto bad_color; - r = static_cast((v0 << 4) | v0); - g = static_cast((v1 << 4) | v1); - b = static_cast((v2 << 4) | v2); - } else { - goto bad_color; - } - for (int y = 0; y < H; ++y) { - for (int x = 0; x < W; ++x) { - size_t i2 = (static_cast(y) * W + x) * 3; - pixels[i2 + 0] = r; - pixels[i2 + 1] = g; - pixels[i2 + 2] = b; - } - } - goto color_ok; - bad_color: - std::fprintf(stderr, - "gen-texture: '%s' is not a valid hex color or 'checker'/'grid'\n", - spec.c_str()); - return 1; - color_ok: ; - } - if (!stbi_write_png(outPath.c_str(), W, H, 3, - pixels.data(), W * 3)) { - std::fprintf(stderr, - "gen-texture: stbi_write_png failed for %s\n", outPath.c_str()); - return 1; - } - std::printf("Wrote %s\n", outPath.c_str()); - std::printf(" size : %dx%d\n", W, H); - std::printf(" spec : %s\n", spec.c_str()); - return 0; - } else if (std::strcmp(argv[i], "--add-texture-to-zone") == 0 && i + 2 < argc) { - // Import an existing PNG into a zone directory. Useful - // for the "I have an artist-painted texture, get it into - // my project" workflow — complements --gen-texture - // (procedural placeholder) and --convert-blp-png (legacy - // BLP migration). - // - // Optional argument lets the user store the - // PNG under a project-specific name (e.g., a generic - // "stone.png" downloaded from a tileset becomes - // "courtyard_floor.png" in the zone). - // - // Refuses to overwrite an existing destination unless the - // source and destination are byte-identical (idempotent - // re-runs are safe). - std::string zoneDir = argv[++i]; - std::string srcPng = argv[++i]; - std::string renameTo; - if (i + 1 < argc && argv[i + 1][0] != '-') renameTo = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir) || !fs::is_directory(zoneDir)) { - std::fprintf(stderr, - "add-texture-to-zone: %s is not a directory\n", - zoneDir.c_str()); - return 1; - } - if (!fs::exists(srcPng) || !fs::is_regular_file(srcPng)) { - std::fprintf(stderr, - "add-texture-to-zone: %s is not a file\n", - srcPng.c_str()); - return 1; - } - // Sanity-check: must end in .png (any case) so users - // don't accidentally drop a .blp/.tga and get surprised - // when nothing renders. - std::string srcExt = fs::path(srcPng).extension().string(); - std::transform(srcExt.begin(), srcExt.end(), srcExt.begin(), - [](unsigned char c) { return std::tolower(c); }); - if (srcExt != ".png") { - std::fprintf(stderr, - "add-texture-to-zone: %s is not a .png " - "(use --convert-blp-png for .blp first)\n", - srcPng.c_str()); - return 1; - } - std::string destLeaf = renameTo.empty() - ? fs::path(srcPng).filename().string() - : renameTo; - // If the rename arg lacks an extension, append .png so - // common typos ("stone" -> "stone.png") just work. - if (fs::path(destLeaf).extension().string().empty()) { - destLeaf += ".png"; - } - std::string destPath = zoneDir + "/" + destLeaf; - std::error_code ec; - if (fs::exists(destPath)) { - // Allow re-running if the bytes already match — makes - // makefile-driven workflows idempotent. - if (fs::file_size(srcPng, ec) == fs::file_size(destPath, ec)) { - std::ifstream a(srcPng, std::ios::binary); - std::ifstream b(destPath, std::ios::binary); - std::stringstream sa, sb; - sa << a.rdbuf(); sb << b.rdbuf(); - if (sa.str() == sb.str()) { - std::printf("Already present: %s (no-op)\n", - destPath.c_str()); - return 0; - } - } - std::fprintf(stderr, - "add-texture-to-zone: %s already exists with different " - "content (delete it first if intentional)\n", - destPath.c_str()); - return 1; - } - fs::copy_file(srcPng, destPath, ec); - if (ec) { - std::fprintf(stderr, - "add-texture-to-zone: copy failed (%s)\n", - ec.message().c_str()); - return 1; - } - uint64_t bytes = fs::file_size(destPath, ec); - std::printf("Imported %s -> %s\n", - srcPng.c_str(), destPath.c_str()); - std::printf(" bytes : %llu\n", - static_cast(bytes)); - std::printf(" next : --add-texture-to-mesh %s\n", - destPath.c_str()); - return 0; } else if (std::strcmp(argv[i], "--export-zone-deps-md") == 0 && i + 1 < argc) { // Markdown counterpart to --list-zone-deps. Writes a sortable // GitHub-rendered table of every external model the zone