Files
tinyusdz/examples/tusdcat/main.cc
Syoyo Fujita 39fbeca468 Add --inspect option to tusdcat for USD Layer/Stage inspection
Adds a new inspection feature similar to pxrUSD's sdfdump, providing
YAML-like tree output of USD Layer structure. New files:
- src/usd-dump.hh/cc: InspectLayer/InspectStage implementation
- GlobMatch/GlobMatchPath functions in str-util for pattern matching

New CLI options:
- --inspect: YAML-like Layer inspection output
- --inspect-json: JSON output (placeholder, not yet implemented)
- --value=MODE: Value printing mode (none/snip/full)
- --snip=N: Number of items to show in snip mode
- --path=PATTERN: Filter prims by path glob pattern (supports **)
- --attr=PATTERN: Filter attributes by name glob pattern
- --time=T or --time=S:E: TimeSamples time query

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 06:35:17 +09:00

735 lines
24 KiB
C++

#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <sstream>
#include "tinyusdz.hh"
#include "layer.hh"
#include "pprinter.hh"
#include "str-util.hh"
#include "io-util.hh"
#include "usd-to-json.hh"
#include "usd-dump.hh"
#include "logger.hh"
#include "tydra/scene-access.hh"
struct CompositionFeatures {
bool subLayers{true};
bool inherits{true};
bool variantSets{true};
bool references{true};
bool payload{true}; // Not 'payloads'
bool specializes{true};
};
static std::string GetFileExtension(const std::string &filename) {
if (filename.find_last_of('.') != std::string::npos)
return filename.substr(filename.find_last_of('.') + 1);
return "";
}
static std::string str_tolower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); }
);
return s;
}
static std::string format_memory_size(size_t bytes) {
const char* units[] = {"B", "KB", "MB", "GB", "TB"};
int unit_index = 0;
double size = static_cast<double>(bytes);
while (size >= 1024.0 && unit_index < 4) {
size /= 1024.0;
unit_index++;
}
std::stringstream ss;
if (unit_index == 0) {
ss << static_cast<size_t>(size) << " " << units[unit_index];
} else {
ss.precision(2);
ss << std::fixed << size << " " << units[unit_index];
}
return ss.str();
}
// Progress bar state
struct ProgressState {
std::chrono::steady_clock::time_point start_time;
bool display_started{false};
float last_progress{0.0f};
static constexpr int kBarWidth = 40;
static constexpr double kDelaySeconds = 3.0; // Don't show progress under 3 seconds
};
static bool progress_callback(float progress, void *userptr) {
ProgressState *state = static_cast<ProgressState*>(userptr);
if (!state) {
return true;
}
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - state->start_time).count();
// Don't show progress if loading takes less than 3 seconds
if (elapsed < ProgressState::kDelaySeconds) {
return true;
}
// Only update display if progress changed significantly (1% or more)
if (progress - state->last_progress < 0.01f && progress < 1.0f) {
return true;
}
state->last_progress = progress;
if (!state->display_started) {
state->display_started = true;
std::cerr << "\n"; // Start on new line
}
int percent = static_cast<int>(progress * 100.0f);
int filled = static_cast<int>(progress * ProgressState::kBarWidth);
std::cerr << "\r[";
for (int i = 0; i < ProgressState::kBarWidth; ++i) {
if (i < filled) {
std::cerr << "=";
} else if (i == filled) {
std::cerr << ">";
} else {
std::cerr << " ";
}
}
std::cerr << "] " << std::setw(3) << percent << " %" << std::flush;
if (progress >= 1.0f) {
std::cerr << "\n"; // Finish with newline
}
return true; // Continue parsing
}
void print_help() {
std::cout << "Usage: tusdcat [OPTIONS] input.usda/usdc/usdz\n";
std::cout << "\n";
std::cout << "Options:\n";
std::cout << " -h, --help Show this help message\n";
std::cout << " -f, --flatten Do composition (load sublayers, references,\n";
std::cout << " payload, evaluate `over`, inherit, variants)\n";
std::cout << " (not fully implemented yet)\n";
std::cout << " --composition=LIST Specify which composition features to enable\n";
std::cout << " (valid when --flatten is supplied).\n";
std::cout << " Comma-separated list of:\n";
std::cout << " l or subLayers, i or inherits,\n";
std::cout << " v or variantSets, r or references,\n";
std::cout << " p or payload, s or specializes\n";
std::cout << " Example: --composition=r,p\n";
std::cout << " --extract-variants Dump variants information to JSON (w.i.p)\n";
std::cout << " --relative Print Path as relative Path (not implemented)\n";
std::cout << " -l, --loadOnly Load/parse USD file only (validate input)\n";
std::cout << " -j, --json Output parsed USD as JSON string\n";
std::cout << " --memstat Print memory usage statistics\n";
std::cout << " --progress Show ASCII progress bar\n";
std::cout << " (only shown if loading takes > 3 seconds)\n";
std::cout << " --loglevel INT Set logging level:\n";
std::cout << " 0=Debug, 1=Warn, 2=Info,\n";
std::cout << " 3=Error, 4=Critical, 5=Off\n";
std::cout << "\n";
std::cout << "Inspect options (YAML-like tree output):\n";
std::cout << " --inspect Inspect Layer structure (YAML-like output)\n";
std::cout << " --inspect-json Inspect Layer structure (JSON output)\n";
std::cout << " --value=MODE Value printing mode:\n";
std::cout << " none = schema only, no values\n";
std::cout << " snip = first N items (default)\n";
std::cout << " full = all values\n";
std::cout << " --snip=N Show first N items in snip mode (default: 8)\n";
std::cout << " --path=PATTERN Filter prims by path glob pattern\n";
std::cout << " (* = any chars, ** = any path segments)\n";
std::cout << " --attr=PATTERN Filter attributes by name glob pattern\n";
std::cout << " --time=T Query TimeSamples at time T\n";
std::cout << " --time=S:E Query TimeSamples in range [S, E]\n";
}
int main(int argc, char **argv) {
// Enable DCOUT output if TINYUSDZ_ENABLE_DCOUT environment variable is set
const char* enable_dcout_env = std::getenv("TINYUSDZ_ENABLE_DCOUT");
if (enable_dcout_env != nullptr && std::strlen(enable_dcout_env) > 0) {
// Any non-empty value enables DCOUT
tinyusdz::g_enable_dcout_output = true;
}
if (argc < 2) {
print_help();
return EXIT_FAILURE;
}
bool has_flatten{false};
bool has_relative{false};
bool has_extract_variants{false};
bool load_only{false};
bool json_output{false};
bool memstat{false};
bool show_progress{false};
// Inspect options
bool do_inspect{false};
tinyusdz::InspectOptions inspect_opts;
constexpr int kMaxIteration = 128;
std::string filepath;
int input_index = -1;
CompositionFeatures comp_features;
for (size_t i = 1; i < argc; i++) {
std::string arg = argv[i];
if ((arg.compare("-h") == 0) || (arg.compare("--help") ==0)) {
print_help();
return EXIT_FAILURE;
} else if ((arg.compare("-f") == 0) || (arg.compare("--flatten") == 0)) {
has_flatten = true;
} else if (arg.compare("--relative") == 0) {
has_relative = true;
} else if ((arg.compare("-l") == 0) || (arg.compare("--loadOnly") == 0)) {
load_only = true;
} else if ((arg.compare("-j") == 0) || (arg.compare("--json") == 0)) {
json_output = true;
} else if (arg.compare("--extract-variants") == 0) {
has_extract_variants = true;
} else if (arg.compare("--memstat") == 0) {
memstat = true;
} else if (arg.compare("--progress") == 0) {
show_progress = true;
} else if (arg.compare("--inspect") == 0) {
do_inspect = true;
inspect_opts.format = tinyusdz::InspectOutputFormat::Yaml;
} else if (arg.compare("--inspect-json") == 0) {
do_inspect = true;
inspect_opts.format = tinyusdz::InspectOutputFormat::Json;
} else if (tinyusdz::startsWith(arg, "--value=")) {
std::string value_str = tinyusdz::removePrefix(arg, "--value=");
if (value_str == "none") {
inspect_opts.value_mode = tinyusdz::InspectValueMode::NoValue;
} else if (value_str == "snip") {
inspect_opts.value_mode = tinyusdz::InspectValueMode::Snip;
} else if (value_str == "full") {
inspect_opts.value_mode = tinyusdz::InspectValueMode::Full;
} else {
std::cerr << "Invalid value mode: " << value_str
<< ". Use: none, snip, or full\n";
return EXIT_FAILURE;
}
} else if (tinyusdz::startsWith(arg, "--snip=")) {
std::string snip_str = tinyusdz::removePrefix(arg, "--snip=");
try {
int snip_val = std::stoi(snip_str);
if (snip_val < 1) {
std::cerr << "Invalid snip value: " << snip_val
<< ". Must be >= 1\n";
return EXIT_FAILURE;
}
inspect_opts.snip_count = static_cast<size_t>(snip_val);
} catch (...) {
std::cerr << "Invalid snip value: " << snip_str << "\n";
return EXIT_FAILURE;
}
} else if (tinyusdz::startsWith(arg, "--path=")) {
inspect_opts.prim_path_pattern = tinyusdz::removePrefix(arg, "--path=");
} else if (tinyusdz::startsWith(arg, "--attr=")) {
inspect_opts.attr_pattern = tinyusdz::removePrefix(arg, "--attr=");
} else if (tinyusdz::startsWith(arg, "--time=")) {
std::string time_str = tinyusdz::removePrefix(arg, "--time=");
inspect_opts.has_time_query = true;
// Check for range format "start:end"
size_t colon_pos = time_str.find(':');
if (colon_pos != std::string::npos) {
std::string start_str = time_str.substr(0, colon_pos);
std::string end_str = time_str.substr(colon_pos + 1);
try {
inspect_opts.time_start = std::stod(start_str);
inspect_opts.time_end = std::stod(end_str);
} catch (...) {
std::cerr << "Invalid time range: " << time_str << "\n";
return EXIT_FAILURE;
}
} else {
// Single time value
try {
double t = std::stod(time_str);
inspect_opts.time_start = t;
inspect_opts.time_end = t;
} catch (...) {
std::cerr << "Invalid time value: " << time_str << "\n";
return EXIT_FAILURE;
}
}
} else if (arg.compare("--loglevel") == 0) {
if (i + 1 >= argc) {
std::cerr << "--loglevel requires an integer argument\n";
return EXIT_FAILURE;
}
i++; // Move to next argument
try {
int log_level = std::stoi(argv[i]);
if (log_level < 0 || log_level > 5) {
std::cerr << "Invalid log level: " << log_level << ". Must be between 0 and 5.\n";
return EXIT_FAILURE;
}
tinyusdz::logging::Logger::getInstance().setLogLevel(
static_cast<tinyusdz::logging::LogLevel>(log_level));
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid log level argument: " << argv[i] << ". Must be an integer.\n";
return EXIT_FAILURE;
} catch (const std::out_of_range& e) {
std::cerr << "Log level value out of range: " << argv[i] << "\n";
return EXIT_FAILURE;
}
} else if (tinyusdz::startsWith(arg, "--composition=")) {
std::string value_str = tinyusdz::removePrefix(arg, "--composition=");
if (value_str.empty()) {
std::cerr << "No values specified to --composition.\n";
exit(-1);
}
std::vector<std::string> items = tinyusdz::split(value_str, ",");
comp_features.subLayers = false;
comp_features.inherits = false;
comp_features.variantSets = false;
comp_features.references = false;
comp_features.payload = false;
comp_features.specializes = false;
for (const auto &item : items) {
if ((item == "l") || (item == "subLayers")) {
comp_features.subLayers = true;
} else if ((item == "i") || (item == "inherits")) {
comp_features.inherits = true;
} else if ((item == "v") || (item == "variantSets")) {
comp_features.variantSets = true;
} else if ((item == "r") || (item == "references")) {
comp_features.references = true;
} else if ((item == "p") || (item == "payload")) {
comp_features.payload = true;
} else if ((item == "s") || (item == "specializes")) {
comp_features.specializes = true;
} else {
std::cerr << "Invalid string for --composition : " << item << "\n";
exit(-1);
}
}
} else {
filepath = arg;
input_index = i;
}
}
if (filepath.empty() || (input_index < 0)) {
std::cout << "Input USD filename missing.\n";
return EXIT_FAILURE;
}
std::string warn;
std::string err;
std::string ext = str_tolower(GetFileExtension(filepath));
std::string base_dir;
base_dir = tinyusdz::io::GetBaseDir(filepath);
// Handle --inspect mode
if (do_inspect) {
// Load as Layer for inspection
tinyusdz::Layer layer;
bool ret = tinyusdz::LoadLayerFromFile(filepath, &layer, &warn, &err);
if (!warn.empty()) {
std::cerr << "WARN: " << warn << "\n";
}
if (!ret) {
std::cerr << "Failed to load USD file as Layer: " << filepath << "\n";
if (!err.empty()) {
std::cerr << err << "\n";
}
return EXIT_FAILURE;
}
// Output inspection result
std::string output = tinyusdz::InspectLayer(layer, inspect_opts);
std::cout << output;
return EXIT_SUCCESS;
}
if (has_flatten) {
if (load_only) {
std::cerr << "--flatten and --loadOnly cannot be specified at a time\n";
return EXIT_FAILURE;
}
// TODO: flatten for USDZ
if (tinyusdz::IsUSDZ(filepath)) {
std::cout << "--flatten is ignored for USDZ at the moment.\n";
tinyusdz::Stage stage;
tinyusdz::USDLoadOptions usdz_options;
// Set up progress callback if requested
ProgressState usdz_progress_state;
if (show_progress) {
usdz_progress_state.start_time = std::chrono::steady_clock::now();
usdz_options.progress_callback = progress_callback;
usdz_options.progress_userptr = &usdz_progress_state;
}
bool ret = tinyusdz::LoadUSDZFromFile(filepath, &stage, &warn, &err, usdz_options);
if (!warn.empty()) {
std::cerr << "WARN : " << warn << "\n";
}
if (!err.empty()) {
std::cerr << "ERR : " << err << "\n";
//return EXIT_FAILURE;
}
if (!ret) {
std::cerr << "Failed to load USDZ file: " << filepath << "\n";
return EXIT_FAILURE;
}
if (memstat) {
size_t stage_mem = stage.estimate_memory_usage();
std::cout << "# Memory Statistics (Stage from USDZ)\n";
std::cout << " Stage memory usage: " << format_memory_size(stage_mem)
<< " (" << stage_mem << " bytes)\n\n";
}
if (json_output) {
#if defined(TINYUSDZ_WITH_JSON)
auto json_result = tinyusdz::ToJSON(stage);
if (json_result) {
std::cout << json_result.value() << "\n";
} else {
std::cerr << "Failed to convert USDZ stage to JSON: " << json_result.error() << "\n";
return EXIT_FAILURE;
}
} else {
std::cout << to_string(stage) << "\n";
#else
std::cerr << "JSON output is not supported in this build\n";
return EXIT_FAILURE;
#endif
}
return EXIT_SUCCESS;
}
tinyusdz::Layer root_layer;
bool ret = tinyusdz::LoadLayerFromFile(filepath, &root_layer, &warn, &err);
if (warn.size()) {
std::cout << "WARN: " << warn << "\n";
}
if (!ret) {
std::cerr << "Failed to read USD data as Layer: \n";
std::cerr << err << "\n";
return -1;
}
if (memstat) {
size_t layer_mem = root_layer.estimate_memory_usage();
std::cout << "# Memory Statistics (Layer)\n";
std::cout << " Layer memory usage: " << format_memory_size(layer_mem)
<< " (" << layer_mem << " bytes)\n\n";
}
std::cout << "# input\n";
std::cout << root_layer << "\n";
tinyusdz::Stage stage;
stage.metas() = root_layer.metas();
std::string warn;
tinyusdz::AssetResolutionResolver resolver;
resolver.set_current_working_path(base_dir);
resolver.set_search_paths({base_dir});
//
// LIVRPS strength ordering
// - [x] Local(subLayers)
// - [x] Inherits
// - [x] VariantSets
// - [x] References
// - [x] Payload
// - [ ] Specializes
//
tinyusdz::Layer src_layer = root_layer;
if (comp_features.subLayers) {
tinyusdz::Layer composited_layer;
if (!tinyusdz::CompositeSublayers(resolver, src_layer, &composited_layer, &warn, &err)) {
std::cerr << "Failed to composite subLayers: " << err << "\n";
return -1;
}
if (warn.size()) {
std::cout << "WARN: " << warn << "\n";
}
std::cout << "# `subLayers` composited\n";
std::cout << composited_layer << "\n";
src_layer = std::move(composited_layer);
}
// TODO: Find more better way to Recursively resolve references/payload/variants
for (int i = 0; i < kMaxIteration; i++) {
bool has_unresolved = false;
if (comp_features.references) {
if (!src_layer.check_unresolved_references()) {
std::cout << "# iter " << i << ": no unresolved references.\n";
} else {
has_unresolved = true;
tinyusdz::Layer composited_layer;
if (!tinyusdz::CompositeReferences(resolver, src_layer, &composited_layer, &warn, &err)) {
std::cerr << "Failed to composite `references`: " << err << "\n";
return -1;
}
if (warn.size()) {
std::cout << "WARN: " << warn << "\n";
}
std::cout << "# `references` composited\n";
std::cout << composited_layer << "\n";
src_layer = std::move(composited_layer);
}
}
if (comp_features.payload) {
if (!src_layer.check_unresolved_payload()) {
std::cout << "# iter " << i << ": no unresolved payload.\n";
} else {
has_unresolved = true;
tinyusdz::Layer composited_layer;
if (!tinyusdz::CompositePayload(resolver, src_layer, &composited_layer, &warn, &err)) {
std::cerr << "Failed to composite `payload`: " << err << "\n";
return -1;
}
if (warn.size()) {
std::cout << "WARN: " << warn << "\n";
}
std::cout << "# `payload` composited\n";
std::cout << composited_layer << "\n";
src_layer = std::move(composited_layer);
}
}
if (comp_features.inherits) {
if (!src_layer.check_unresolved_inherits()) {
std::cout << "# iter " << i << ": no unresolved inherits.\n";
} else {
has_unresolved = true;
tinyusdz::Layer composited_layer;
if (!tinyusdz::CompositeInherits(src_layer, &composited_layer, &warn, &err)) {
std::cerr << "Failed to composite `inherits`: " << err << "\n";
return -1;
}
if (warn.size()) {
std::cout << "WARN: " << warn << "\n";
}
std::cout << "# `inherits` composited\n";
std::cout << composited_layer << "\n";
src_layer = std::move(composited_layer);
}
}
if (comp_features.variantSets) {
if (!src_layer.check_unresolved_variant()) {
std::cout << "# iter " << i << ": no unresolved variant.\n";
} else {
has_unresolved = true;
tinyusdz::Layer composited_layer;
if (!tinyusdz::CompositeVariant(src_layer, &composited_layer, &warn, &err)) {
std::cerr << "Failed to composite `variantSet`: " << err << "\n";
return -1;
}
if (warn.size()) {
std::cout << "WARN: " << warn << "\n";
}
std::cout << "# `variantSet` composited\n";
std::cout << composited_layer << "\n";
src_layer = std::move(composited_layer);
}
}
// TODO
// - [ ] specializes
// - [ ] `class` Prim?
std::cout << "# has_unresolved_references: " << src_layer.check_unresolved_references() << "\n";
std::cout << "# all resolved? " << !has_unresolved << "\n";
if (!has_unresolved) {
std::cout << "# of composition iteration to resolve fully: " << (i + 1) << "\n";
break;
}
}
if (has_extract_variants) {
tinyusdz::Dictionary dict;
if (!tinyusdz::ExtractVariants(src_layer, &dict, &err)) {
std::cerr << "Failed to extract variants info: " << err;
} else {
std::cout << "== Variants info ==\n" << tinyusdz::to_string(dict) << "\n";
}
}
tinyusdz::Stage comp_stage;
ret = LayerToStage(std::move(src_layer), &comp_stage, &warn, &err);
if (warn.size()) {
std::cout << warn<< "\n";
}
if (!ret) {
std::cerr << err << "\n";
}
if (memstat) {
size_t stage_mem = comp_stage.estimate_memory_usage();
std::cout << "\n# Memory Statistics (Stage after composition)\n";
std::cout << " Stage memory usage: " << format_memory_size(stage_mem)
<< " (" << stage_mem << " bytes)\n\n";
}
if (json_output) {
#if defined(TINYUSDZ_WITH_JSON)
auto json_result = tinyusdz::ToJSON(comp_stage);
if (json_result) {
std::cout << json_result.value() << "\n";
} else {
std::cerr << "Failed to convert composed stage to JSON: " << json_result.error() << "\n";
return EXIT_FAILURE;
}
#else
std::cerr << "JSON output is not supported in this build\n";
#endif
} else {
std::cout << comp_stage.ExportToString() << "\n";
}
using MeshMap = std::map<std::string, const tinyusdz::GeomMesh *>;
MeshMap meshmap;
tinyusdz::tydra::ListPrims(comp_stage, meshmap);
for (const auto &item : meshmap) {
std::cout << "Prim : " << item.first << "\n";
}
} else {
tinyusdz::Stage stage;
tinyusdz::USDLoadOptions options;
// Set up progress callback if requested
ProgressState progress_state;
if (show_progress) {
progress_state.start_time = std::chrono::steady_clock::now();
options.progress_callback = progress_callback;
options.progress_userptr = &progress_state;
}
// auto detect format.
bool ret = tinyusdz::LoadUSDFromFile(filepath, &stage, &warn, &err, options);
if (!warn.empty()) {
std::cerr << "WARN : " << warn << "\n";
}
if (!err.empty()) {
std::cerr << "ERR : " << err << "\n";
//return EXIT_FAILURE;
}
if (!ret) {
std::cerr << "Failed to load USD file: " << filepath << "\n";
return EXIT_FAILURE;
}
if (load_only) {
if (memstat) {
size_t stage_mem = stage.estimate_memory_usage();
std::cout << "# Memory Statistics (Stage)\n";
std::cout << " Stage memory usage: " << format_memory_size(stage_mem)
<< " (" << stage_mem << " bytes)\n";
}
return EXIT_SUCCESS;
}
if (memstat) {
size_t stage_mem = stage.estimate_memory_usage();
std::cout << "# Memory Statistics (Stage)\n";
std::cout << " Stage memory usage: " << format_memory_size(stage_mem)
<< " (" << stage_mem << " bytes)\n\n";
}
if (json_output) {
#if defined(TINYUSDZ_WITH_JSON)
auto json_result = tinyusdz::ToJSON(stage);
if (json_result) {
std::cout << json_result.value() << "\n";
} else {
std::cerr << "Failed to convert stage to JSON: " << json_result.error() << "\n";
return EXIT_FAILURE;
}
#else
std::cerr << "JSON output is not supported in this build\n";
#endif
} else {
std::string s = stage.ExportToString(has_relative);
std::cout << s << "\n";
}
if (has_extract_variants) {
tinyusdz::Dictionary dict;
if (!tinyusdz::ExtractVariants(stage, &dict, &err)) {
std::cerr << "Failed to extract variants info: " << err;
} else {
std::cout << "== Variants info ==\n" << tinyusdz::to_string(dict) << "\n";
}
}
}
return EXIT_SUCCESS;
}