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.
This commit is contained in:
Kelsi 2026-05-09 09:34:19 -07:00
parent e117e5aaff
commit f5f4c3d782
4 changed files with 264 additions and 200 deletions

View file

@ -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

View file

@ -0,0 +1,241 @@
#include "cli_texture_helpers.hpp"
#include "stb_image_write.h"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <system_error>
#include <vector>
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.
//
// <colorHex|pattern>:
// "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<uint8_t> pixels(static_cast<size_t>(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<size_t>(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<size_t>(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<uint8_t>((hi << 4) | lo);
if ((hi = fromHex(hex[2])) < 0) goto bad_color;
if ((lo = fromHex(hex[3])) < 0) goto bad_color;
g = static_cast<uint8_t>((hi << 4) | lo);
if ((hi = fromHex(hex[4])) < 0) goto bad_color;
if ((lo = fromHex(hex[5])) < 0) goto bad_color;
b = static_cast<uint8_t>((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<uint8_t>((v0 << 4) | v0);
g = static_cast<uint8_t>((v1 << 4) | v1);
b = static_cast<uint8_t>((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<size_t>(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 <renameTo> 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<unsigned long long>(bytes));
std::printf(" next : --add-texture-to-mesh <wom-base> %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

View file

@ -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

View file

@ -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.
//
// <colorHex|pattern>:
// "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<uint8_t> pixels(static_cast<size_t>(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<size_t>(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<size_t>(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<uint8_t>((hi << 4) | lo);
if ((hi = fromHex(hex[2])) < 0) goto bad_color;
if ((lo = fromHex(hex[3])) < 0) goto bad_color;
g = static_cast<uint8_t>((hi << 4) | lo);
if ((hi = fromHex(hex[4])) < 0) goto bad_color;
if ((lo = fromHex(hex[5])) < 0) goto bad_color;
b = static_cast<uint8_t>((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<uint8_t>((v0 << 4) | v0);
g = static_cast<uint8_t>((v1 << 4) | v1);
b = static_cast<uint8_t>((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<size_t>(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 <renameTo> 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<unsigned long long>(bytes));
std::printf(" next : --add-texture-to-mesh <wom-base> %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