feat(editor): complete importOpen, keyboard shortcuts, DBC exporter

- importOpen now loads WHM alpha maps + full WOT metadata (textures,
  layers, holes, water) — was height-only stub
- DBC exporter migrated to nlohmann/json (last naive JSON file)
- Add Z-axis gizmo constraint (Z key) alongside X/Y
- Add Ctrl+Y as alternate redo binding
- Add F1 keyboard shortcut for help panel toggle
- Update help panel: document Z-axis, Ctrl+Y, all shortcuts
This commit is contained in:
Kelsi 2026-05-05 13:15:00 -07:00
parent 5b180c5579
commit 97e7a4c71a
5 changed files with 117 additions and 35 deletions

View file

@ -1,6 +1,7 @@
#include "dbc_exporter.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
@ -16,49 +17,37 @@ bool DBCExporter::exportAsJson(pipeline::AssetManager* am,
namespace fs = std::filesystem;
fs::create_directories(fs::path(outputPath).parent_path());
std::ofstream f(outputPath);
if (!f) return false;
f << "{\n";
f << " \"format\": \"wowee-dbc-json-1.0\",\n";
f << " \"source\": \"" << dbcName << "\",\n";
f << " \"recordCount\": " << dbc->getRecordCount() << ",\n";
f << " \"fieldCount\": " << dbc->getFieldCount() << ",\n";
f << " \"records\": [\n";
nlohmann::json j;
j["format"] = "wowee-dbc-json-1.0";
j["source"] = dbcName;
j["recordCount"] = dbc->getRecordCount();
j["fieldCount"] = dbc->getFieldCount();
nlohmann::json records = nlohmann::json::array();
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
f << " [";
for (uint32_t j = 0; j < dbc->getFieldCount(); j++) {
// Try to detect string fields vs numeric
uint32_t val = dbc->getUInt32(i, j);
// Check if it looks like a string offset (points into string block)
std::string strVal = dbc->getString(i, j);
nlohmann::json row = nlohmann::json::array();
for (uint32_t field = 0; field < dbc->getFieldCount(); field++) {
uint32_t val = dbc->getUInt32(i, field);
std::string strVal = dbc->getString(i, field);
if (!strVal.empty() && strVal[0] != '\0' && strVal.size() < 200) {
// Escape quotes in string
std::string escaped;
for (char c : strVal) {
if (c == '"') escaped += "\\\"";
else if (c == '\\') escaped += "\\\\";
else if (c == '\n') escaped += "\\n";
else escaped += c;
}
f << "\"" << escaped << "\"";
row.push_back(strVal);
} else {
// Check if it's a float
float fval = dbc->getFloat(i, j);
float fval = dbc->getFloat(i, field);
if (val != 0 && fval != 0.0f && fval > -1e10f && fval < 1e10f &&
static_cast<uint32_t>(fval) != val) {
f << fval;
row.push_back(fval);
} else {
f << val;
row.push_back(val);
}
}
if (j + 1 < dbc->getFieldCount()) f << ", ";
}
f << "]" << (i + 1 < dbc->getRecordCount() ? "," : "") << "\n";
records.push_back(row);
}
j["records"] = records;
f << " ]\n}\n";
std::ofstream f(outputPath);
if (!f) return false;
f << j.dump(2) << "\n";
LOG_INFO("DBC exported as JSON: ", dbcName, "", outputPath,
" (", dbc->getRecordCount(), " records)");
@ -67,7 +56,6 @@ bool DBCExporter::exportAsJson(pipeline::AssetManager* am,
int DBCExporter::exportZoneDBCs(pipeline::AssetManager* am,
const std::string& outputDir) {
// Zone-relevant DBCs for custom content
const char* zoneDBCs[] = {
"AreaTable.dbc",
"Map.dbc",

View file

@ -241,6 +241,9 @@ void EditorApp::processEvents() {
if (sc == SDL_SCANCODE_6) setMode(EditorMode::Quest);
}
// F1 handled by UI (showHelp_ toggle)
// F1 = toggle help
if (sc == SDL_SCANCODE_F1 && !io.WantCaptureKeyboard)
ui_.toggleHelp();
// Transform shortcuts (Blender-style)
if (objectPlacer_.getSelected()) {
if (sc == SDL_SCANCODE_G) startGizmoMode(TransformMode::Move);
@ -248,6 +251,8 @@ void EditorApp::processEvents() {
if (sc == SDL_SCANCODE_T) startGizmoMode(TransformMode::Scale);
if (sc == SDL_SCANCODE_X) setGizmoAxis(TransformAxis::X);
if (sc == SDL_SCANCODE_Y) setGizmoAxis(TransformAxis::Y);
if (sc == SDL_SCANCODE_Z && !(event.key.keysym.mod & KMOD_CTRL))
setGizmoAxis(TransformAxis::Z);
if (sc == SDL_SCANCODE_ESCAPE) {
viewport_.getGizmo().endDrag();
viewport_.getGizmo().setMode(TransformMode::None);
@ -269,10 +274,16 @@ void EditorApp::processEvents() {
ui_.openNewTerrainDialog();
if (sc == SDL_SCANCODE_O && (event.key.keysym.mod & KMOD_CTRL))
ui_.openLoadDialog();
// Ctrl+Y = Redo (alternate binding)
if (sc == SDL_SCANCODE_Y && (event.key.keysym.mod & KMOD_CTRL)) {
if (terrainEditor_.history().canRedo()) {
terrainEditor_.redo();
showToast("Redo");
}
}
if (sc == SDL_SCANCODE_Z && (event.key.keysym.mod & KMOD_CTRL)) {
bool isRedo = (event.key.keysym.mod & KMOD_SHIFT) != 0;
if (isRedo) {
// Ctrl+Shift+Z = Redo (sculpt only for now)
if (terrainEditor_.history().canRedo()) {
terrainEditor_.redo();
showToast("Redo");

View file

@ -418,14 +418,14 @@ void EditorUI::renderMenuBar(EditorApp& app) {
ImGui::BulletText("Ctrl+click — select object/NPC");
ImGui::BulletText("Ctrl+S — quick save");
ImGui::BulletText("Ctrl+Z — undo");
ImGui::BulletText("Ctrl+Shift+Z — redo");
ImGui::BulletText("Ctrl+Shift+Z / Ctrl+Y — redo");
ImGui::BulletText("Delete — remove selected");
ImGui::Separator();
ImGui::Text("Object Transform:");
ImGui::BulletText("G — move mode (then drag)");
ImGui::BulletText("R — rotate mode (then drag)");
ImGui::BulletText("T — scale mode (then drag)");
ImGui::BulletText("X/Y — constrain to axis");
ImGui::BulletText("X/Y/Z — constrain to axis");
ImGui::BulletText("Escape — deselect / cancel");
ImGui::BulletText("Right-click — context menu");
ImGui::Separator();

View file

@ -20,6 +20,7 @@ public:
void processActions(EditorApp& app);
void openNewTerrainDialog() { showNewDialog_ = true; }
void openLoadDialog() { showLoadDialog_ = true; }
void toggleHelp() { showHelp_ = !showHelp_; }
PaintMode getPaintMode() const { return paintMode_; }

View file

@ -217,6 +217,7 @@ int WoweeTerrain::exportAlphaMaps(const pipeline::ADTTerrain& terrain,
}
bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain& terrain) {
// Load binary heightmap (.whm)
std::string hmPath = basePath + ".whm";
std::ifstream f(hmPath, std::ios::binary);
if (!f) return false;
@ -233,10 +234,91 @@ bool WoweeTerrain::importOpen(const std::string& basePath, pipeline::ADTTerrain&
for (int ci = 0; ci < 256; ci++) {
auto& chunk = terrain.chunks[ci];
chunk.heightMap.loaded = true;
chunk.indexX = ci % 16;
chunk.indexY = ci / 16;
float base;
f.read(reinterpret_cast<char*>(&base), 4);
chunk.position[2] = base;
f.read(reinterpret_cast<char*>(chunk.heightMap.heights.data()), 145 * 4);
uint32_t alphaSize = 0;
if (f.read(reinterpret_cast<char*>(&alphaSize), 4) && alphaSize > 0 && alphaSize <= 65536) {
chunk.alphaMap.resize(alphaSize);
f.read(reinterpret_cast<char*>(chunk.alphaMap.data()), alphaSize);
}
for (int i = 0; i < 145; i++) {
chunk.normals[i * 3 + 0] = 0;
chunk.normals[i * 3 + 1] = 0;
chunk.normals[i * 3 + 2] = 127;
}
}
// Load JSON metadata (.wot)
std::string wotPath = basePath + ".wot";
std::ifstream wf(wotPath);
if (wf) {
try {
auto j = nlohmann::json::parse(wf);
terrain.coord.x = j.value("tileX", 0);
terrain.coord.y = j.value("tileY", 0);
float tileSize = 533.33333f;
float chunkSize = tileSize / 16.0f;
for (int cy = 0; cy < 16; cy++) {
for (int cx = 0; cx < 16; cx++) {
auto& chunk = terrain.chunks[cy * 16 + cx];
chunk.position[0] = (32.0f - terrain.coord.x) * tileSize - cx * chunkSize;
chunk.position[1] = (32.0f - terrain.coord.y) * tileSize - cy * chunkSize;
}
}
if (j.contains("textures") && j["textures"].is_array()) {
for (const auto& tex : j["textures"]) {
if (tex.is_string() && !tex.get<std::string>().empty())
terrain.textures.push_back(tex.get<std::string>());
}
}
if (j.contains("chunkLayers") && j["chunkLayers"].is_array()) {
const auto& layers = j["chunkLayers"];
for (int ci = 0; ci < std::min(256, static_cast<int>(layers.size())); ci++) {
const auto& cl = layers[ci];
if (cl.contains("layers") && cl["layers"].is_array()) {
for (const auto& texId : cl["layers"]) {
pipeline::TextureLayer layer{};
layer.textureId = texId.get<uint32_t>();
layer.flags = terrain.chunks[ci].layers.empty() ? 0 : 0x100;
terrain.chunks[ci].layers.push_back(layer);
}
}
if (cl.contains("holes"))
terrain.chunks[ci].holes = cl["holes"].get<uint16_t>();
}
}
if (j.contains("water") && j["water"].is_array()) {
for (const auto& w : j["water"]) {
if (w.is_null()) continue;
int wci = w.value("chunk", -1);
if (wci < 0 || wci >= 256) continue;
pipeline::ADTTerrain::WaterLayer wl;
wl.liquidType = w.value("type", 0u);
wl.maxHeight = w.value("height", 0.0f);
wl.minHeight = wl.maxHeight;
wl.x = 0; wl.y = 0; wl.width = 9; wl.height = 9;
wl.heights.assign(81, wl.maxHeight);
wl.mask.assign(8, 0xFF);
terrain.waterData[wci].layers.push_back(wl);
}
}
LOG_INFO("WOT metadata loaded: tile [", terrain.coord.x, ",", terrain.coord.y,
"], ", terrain.textures.size(), " textures");
} catch (const std::exception& e) {
LOG_WARNING("Could not parse WOT metadata: ", e.what());
}
}
LOG_INFO("Open terrain imported: ", basePath);