From 20bd4170028f3138a3de4befe80a75497450a7dc Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 19:35:32 -0700 Subject: [PATCH] feat(editor): add --export-bones-dot for Graphviz bone-tree visualization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders WOM bone hierarchy as Graphviz DOT. Mirrors --export-quest-graph for skeleton trees: a 50-bone tree from --info-bones is hard to read in text; pipe this through 'dot -Tpng' for the picture: wowee_editor --export-bones-dot HumanMale dot -Tpng HumanMale.bones.dot -o bones.png Visual encoding: - lightgreen fill: keybones (named anchor points referenced by gameplay systems — head, hands, feet, etc.) - lightgrey fill: internal/blend bones (non-key, used for shape only) - goldenrod border: root bones (parent=-1, top of skeleton hierarchy) Edges flow parent -> child (rankdir=TB so root is at top, leaves at bottom). Useful for skeleton-debugging: - 'why is this finger not following its hand?' -> see the parent chain - 'is this bone really a root or did its parent get deleted?' -> goldenrod border makes intentional roots vs accidental ones obvious - understanding which bones gameplay code can reference by key id Verified on a 5-bone synthesized skeleton (3-deep chain + 1 detached root + mix of key/internal): DOT correctly emits 5 nodes with the right colors (3 lightgreen for key bones, 2 lightgrey for internal, 2 with goldenrod root borders), 3 edges traversing the chain. --- tools/editor/main.cpp | 61 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 8cec89c0..4d1fb596 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -701,6 +701,8 @@ static void printUsage(const char* argv0) { std::printf(" List M2 animation sequences (id, duration, flags)\n"); std::printf(" --info-bones [--json]\n"); std::printf(" List M2 bones with parent tree, key-bone IDs, pivot offsets\n"); + std::printf(" --export-bones-dot [out.dot]\n"); + std::printf(" Render WOM bone hierarchy as Graphviz DOT (pipe to `dot -Tpng -o bones.png`)\n"); std::printf(" --list-zone-textures [--json]\n"); std::printf(" Aggregate texture refs across all WOM models in a zone (deduped)\n"); std::printf(" --info-wob [--json]\n"); @@ -820,7 +822,7 @@ int main(int argc, char* argv[]) { static const char* kArgRequired[] = { "--data", "--info", "--info-batches", "--info-textures", "--info-doodads", "--info-attachments", "--info-particles", "--info-sequences", - "--info-bones", "--list-zone-textures", + "--info-bones", "--export-bones-dot", "--list-zone-textures", "--info-wob", "--info-woc", "--info-wot", "--info-creatures", "--info-objects", "--info-quests", "--info-extract", "--info-extract-tree", "--info-extract-budget", @@ -1526,6 +1528,63 @@ int main(int argc, char* argv[]) { b.pivot.x, b.pivot.y, b.pivot.z); } return 0; + } else if (std::strcmp(argv[i], "--export-bones-dot") == 0 && i + 1 < argc) { + // Render WOM bone hierarchy as Graphviz DOT. Mirrors + // --export-quest-graph for skeleton trees: trying to read + // a 50-bone tree from --info-bones output is painful; + // pipe this through `dot -Tpng` for the picture. + std::string base = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") + base = base.substr(0, base.size() - 4); + if (!wowee::pipeline::WoweeModelLoader::exists(base)) { + std::fprintf(stderr, + "export-bones-dot: WOM not found: %s.wom\n", base.c_str()); + return 1; + } + if (outPath.empty()) outPath = base + ".bones.dot"; + auto wom = wowee::pipeline::WoweeModelLoader::load(base); + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-bones-dot: cannot write %s\n", outPath.c_str()); + return 1; + } + out << "digraph BoneTree {\n"; + out << " // Generated by wowee_editor --export-bones-dot\n"; + out << " rankdir=TB;\n"; + out << " node [shape=box, style=filled, fontname=\"sans-serif\", fontsize=10];\n"; + // Color: green for keybones (named anchor points), gray for + // internal/blend bones. Root bones (parent=-1) get yellow border. + for (size_t k = 0; k < wom.bones.size(); ++k) { + const auto& b = wom.bones[k]; + bool isKey = (b.keyBoneId >= 0); + std::string fill = isKey ? "lightgreen" : "lightgrey"; + std::string label = "[" + std::to_string(k) + "]"; + if (isKey) label += "\\nkey=" + std::to_string(b.keyBoneId); + out << " b" << k << " [label=\"" << label + << "\", fillcolor=" << fill; + if (b.parentBone == -1) out << ", penwidth=2, color=goldenrod"; + out << "];\n"; + } + // Edges: child -> parent (parent is up). + int rootCount = 0; + for (size_t k = 0; k < wom.bones.size(); ++k) { + int16_t p = wom.bones[k].parentBone; + if (p < 0 || p >= static_cast(wom.bones.size())) { + rootCount++; + continue; + } + out << " b" << p << " -> b" << k << ";\n"; + } + out << "}\n"; + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" %zu bones, %d root(s)\n", + wom.bones.size(), rootCount); + std::printf(" next: dot -Tpng %s -o bones.png\n", outPath.c_str()); + return 0; } else if (std::strcmp(argv[i], "--list-zone-textures") == 0 && i + 1 < argc) { // Aggregate texture references across every WOM model in a // zone directory. Companion to --list-zone-deps (which lists