Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

F3D: Add support for -D, --define and -R,--reset CLI option to set/reset libf3d options from CLI #2035

Merged
merged 8 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions application/F3DOptionsTools.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ static inline const std::array<CLIGroup, 8> CLIOptions = {{

/**
* True boolean options need to be filtered out in ParseCLIOptions
* Also filter out special options like `define` and `reset`
* This is the easiest, compile time way to do it
*/
constexpr std::array CLIBooleans = {"version", "help", "list-readers", "scan-plugins", "list-rendering-backends"};
constexpr std::array CLIBooleans = {"version", "help", "list-readers", "scan-plugins", "list-rendering-backends", "define", "reset"};

//----------------------------------------------------------------------------
/**
Expand Down Expand Up @@ -410,6 +411,13 @@ F3DOptionsTools::OptionsDict F3DOptionsTools::ParseCLIOptions(
// cxxopts values need to live somewhere until parsing is done
std::vector<std::shared_ptr<cxxopts::Value>> cxxoptsValues;
auto cxxoptsInputPositionals = cxxopts::value<std::vector<std::string>>(positionals);

std::vector<std::string> defines;
auto cxxoptsDefines = cxxopts::value<std::vector<std::string>>(defines);

std::vector<std::string> resets;
auto cxxoptsResets = cxxopts::value<std::vector<std::string>>(resets);

try
{
cxxopts::Options cxxOptions(execName, F3D::AppTitle);
Expand All @@ -423,6 +431,8 @@ F3DOptionsTools::OptionsDict F3DOptionsTools::ParseCLIOptions(
if (std::string(optionGroup.GroupName) == "Applicative")
{
group("input", "Input files", cxxoptsInputPositionals, "<files>");
group("D,define", "Define libf3d options", cxxoptsDefines, "libf3d.option=value");
group("R,reset", "Reset libf3d options", cxxoptsResets, "libf3d.option");
}

// Add each option to cxxopts
Expand Down Expand Up @@ -560,9 +570,28 @@ F3DOptionsTools::OptionsDict F3DOptionsTools::ParseCLIOptions(
// Discard boolean option like `--version` or `--help`
if (std::find(::CLIBooleans.begin(), ::CLIBooleans.end(), res.key()) == ::CLIBooleans.end())
{
cliOptionsDict.emplace(res.key(), res.value());
cliOptionsDict[res.key()] = res.value();
}
}

// Handle defines and add them as proper options
for (const std::string& define : defines)
{
std::string::size_type sepIdx = define.find_first_of('=');
if (sepIdx == std::string::npos)
{
f3d::log::warn("Could not parse a define '", define, "'");
continue;
}
cliOptionsDict[define.substr(0, sepIdx)] = define.substr(sepIdx + 1);
}

// Handles reset using the dedicated syntax
for (const std::string& reset : resets)
{
cliOptionsDict["reset-" + reset] = "";
}

return cliOptionsDict;
}
catch (const cxxopts::exceptions::exception& ex)
Expand Down
119 changes: 85 additions & 34 deletions application/F3DStarter.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -474,45 +474,66 @@ class F3DStarter::F3DInternals
appOptions[key] = value;
if (logOptions)
{
loggingMap.emplace(key, std::tuple(key, source, pattern, value));
loggingMap[key] = std::tuple(key, source, pattern, value);
}
continue;
}

std::string libf3dOptionName = key;

// Convert key into a libf3d option name if possible
auto libf3dIter = F3DOptionsTools::LibOptionsNames.find(key);
std::string libf3dOptionName = key;
std::string keyForLog = key;
auto libf3dIter = F3DOptionsTools::LibOptionsNames.find(libf3dOptionName);
if (libf3dIter != F3DOptionsTools::LibOptionsNames.end())
{
libf3dOptionName = std::string(libf3dIter->second);
}

std::string libf3dOptionValue = value;
bool reset = false;

// Handle options reset
// XXX: Use starts_with once C++20 is supported
if (libf3dOptionName.rfind("reset-", 0) == 0)
{
reset = true;
libf3dOptionName = libf3dOptionName.substr(6);
keyForLog = libf3dOptionName;
libf3dOptionValue = "reset";
}

try
{
// Assume this is a libf3d option and set the value
libOptions.setAsString(libf3dOptionName, value);
// Assume this is a libf3d option and set/reset the value
if (reset)
{
libOptions.reset(libf3dOptionName);
}
else
{
libOptions.setAsString(libf3dOptionName, libf3dOptionValue);
}

// Log the option if needed
if (logOptions)
{
loggingMap.emplace(libf3dOptionName, std::tuple(key, source, pattern, value));
loggingMap[libf3dOptionName] =
std::tuple(keyForLog, source, pattern, libf3dOptionValue);
}
}
catch (const f3d::options::parsing_exception& ex)
{
std::string origin =
source.empty() ? pattern : source.string() + ":`" + pattern + "`";
f3d::log::warn("Could not set '", key, "' to '", value, "' from ", origin,
" because: ", ex.what());
f3d::log::warn("Could not set '", keyForLog, "' to '", libf3dOptionValue, "' from ",
origin, " because: ", ex.what());
}
catch (const f3d::options::inexistent_exception&)
{
std::string origin =
source.empty() ? pattern : source.string() + ":`" + pattern + "`";
auto [closestName, dist] =
F3DOptionsTools::GetClosestOption(libf3dOptionName, true);
f3d::log::warn("'", key, "' option from ", origin,
f3d::log::warn("'", keyForLog, "' option from ", origin,
" does not exists , did you mean '", closestName, "'?");
}
}
Expand All @@ -539,7 +560,7 @@ class F3DStarter::F3DInternals
this->UpdateInterdependentOptions();
}

void UpdateTypedAppOptions(const std::map<std::string, std::string>& appOptions)
void UpdateTypedAppOptions(const F3DOptionsTools::OptionsDict& appOptions)
{
// Update typed app options from app options
this->AppOptions.Output = f3d::options::parse<std::string>(appOptions.at("output"));
Expand All @@ -548,14 +569,19 @@ class F3DStarter::F3DInternals
this->AppOptions.NoRender = f3d::options::parse<bool>(appOptions.at("no-render"));
this->AppOptions.RenderingBackend =
f3d::options::parse<std::string>(appOptions.at("rendering-backend"));
if (!appOptions.at("max-size").empty())

std::string maxSize = appOptions.at("max-size");
if (!maxSize.empty())
{
this->AppOptions.MaxSize = f3d::options::parse<double>(appOptions.at("max-size"));
this->AppOptions.MaxSize = f3d::options::parse<double>(maxSize);
}
if (!appOptions.at("animation-time").empty())

std::string animationTime = appOptions.at("animation-time");
if (!animationTime.empty())
{
this->AppOptions.AnimationTime = f3d::options::parse<double>(appOptions.at("animation-time"));
this->AppOptions.AnimationTime = f3d::options::parse<double>(animationTime);
}

this->AppOptions.FrameRate = f3d::options::parse<double>(appOptions.at("frame-rate"));
this->AppOptions.Watch = f3d::options::parse<bool>(appOptions.at("watch"));
this->AppOptions.Plugins = { f3d::options::parse<std::vector<std::string>>(
Expand All @@ -572,9 +598,10 @@ class F3DStarter::F3DInternals
f3d::options::parse<std::string>(appOptions.at("colormap-file"));

std::optional<f3d::direction_t> camDir;
if (!appOptions.at("camera-direction").empty())
std::string camDirStr = appOptions.at("camera-direction");
if (!camDirStr.empty())
{
camDir = f3d::options::parse<f3d::direction_t>(appOptions.at("camera-direction"));
camDir = f3d::options::parse<f3d::direction_t>(camDirStr);
}

this->AppOptions.CamConf = { f3d::options::parse<std::vector<double>>(
Expand Down Expand Up @@ -821,26 +848,37 @@ int F3DStarter::Start(int argc, char** argv)
// XXX: the local variable are initialized manually for simplicity
// but this duplicate the initialization value as it is present in
// F3DOptionTools::DefaultAppOptions too
F3DOptionsTools::OptionsDict::const_iterator iter;

bool noConfig = false;
if (cliOptionsDict.find("no-config") != cliOptionsDict.end())
iter = cliOptionsDict.find("no-config");
if (iter != cliOptionsDict.end())
{
noConfig = f3d::options::parse<bool>(cliOptionsDict["no-config"]);
noConfig = f3d::options::parse<bool>(iter->second);
}

std::string config;
if (!noConfig && cliOptionsDict.find("config") != cliOptionsDict.end())
if (!noConfig)
{
config = f3d::options::parse<std::string>(cliOptionsDict["config"]);
iter = cliOptionsDict.find("config");
if (iter != cliOptionsDict.end())
{
config = f3d::options::parse<std::string>(iter->second);
}
}

bool renderToStdout = false;
if (cliOptionsDict.find("output") != cliOptionsDict.end())
iter = cliOptionsDict.find("output");
if (iter != cliOptionsDict.end())
{
renderToStdout = f3d::options::parse<std::string>(cliOptionsDict["output"]) == "-";
renderToStdout = f3d::options::parse<std::string>(iter->second) == "-";
}

this->Internals->AppOptions.VerboseLevel = "info";
if (cliOptionsDict.find("verbose") != cliOptionsDict.end())
iter = cliOptionsDict.find("verbose");
if (iter != cliOptionsDict.end())
{
this->Internals->AppOptions.VerboseLevel =
f3d::options::parse<std::string>(cliOptionsDict["verbose"]);
this->Internals->AppOptions.VerboseLevel = f3d::options::parse<std::string>(iter->second);
}

// Set verbosity level early from command line
Expand Down Expand Up @@ -1210,20 +1248,32 @@ void F3DStarter::LoadFileGroup(
f3d::log::debug("========== Loading 3D files ==========");

// Recover current options from the engine
const f3d::options& dynamicOptions = this->Internals->Engine->getOptions();
f3d::options& dynamicOptions = this->Internals->Engine->getOptions();

// reset forced options to avoid logging noise
dynamicOptions.ui.dropzone = false;
dynamicOptions.ui.filename_info = "";

// Detect interactively changed options and store them into the dynamic options dict
// options names are shared between options instance
F3DOptionsTools::OptionsDict dynamicOptionsDict;
std::vector<std::string> optionNames = dynamicOptions.getNames();
std::vector<std::string> optionNames = dynamicOptions.getAllNames();
for (const auto& name : optionNames)
{
if (!dynamicOptions.isSame(this->Internals->LibOptions, name))
{
// XXX Currently an assert is enough but it should be a proper try/catch once
// we add a mechanism to unset an option
assert(dynamicOptions.hasValue(name));
dynamicOptionsDict[name] = dynamicOptions.getAsString(name);
if (!dynamicOptions.hasValue(name))
{
// If a dynamic option has been changed and does not have value, it means it was reset using
// the command line reset it using the dedicated syntax
dynamicOptionsDict["reset-" + name] = "";
}
else
{
// No need for a try/catch block here, this call cannot trigger
// an exception with current code path
dynamicOptionsDict[name] = dynamicOptions.getAsString(name);
}
}
}

Expand Down Expand Up @@ -1399,8 +1449,9 @@ void F3DStarter::LoadFileGroup(
}
}

// XXX: We can force dropzone and filename_info because they cannot be set
// manually by the user for now
// XXX: Here we potentially override user set libf3d options
// but there is no way to detect if an option has been set
// by the user or not.
f3d::options& options = this->Internals->Engine->getOptions();
options.ui.dropzone = this->Internals->LoadedFiles.empty();
options.ui.filename_info = filenameInfo;
Expand Down
24 changes: 15 additions & 9 deletions application/testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ f3d_test(NAME TestFilenameCommasSpaces DATA "tetrahedron, with commas & spaces.s
f3d_test(NAME TestFont DATA suzanne.ply ARGS -n --font-file=${F3D_SOURCE_DIR}/testing/data/Crosterian.ttf UI)
f3d_test(NAME TestFontScale2 DATA suzanne.ply ARGS -n --font-scale=2 UI)
f3d_test(NAME TestFontScale3 DATA suzanne.ply ARGS -n --font-scale=3 UI)
f3d_test(NAME TestDefines DATA dragon.vtu ARGS -Dscene.up_direction=+Z --define=model.point_sprites.enable=on)
f3d_test(NAME TestDefinesInvalid DATA dragon.vtu ARGS -Dscene.up_direction+Z REGEXP "Could not parse a define" NO_BASELINE)
f3d_test(NAME TestDefinesInexistent DATA dragon.vtu ARGS -Dscene.up_director=+Z REGEXP "option from CLI options does not exists" NO_BASELINE)
f3d_test(NAME TestConfigReset DATA suzanne.stl ARGS -Rrender.grid.enable --reset=ui.axis CONFIG ${F3D_SOURCE_DIR}/testing/configs/complex.json)
f3d_test(NAME TestConfigResetInexistent DATA suzanne.stl ARGS -Rrender.glid.enable REGEXP "option from CLI options does not exists" NO_BASELINE)
f3d_test(NAME TestAnimationIndex DATA InterpolationTest.glb ARGS --animation-index=7 --animation-time=0.5 --animation-progress)
f3d_test(NAME TestMultiFileAnimationIndex DATA InterpolationTest.glb BoxAnimated.gltf ARGS --animation-index=9 --animation-time=0.85 --animation-progress --multi-file-mode=all)
f3d_test(NAME TestMultiFileAnimationNoAnimationSupport DATA f3d.glb world.obj ARGS --multi-file-mode=all --animation-time=2 --animation-progress)
Expand Down Expand Up @@ -447,7 +452,7 @@ f3d_test(NAME TestInteractionCheatsheetAnimationName DATA InterpolationTest.glb
f3d_test(NAME TestInteractionCheatsheetConfigFile DATA dragon.vtu CONFIG ${F3D_SOURCE_DIR}/testing/configs/bindings.json INTERACTION UI) #H;ScrollDown

if(NOT F3D_MODULE_RAYTRACING)
f3d_test(NAME TestInteractionCheatsheetCentered DATA cow.vtp ARGS --resolution=500,1500 INTERACTION UI LONG_TIMEOUT) #H
f3d_test(NAME TestInteractionCheatsheetCentered DATA cow.vtp RESOLUTION 500,1500 INTERACTION UI LONG_TIMEOUT) #H
endif()

f3d_test(NAME TestCameraPersp DATA Cameras.gltf ARGS --camera-index=0)
Expand Down Expand Up @@ -920,16 +925,17 @@ f3d_test(NAME TestInteractionRecord DATA cow.vtp ARGS --interaction-test-record=
f3d_test(NAME TestInteractionPlay DATA cow.vtp ARGS --interaction-test-play=${CMAKE_BINARY_DIR}/Testing/Temporary/TestInteractionRecord.log DEPENDS TestInteractionRecord NO_BASELINE)

# Command Script Test
f3d_test(NAME TestCommandScriptBasic DATA dragon.vtu SCRIPT TestCommandScriptBasic.txt --reference=${F3D_SOURCE_DIR}/testing/baselines/TestCommandScriptBasic.png)
f3d_test(NAME TestCommandScriptInvalidCommand DATA dragon.vtu SCRIPT TestCommandScriptInvalid.txt REGEXP "Command: \"INVALID_COMMAND_1\" is not recognized, ignoring" NO_BASELINE)
f3d_test(NAME TestCommandScriptBasic DATA dragon.vtu SCRIPT TestCommandScriptBasic.txt) # roll_camera 90;toggle ui.scalar_bar;print_scene_info;increase_light_intensity
f3d_test(NAME TestCommandScriptInvalidCommand DATA dragon.vtu SCRIPT TestCommandScriptInvalid.txt REGEXP "Command: \"INVALID_COMMAND_1\" is not recognized, ignoring" NO_BASELINE) # INVALID_COMMAND_1
f3d_test(NAME TestCommandScriptMissingFile DATA dragon.vtu SCRIPT TestCommandScriptMissingFile.txt REGEXP "Unable to open command script file" NO_BASELINE)
f3d_test(NAME TestCommandScriptPrintScene DATA dragon.vtu SCRIPT TestCommandScriptPrintScene.txt REGEXP "Camera position: 2.23745,3.83305,507.598" NO_BASELINE) # print_scene_info
f3d_test(NAME TestCommandScriptPrintColoring DATA dragon.vtu SCRIPT TestCommandScriptPrintColoring.txt REGEXP "Not coloring" NO_BASELINE) # print_coloring_info
f3d_test(NAME TestCommandScriptPrintMesh DATA dragon.vtu SCRIPT TestCommandScriptPrintMesh.txt REGEXP "Number of points: 13268" NO_BASELINE) # print_mesh_info
f3d_test(NAME TestCommandScriptPrintOptions DATA dragon.vtu SCRIPT TestCommandScriptPrintOptions.txt REGEXP "interactor.invert_zoom: false" NO_BASELINE) # print_options_info
f3d_test(NAME TestCommandScriptAlias DATA dragon.vtu SCRIPT TestCommandScriptAlias.txt --reference=${F3D_SOURCE_DIR}/testing/baselines/TestCommandScriptAlias.png) # alias myrotate roll_camera 90;myrotate
f3d_test(NAME TestParseOptionalBoolExtraArg DATA dragon.vtu SCRIPT TestParseOptionalBoolExtraArg.txt REGEXP "Command: load_previous_file_group takes at most 1 argument, got 2 arguments instead." NO_BASELINE)
f3d_test(NAME TestRemoveFileGroups DATA dragon.vtu SCRIPT TestRemoveFileGroups.txt NO_DATA_FORCE_RENDER)
f3d_test(NAME TestCommandScriptReset DATA dragon.vtu suzanne.stl ARGS --edges SCRIPT TestCommandScriptReset.txt) # reset render.show_edges; load_next_file_group;
f3d_test(NAME TestParseOptionalBoolExtraArg DATA dragon.vtu SCRIPT TestParseOptionalBoolExtraArg.txt REGEXP "Command: load_previous_file_group takes at most 1 argument, got 2 arguments instead." NO_BASELINE) # load_previous_file_group true extra
f3d_test(NAME TestRemoveFileGroups DATA dragon.vtu SCRIPT TestRemoveFileGroups.txt NO_DATA_FORCE_RENDER) # remove_file_groups

# Window position test
f3d_test(NAME TestPosition DATA dragon.vtu ARGS --position=100,100 NO_BASELINE)
Expand Down Expand Up @@ -1202,10 +1208,10 @@ if(NOT WIN32)
f3d_test(NAME TestColorMapTooLong DATA dragon.vtu ARGS --colormap-file=${_f3d_test_invalid_folder}/file.ext --scalar-coloring REGEXP "File name too long" NO_BASELINE)
f3d_test(NAME TestScreenshotTooLong DATA suzanne.ply ARGS --screenshot-filename=${_f3d_test_invalid_folder}/file.ext --interaction-test-play=${F3D_SOURCE_DIR}/testing/recordings/TestScreenshot.log REGEXP "File name too long" NO_BASELINE)
f3d_test(NAME TestInputTooLong ARGS --input=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE)
f3d_test(NAME TestReferenceTooLong DATA suzanne.ply ARGS --output=file.png --reference=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE)
f3d_test(NAME TestOutputTooLong DATA suzanne.ply ARGS --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE)
f3d_test(NAME TestOutputWithReferenceTooLong DATA suzanne.ply ARGS --reference=file.png --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE)
f3d_test(NAME TestOutputWithExistingReferenceTooLong DATA suzanne.ply ARGS --reference=${F3D_SOURCE_DIR}/testing/data/world.png --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE)
f3d_test(NAME TestReferenceTooLong DATA suzanne.ply ARGS --output=file.png --reference=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
f3d_test(NAME TestOutputTooLong DATA suzanne.ply ARGS --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
f3d_test(NAME TestOutputWithReferenceTooLong DATA suzanne.ply ARGS --reference=file.png --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
f3d_test(NAME TestOutputWithExistingReferenceTooLong DATA suzanne.ply ARGS --reference=${F3D_SOURCE_DIR}/testing/data/world.png --output=${_f3d_test_invalid_folder}/file.ext REGEXP "File name too long" NO_BASELINE NO_OUTPUT)
endif()

# Test failure without a reference, please do not create a TestNoRef.png file
Expand Down
2 changes: 1 addition & 1 deletion doc/user/CONFIGURATION_FILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The third block specifies raytracing usage for .gltf and .glb files.
The last block specifies that volume rendering should be used with .mhd files.

The following options <b> cannot </b> be set via config file:
`help`, `version`, `list-readers`, `list-rendering-backends`, `scan-plugins`, `config`, `no-config` and `input`.
`help`, `version`, `list-readers`, `list-rendering-backends`, `scan-plugins`, `config`, `no-config`, `define`, `reset` and `input`.

The following options <b>are only taken on the first load</b>:
`no-render`, `output`, `position`, `resolution`, `frame-rate` and all testing options.
Expand Down
Loading