diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 0f6792d3d2..4dec6867b0 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -10,10 +10,11 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/F3DConfig.h.in" "${CMAKE_CURRENT_BINARY_DIR}/F3DConfig.h") set(F3D_SOURCE_FILES + ${CMAKE_CURRENT_BINARY_DIR}/F3DIcon.cxx ${CMAKE_CURRENT_SOURCE_DIR}/F3DColorMapTools.cxx ${CMAKE_CURRENT_SOURCE_DIR}/F3DConfigFileTools.cxx - ${CMAKE_CURRENT_BINARY_DIR}/F3DIcon.cxx - ${CMAKE_CURRENT_SOURCE_DIR}/F3DOptionsParser.cxx + ${CMAKE_CURRENT_SOURCE_DIR}/F3DOptionsTools.cxx + ${CMAKE_CURRENT_SOURCE_DIR}/F3DPluginsTools.cxx ${CMAKE_CURRENT_SOURCE_DIR}/F3DStarter.cxx ${CMAKE_CURRENT_SOURCE_DIR}/F3DSystemTools.cxx ${CMAKE_CURRENT_SOURCE_DIR}/main.cxx @@ -86,6 +87,10 @@ set(f3d_compile_options_private "") set(f3d_compile_options_public "") set(f3d_link_options_public "") +# This is required to avoid cxxopts +# splitting input positional option by commas +target_compile_definitions(f3d PRIVATE "CXXOPTS_VECTOR_DELIMITER='\\0'") + # Headless EGL build if (VTK_OPENGL_HAS_EGL) target_compile_definitions(f3d PRIVATE F3D_HEADLESS_BUILD) diff --git a/application/F3DColorMapTools.cxx b/application/F3DColorMapTools.cxx index 0533f3246b..57961f65b2 100644 --- a/application/F3DColorMapTools.cxx +++ b/application/F3DColorMapTools.cxx @@ -1,6 +1,6 @@ #include "F3DColorMapTools.h" -#include "F3DConfigFileTools.h" +#include "F3DSystemTools.h" #include "image.h" #include "log.h" @@ -23,16 +23,14 @@ std::string Find(const std::string& str) } } - std::vector dirsToCheck; - dirsToCheck.emplace_back(F3DConfigFileTools::GetUserConfigFileDirectory() / "colormaps"); + std::vector dirsToCheck{ F3DSystemTools::GetUserConfigFileDirectory() / "colormaps", #ifdef __APPLE__ - dirsToCheck.emplace_back("/usr/local/etc/f3d/colormaps"); + "/usr/local/etc/f3d/colormaps", #endif #ifdef __linux__ - dirsToCheck.emplace_back("/etc/f3d/colormaps"); - dirsToCheck.emplace_back("/usr/share/f3d/colormaps"); + "/etc/f3d/colormaps", "/usr/share/f3d/colormaps", #endif - dirsToCheck.emplace_back(F3DConfigFileTools::GetBinaryResourceDirectory() / "colormaps"); + F3DSystemTools::GetBinaryResourceDirectory() / "colormaps" }; for (const fs::path& dir : dirsToCheck) { diff --git a/application/F3DConfigFileTools.cxx b/application/F3DConfigFileTools.cxx index 875ae7a0fc..12cf787b50 100644 --- a/application/F3DConfigFileTools.cxx +++ b/application/F3DConfigFileTools.cxx @@ -3,78 +3,24 @@ #include "F3DConfig.h" #include "F3DSystemTools.h" +#include "nlohmann/json.hpp" + #include "log.h" -#include #include +#include +#include #include namespace fs = std::filesystem; -//---------------------------------------------------------------------------- -fs::path F3DConfigFileTools::GetUserConfigFileDirectory() -{ - std::string applicationName = "f3d"; - fs::path dirPath; -#if defined(_WIN32) - const char* appData = std::getenv("APPDATA"); - if (!appData) - { - return {}; - } - dirPath = fs::path(appData); -#else - // Implementing XDG specifications - const char* xdgConfigHome = std::getenv("XDG_CONFIG_HOME"); - if (xdgConfigHome && strlen(xdgConfigHome) > 0) - { - dirPath = fs::path(xdgConfigHome); - } - else - { - const char* home = std::getenv("HOME"); - if (!home || strlen(home) == 0) - { - return {}; - } - dirPath = fs::path(home); - dirPath /= ".config"; - } -#endif - dirPath /= applicationName; - return dirPath; -} - -//---------------------------------------------------------------------------- -fs::path F3DConfigFileTools::GetBinaryResourceDirectory() +namespace { - fs::path dirPath; - try - { - dirPath = F3DSystemTools::GetApplicationPath(); - - // transform path to exe to path to install - // /install/bin/f3d -> /install - dirPath = fs::canonical(dirPath).parent_path().parent_path(); - - // Add binary specific paths -#if F3D_MACOS_BUNDLE - dirPath /= "Resources"; -#else - dirPath /= "share/f3d"; -#endif - } - catch (const fs::filesystem_error&) - { - f3d::log::debug("Cannot recover binary configuration file directory: ", dirPath.string()); - return {}; - } - - return dirPath; -} - //---------------------------------------------------------------------------- -std::vector F3DConfigFileTools::GetConfigPaths(const std::string& configSearch) +/** + * Recover a OS-specific vector of potential config file directories + */ +std::vector GetConfigPaths(const std::string& configSearch) { std::vector paths; @@ -88,8 +34,8 @@ std::vector F3DConfigFileTools::GetConfigPaths(const std::string& conf "/etc/f3d", "/usr/share/f3d/configs", #endif - F3DConfigFileTools::GetBinaryResourceDirectory() / "configs", - F3DConfigFileTools::GetUserConfigFileDirectory(), + F3DSystemTools::GetBinaryResourceDirectory() / "configs", + F3DSystemTools::GetUserConfigFileDirectory(), }; for (const fs::path& dir : dirsToCheck) @@ -131,3 +77,133 @@ std::vector F3DConfigFileTools::GetConfigPaths(const std::string& conf return paths; } +} + +//---------------------------------------------------------------------------- +F3DOptionsTools::OptionsEntries F3DConfigFileTools::ReadConfigFiles(const std::string& userConfig) +{ + // Default config directory name + std::string configSearch = "config"; + if (!userConfig.empty()) + { + // Check if provided userConfig is a full path + auto path = fs::path(userConfig); + if (path.stem() == userConfig || path.filename() == userConfig) + { + // Only a stem or a filename, use provided userConfig as configSearch + configSearch = userConfig; + } + else + { + // Assume its a full path and use as is, not searching for config files + configSearch = ""; + } + } + + // Recover config paths to search for config files + std::vector configPaths; + if (!configSearch.empty()) + { + for (const auto& path : ::GetConfigPaths(configSearch)) + { + configPaths.emplace_back(path); + } + } + else + { + configPaths.emplace_back(userConfig); + } + + // Recover actual individual config file paths + std::set actualConfigFilePaths; + for (auto configPath : configPaths) + { + // Recover an absolute canonical path to config file + try + { + configPath = fs::canonical(fs::path(configPath)).string(); + } + catch (const fs::filesystem_error&) + { + f3d::log::error("Configuration file does not exist: ", configPath.string(), " , ignoring it"); + continue; + } + + // Recover all config files if needed in directories + if (fs::is_directory(configPath)) + { + f3d::log::debug("Using config directory ", configPath.string()); + for (auto& entry : std::filesystem::directory_iterator(configPath)) + { + actualConfigFilePaths.emplace(entry); + } + } + else + { + f3d::log::debug("Using config file ", configPath.string()); + actualConfigFilePaths.emplace(configPath); + } + } + + // If we used a configSearch but did not find any, warn the user + if (!configSearch.empty() && actualConfigFilePaths.empty()) + { + f3d::log::warn("Configuration file for \"", configSearch, "\" could not be found"); + } + + // Read config files + F3DOptionsTools::OptionsEntries confEntries; + for (const auto& configFilePath : actualConfigFilePaths) + { + std::ifstream file(configFilePath); + if (!file.is_open()) + { + // Cannot be tested + f3d::log::warn( + "Unable to open the configuration file: ", configFilePath.string(), " , ignoring it"); + continue; + } + + // Read the file into a json + nlohmann::ordered_json json; + try + { + file >> json; + } + catch (const std::exception& ex) + { + f3d::log::error( + "Unable to parse the configuration file ", configFilePath.string(), " , ignoring it"); + f3d::log::error(ex.what()); + continue; + } + + // For each config "pattern" + for (const auto& configBlock : json.items()) + { + // Add each config entry into an option dict + F3DOptionsTools::OptionsDict entry; + for (const auto& item : configBlock.value().items()) + { + if (item.value().is_number() || item.value().is_boolean()) + { + entry[item.key()] = nlohmann::to_string(item.value()); + } + else if (item.value().is_string()) + { + entry[item.key()] = item.value().get(); + } + else + { + f3d::log::error(item.key(), " from ", configFilePath.string(), + " must be a string, a boolean or a number, ignoring entry"); + continue; + } + } + + // Emplace the option dict for that pattern into the config entries vector + confEntries.emplace_back(entry, configFilePath, configBlock.key()); + } + } + return confEntries; +} diff --git a/application/F3DConfigFileTools.h b/application/F3DConfigFileTools.h index 2991712a76..d166032a8b 100644 --- a/application/F3DConfigFileTools.h +++ b/application/F3DConfigFileTools.h @@ -1,21 +1,20 @@ +#ifndef F3DConfigFileTools_h +#define F3DConfigFileTools_h /** * @class F3DConfigFileTools - * @brief A namespace to recover path to config file and related directories, cross platform - * + * @brief A namespace to parse config files */ +#include "F3DOptionsTools.h" -#ifndef F3DConfigFileTools_h -#define F3DConfigFileTools_h - -#include #include -#include namespace F3DConfigFileTools { -std::filesystem::path GetUserConfigFileDirectory(); -std::filesystem::path GetBinaryResourceDirectory(); -std::vector GetConfigPaths(const std::string& configSearch); +/** + * Read config files using userConfig if any, return an optionEntries + * containing ordered optionDict. + */ +F3DOptionsTools::OptionsEntries ReadConfigFiles(const std::string& userConfig); } #endif diff --git a/application/F3DOptionsParser.cxx b/application/F3DOptionsParser.cxx deleted file mode 100644 index 7fe5986d86..0000000000 --- a/application/F3DOptionsParser.cxx +++ /dev/null @@ -1,841 +0,0 @@ -#include "F3DOptionsParser.h" - -#include "F3DConfig.h" -#include "F3DConfigFileTools.h" -#include "F3DException.h" -#include "F3DSystemTools.h" - -#include "cxxopts.hpp" -#include "nlohmann/json.hpp" - -#include "engine.h" -#include "export.h" -#include "interactor.h" -#include "log.h" -#include "options.h" -#include "utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -//---------------------------------------------------------------------------- -class ConfigurationOptions -{ -public: - ConfigurationOptions(int argc, char** argv) - : Argc(argc) - , Argv(argv) - { - this->ExecutableName = argc > 0 && argv[0][0] ? fs::path(argv[0]).filename().string() : "f3d"; - } - - void GetOptions(F3DAppOptions& appOptions, f3d::options& options, - std::vector& inputs, const std::string& filePathForConfigBlock = "", - bool allOptionsInitialized = false, bool parseCommandLine = true); - bool InitializeDictionaryFromConfigFile(const std::string& userConfigFile); - void LoadPlugins(const F3DAppOptions& appOptions) const; - std::vector GetPluginSearchPaths() const; - - enum class HasDefault : bool - { - YES = true, - NO = false - }; - - enum class MayHaveConfig : bool - { - YES = true, - NO = false - }; - - enum class HasImplicitValue : bool - { - YES = true, - NO = false - }; - -protected: - bool GetOptionConfig(const std::string& option, std::string& configValue) const - { - auto localIt = this->CurrentFileConfig.find(option); - if (localIt != this->CurrentFileConfig.end()) - { - configValue = localIt->second; - return true; - } - return false; - } - - template - static std::string ToString(T currValue) - { - std::stringstream ss; - ss << currValue; - return ss.str(); - } - - static std::string ToString(bool currValue) - { - return currValue ? "true" : "false"; - } - - template - static std::string ToString(const std::vector& currValue) - { - std::stringstream ss; - for (size_t i = 0; i < currValue.size(); i++) - { - ss << currValue[i]; - if (i != currValue.size() - 1) - { - ss << ","; - } - } - return ss.str(); - } - - std::string CollapseName(const std::string& longName, const std::string& shortName) const - { - std::stringstream ss; - if (shortName != "") - { - ss << shortName << ","; - } - ss << longName; - return ss.str(); - } - - void DeclareOption(cxxopts::OptionAdder& group, const std::string& longName, - const std::string& shortName, const std::string& doc) - { - this->AllLongOptions.push_back(longName); - group(this->CollapseName(longName, shortName), doc); - } - - template - void DeclareOption(cxxopts::OptionAdder& group, const std::string& longName, - const std::string& shortName, const std::string& doc, T& var, HasDefault hasDefault, - MayHaveConfig mayHaveConfig, const std::string& argHelp = "", - HasImplicitValue hasImplicitValue = HasImplicitValue::NO, const std::string& implicitValue = "") - { - bool hasDefaultBool = hasDefault == HasDefault::YES; - auto val = cxxopts::value(var); - - if (hasImplicitValue == HasImplicitValue::YES) - { - val->implicit_value(implicitValue); - } - - std::string defaultVal; - if (hasDefaultBool) - { - defaultVal = ConfigurationOptions::ToString(var); - } - - if (mayHaveConfig == MayHaveConfig::YES) - { - hasDefaultBool |= this->GetOptionConfig(longName, defaultVal); - } - - if (hasDefaultBool) - { - val = val->default_value(defaultVal); - } - var = {}; - this->AllLongOptions.push_back(longName); - group(this->CollapseName(longName, shortName), doc, val, argHelp); - } - - void PrintHelpPair( - const std::string& key, const std::string& help, int keyWidth = 10, int helpWidth = 70); - void PrintHelp(const cxxopts::Options& cxxOptions); - void PrintVersion(); - void PrintReadersList(); - void PrintPluginsScan(); - - std::pair GetClosestOption(const std::string& option) - { - std::pair ret = { "", std::numeric_limits::max() }; - - for (const std::string& name : this->AllLongOptions) - { - int distance = f3d::utils::textDistance(name, option); - if (distance < ret.second) - { - ret = { name, distance }; - } - } - - return ret; - } - -private: - int Argc; - char** Argv; - - using ConfigDict = std::map; - using ConfigEntry = std::tuple; - using ConfigEntries = std::vector; - ConfigEntries GlobalConfigEntries; - ConfigEntries RegexConfigEntries; - std::string ExecutableName; - std::vector AllLongOptions; - - ConfigDict CurrentFileConfig; -}; - -//---------------------------------------------------------------------------- -std::vector ConfigurationOptions::GetPluginSearchPaths() const -{ - // Recover F3D_PLUGINS_PATH first - auto searchPaths = F3DSystemTools::GetVectorEnvironnementVariable("F3D_PLUGINS_PATH"); -#if F3D_MACOS_BUNDLE - return searchPaths; -#else - // Add a executable related path - auto tmpPath = F3DSystemTools::GetApplicationPath(); - tmpPath = tmpPath.parent_path().parent_path(); - tmpPath /= F3D::PluginsInstallDir; - searchPaths.push_back(tmpPath.string()); - return searchPaths; -#endif -} - -//---------------------------------------------------------------------------- -void ConfigurationOptions::LoadPlugins(const F3DAppOptions& appOptions) const -{ - try - { - f3d::engine::autoloadPlugins(); - - for (const std::string& plugin : appOptions.Plugins) - { - if (!plugin.empty()) - { - f3d::engine::loadPlugin(plugin, this->GetPluginSearchPaths()); - } - } - } - catch (const f3d::engine::plugin_exception& e) - { - f3d::log::warn("Plugin failed to load: ", e.what()); - } -} - -//---------------------------------------------------------------------------- -void ConfigurationOptions::PrintPluginsScan() -{ -#if F3D_MACOS_BUNDLE - f3d::log::error("option not supported with the macOS bundle"); -#else - auto appPath = F3DSystemTools::GetApplicationPath(); - appPath = appPath.parent_path().parent_path(); - - appPath /= "share/f3d/plugins"; - - auto plugins = f3d::engine::getPluginsList(appPath.string()); - - f3d::log::info("Found ", plugins.size(), " plugins:"); - - for (const std::string& p : plugins) - { - f3d::log::info(" - ", p); - } -#endif -} - -//---------------------------------------------------------------------------- -void ConfigurationOptions::GetOptions(F3DAppOptions& appOptions, f3d::options& options, - std::vector& inputs, const std::string& filePathForConfigBlock, - bool allOptionsInitialized, bool parseCommandLine) -{ - inputs.clear(); /* needed because this function is called multiple times */ - - /* start with an empty config ... */ - ConfigDict tmpConfig; - - const auto update = [&](const ConfigDict& config) - { - /* insert or update the values from the argument into `tmpConfig`. - * also log the insertions/updates details in verbose mode */ - for (const auto& [key, value] : config) - { - const std::string del = tmpConfig.count(key) ? key + ": " + tmpConfig[key] : ""; - const std::string add = std::string(key).append(": ").append(value); - if (add == del) - { - f3d::log::debug("= ", add); - } - else - { - if (!del.empty()) - { - f3d::log::debug("- ", del); - } - f3d::log::debug("+ ", add); - } - - tmpConfig[key] = value; - } - }; - - /* apply global entries, `pattern` will always be `"global"` here */ - for (const auto& [conf, source, pattern] : this->GlobalConfigEntries) - { - f3d::log::debug("using `", pattern, "` config from ", source.string()); - update(conf); - } - - /* ... then go through matching regex configs and override */ - if (!filePathForConfigBlock.empty()) - { - for (auto const& [conf, source, pattern] : this->RegexConfigEntries) - { - std::regex re(pattern, std::regex_constants::icase); - std::smatch matches; - if (std::regex_match(filePathForConfigBlock, matches, re)) - { - f3d::log::debug("using `", pattern, "` config from ", source.string()); - update(conf); - } - } - } - - this->CurrentFileConfig = tmpConfig; - - // When parsing multiple times, hasDefault should be forced to yes after the first pass as all - // options are expected to be already initialized, which means they have a "default" in the - // cxxopts sense. - HasDefault LocalHasDefaultNo = allOptionsInitialized ? HasDefault::YES : HasDefault::NO; - - std::unordered_map libf3dOptions; - std::vector keys = options.getNames(); - for (const std::string& key : keys) - { - libf3dOptions.emplace(key, options.getAsString(key)); - } - - try - { - cxxopts::Options cxxOptions(this->ExecutableName, F3D::AppTitle); - cxxOptions.custom_help("[OPTIONS...] file1 file2 ..."); - // clang-format off - auto grp0 = cxxOptions.add_options("Applicative"); - this->DeclareOption(grp0, "output", "", "Render to file", appOptions.Output, LocalHasDefaultNo, MayHaveConfig::YES, ""); - this->DeclareOption(grp0, "no-background", "", "No background when render to file", appOptions.NoBackground, HasDefault::YES, MayHaveConfig::YES); - this->DeclareOption(grp0, "help", "h", "Print help"); - this->DeclareOption(grp0, "version", "", "Print version details"); - this->DeclareOption(grp0, "readers-list", "", "Print the list of readers"); - this->DeclareOption(grp0, "config", "", "Specify the configuration file to use. absolute/relative path or filename/filestem to search in configuration file locations.", appOptions.UserConfigFile, LocalHasDefaultNo, MayHaveConfig::NO, ""); - this->DeclareOption(grp0, "dry-run", "", "Do not read the configuration file", appOptions.DryRun, HasDefault::YES, MayHaveConfig::NO ); - this->DeclareOption(grp0, "no-render", "", "Do not render anything and quit right after loading the first file, use with --verbose to recover information about a file.", appOptions.NoRender, HasDefault::YES, MayHaveConfig::YES); - this->DeclareOption(grp0, "max-size", "", "Maximum size in Mib of a file to load, negative value means unlimited", appOptions.MaxSize, HasDefault::YES, MayHaveConfig::YES, ""); - this->DeclareOption(grp0, "watch", "", "Watch current file and automatically reload it whenever it is modified on disk", appOptions.Watch, HasDefault::YES, MayHaveConfig::YES ); - this->DeclareOption(grp0, "load-plugins", "", "List of plugins to load separated with a comma", appOptions.Plugins, LocalHasDefaultNo, MayHaveConfig::YES, ""); - this->DeclareOption(grp0, "scan-plugins", "", "Scan standard directories for plugins and display available plugins (result can be incomplete)"); - this->DeclareOption(grp0, "screenshot-filename", "", "Screenshot filename", appOptions.ScreenshotFilename, HasDefault::YES, MayHaveConfig::YES, ""); - - auto grp1 = cxxOptions.add_options("General"); - this->DeclareOption(grp1, "verbose", "", "Set verbose level, providing more information about the loaded data in the console output", appOptions.VerboseLevel, HasDefault::YES, MayHaveConfig::YES, "{debug, info, warning, error, quiet}", HasImplicitValue::YES, "debug"); - this->DeclareOption(grp1, "progress", "", "Show loading progress bar", libf3dOptions["ui.loader_progress"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "animation-progress", "", "Show animation progress bar", libf3dOptions["ui.animation_progress"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "geometry-only", "", "Do not read materials, cameras and lights from file", appOptions.GeometryOnly, HasDefault::YES, MayHaveConfig::YES); - this->DeclareOption(grp1, "group-geometries", "", "When opening multiple files, show them all in the same scene. Force geometry-only. The configuration file for the first file will be loaded.", appOptions.GroupGeometries, HasDefault::YES, MayHaveConfig::NO); - this->DeclareOption(grp1, "up", "", "Up direction", libf3dOptions["scene.up_direction"], HasDefault::YES, MayHaveConfig::YES, "{-X, +X, -Y, +Y, -Z, +Z}"); - this->DeclareOption(grp1, "axis", "x", "Show axes", libf3dOptions["interactor.axis"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "grid", "g", "Show grid", libf3dOptions["render.grid.enable"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "grid-absolute", "", "Position grid at the absolute origin instead of below the model", libf3dOptions["render.grid.absolute"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "grid-unit", "", "Size of grid unit square, set to a non-positive value for automatic computation", libf3dOptions["render.grid.unit"], HasDefault::YES, MayHaveConfig::YES); - this->DeclareOption(grp1, "grid-subdivisions", "", "Number of grid subdivisions", libf3dOptions["render.grid.subdivisions"], HasDefault::YES, MayHaveConfig::YES); - this->DeclareOption(grp1, "grid-color", "", "Color of main grid lines", libf3dOptions["render.grid.color"], HasDefault::YES, MayHaveConfig::YES); - this->DeclareOption(grp1, "edges", "e", "Show cell edges", libf3dOptions["render.show_edges"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "camera-index", "", "Select the camera to use", libf3dOptions["scene.camera.index"], HasDefault::YES, MayHaveConfig::YES, ""); - this->DeclareOption(grp1, "trackball", "k", "Enable trackball interaction", libf3dOptions["interactor.trackball"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "invert-zoom", "", "Invert zoom direction with right mouse click", libf3dOptions["interactor.invert_zoom"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "animation-autoplay", "", "Automatically start animation", libf3dOptions["scene.animation.autoplay"], HasDefault::YES, MayHaveConfig::YES, "", HasImplicitValue::YES, "1"); - this->DeclareOption(grp1, "animation-index", "", "Select animation to show", libf3dOptions["scene.animation.index"], HasDefault::YES, MayHaveConfig::YES, ""); - this->DeclareOption(grp1, "animation-speed-factor", "", "Set animation speed factor", libf3dOptions["scene.animation.speed_factor"], HasDefault::YES, MayHaveConfig::YES, ""); - this->DeclareOption(grp1, "animation-time", "", "Set animation time to load", libf3dOptions["scene.animation.time"], HasDefault::YES, MayHaveConfig::YES, "