Core: Show all path template errors in the UI

The path templates feature implemented in #134860 highlights path
properties that have syntax errors in red and provides a list of those
errors in the path's tool tip. However, syntax errors are not the only
kind of error.

For example, if given the render output path `//render/{blend_nam}`,
with a typo in the variable `blend_name`, this error is not indicated
until actually trying to render. This is because the path is
*syntactically* correct, and the error is due to `blend_nam` not being
an available variable.

This PR resolves this: paths with path template support now indicate all
errors in the UI, not just syntax errors, with red highlights and a list
of errors in the tool tip.

The primary mechanisms for this are:
- A new function `BKE_build_template_variables_for_prop()`, which takes
an RNA pointer and produces an appropriate template variable map for the
given property, which can then be used to fully validate a path
property's value in the UI code.
- A new enum `PropertyPathTemplateType` has been added to `PropertyRNA`,
which determines what variables should be made available to a property
and how they should be build. This is used by
`BKE_build_template_variables_for_prop()`.

Additionally, the following changes have been made to help ensure that
`BKE_path_apply_template()` and `BKE_path_validate_template()` produce
identical errors:
- Both functions now call into a third static function `eval_template()`
  that does the actual work, optionally modifying the input path or not.
- Previously only `BKE_path_apply_template()` had unit tests, but now
  the unit tests have been reorganized to simultaneously test both
  `BKE_path_apply_template()` and `BKE_path_validate_template()`.

Pull Request: https://projects.blender.org/blender/blender/pulls/138679
This commit is contained in:
Nathan Vegdahl
2025-06-02 11:40:05 +02:00
committed by Nathan Vegdahl
parent 0a1ff2b2ff
commit 5ff12bdc98
20 changed files with 491 additions and 285 deletions

View File

@@ -22,6 +22,10 @@
#include "DNA_scene_types.h"
struct bContext;
struct PointerRNA;
struct PropertyRNA;
namespace blender::bke::path_templates {
/**
@@ -129,7 +133,22 @@ bool operator==(const Error &left, const Error &right);
} // namespace blender::bke::path_templates
/**
* Build a template variable map based on available information.
* Build a template variable map for the passed RNA property.
*
* \param C: the context to use for building some variables. This is needed in
* some cases when the property and its owner do not provide the data needed for
* a variable. This parameter can be null, but the variables it's needed for
* will then be absent in the returned variable map.
*
* \return On success, returns the template variables for the property. If no
* property is provided or if the property doesn't support path templates,
* returns nullopt.
*/
std::optional<blender::bke::path_templates::VariableMap> BKE_build_template_variables_for_prop(
const bContext *C, PointerRNA *ptr, PropertyRNA *prop);
/**
* Build a template variable map for render output paths.
*
* All parameters are allowed to be null, in which case the variables derived
* from those parameters will simply not be included.
@@ -154,21 +173,34 @@ bool operator==(const Error &left, const Error &right);
*
* \see BLI_path_abs()
*/
blender::bke::path_templates::VariableMap BKE_build_template_variables(
blender::bke::path_templates::VariableMap BKE_build_template_variables_for_render_path(
const char *blend_file_path, const RenderData *render_data);
/**
* Validate the template syntax in the given path.
* Check if a path contains any templating syntax at all.
*
* This does *not* validate whether any variables referenced in the path exist
* or not, nor whether the format specification in a variable expression is
* appropriate for its type. This only validates that the template syntax itself
* is valid.
* This is primarily intended to be used as a pre-check in performance-sensitive
* code to skip path template processing when it's not needed.
*
* \return An empty vector if valid, or a vector of the parse errors if invalid.
* \return False if the path contains no templating syntax (no template
* processing is needed). True if the path does contain templating syntax
* (template processing *is* needed).
*/
blender::Vector<blender::bke::path_templates::Error> BKE_validate_template_syntax(
blender::StringRef path);
bool BKE_path_contains_template_syntax(blender::StringRef path);
/**
* Validate the templating in the given path.
*
* This produces identical errors as `BKE_path_apply_template()`, but
* without modifying the path on success.
*
* \return An empty vector if the templating in the path is valid, or a vector
* of the errors if invalid.
*
* \see BKE_path_apply_template()
*/
blender::Vector<blender::bke::path_templates::Error> BKE_path_validate_template(
blender::StringRef path, const blender::bke::path_templates::VariableMap &template_variables);
/**
* Perform variable substitution and escaping on the given path.

View File

@@ -6,9 +6,19 @@
#include "BLT_translation.hh"
#include "BLI_span.hh"
#include "BKE_context.hh"
#include "BKE_main.hh"
#include "BKE_path_templates.hh"
#include "BKE_scene.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "DNA_ID_enums.h"
#include "DNA_node_types.h"
namespace blender::bke::path_templates {
bool VariableMap::contains(blender::StringRef name) const
@@ -102,8 +112,69 @@ bool operator==(const Error &left, const Error &right)
using namespace blender::bke::path_templates;
VariableMap BKE_build_template_variables(const char *blend_file_path,
const RenderData *render_data)
std::optional<VariableMap> BKE_build_template_variables_for_prop(const bContext *C,
PointerRNA *ptr,
PropertyRNA *prop)
{
/*
* This function should be maintained such that it always produces variables
* consistent with the variables produced elsewhere in the code base for the
* same property. For example, render paths are processed in the rendering
* code and variables are built for that purpose there, and this function
* should produce variables consistent with that for those render path
* properties here.
*
* The recommended strategy when adding support for additional path templating
* use cases (that don't already have an appropriate
* `PropertyPathTemplateType` item) is to:
*
* 1. Create a separate function to build variables for that use case (see
* e.g. `BKE_build_template_variables_for_render_path()`).
* 2. Call that function from here in the switch statement below.
* 3. Also call that function from the other parts of the code base that need
* it.
*/
/* No property passed, or it doesn't support path templates. */
if (ptr == nullptr || prop == nullptr ||
(RNA_property_flag(prop) & PROP_PATH_SUPPORTS_TEMPLATES) == 0)
{
return std::nullopt;
}
switch (RNA_property_path_template_type(prop)) {
case PROP_VARIABLES_NONE: {
BLI_assert_msg(
false,
"Should never have `PROP_VARIABLES_NONE` for a path that supports path templates.");
return VariableMap();
}
/* Scene render output path, the compositor's File Output node's paths, etc. */
case PROP_VARIABLES_RENDER_OUTPUT: {
const RenderData *render_data;
if (GS(ptr->owner_id->name) == ID_SCE) {
render_data = &reinterpret_cast<const Scene *>(ptr->owner_id)->r;
}
else {
const Scene *scene = CTX_data_scene(C);
render_data = scene ? &scene->r : nullptr;
}
return BKE_build_template_variables_for_render_path(BKE_main_blendfile_path_from_global(),
render_data);
}
}
/* All paths that support path templates should be handled above, and any that
* aren't should already be rejected by the test at the top of the function. */
BLI_assert_unreachable();
return std::nullopt;
}
VariableMap BKE_build_template_variables_for_render_path(const char *blend_file_path,
const RenderData *render_data)
{
VariableMap variables;
@@ -620,25 +691,41 @@ static std::optional<Error> token_to_syntax_error(const Token &token)
return std::nullopt;
}
blender::Vector<Error> BKE_validate_template_syntax(blender::StringRef path)
bool BKE_path_contains_template_syntax(blender::StringRef path)
{
const blender::Vector<Token> tokens = parse_template(path);
blender::Vector<Error> errors;
for (const Token &token : tokens) {
if (std::optional<Error> error = token_to_syntax_error(token)) {
errors.append(*error);
}
}
return errors;
return path.find_first_of("{}") != std::string_view::npos;
}
blender::Vector<Error> BKE_path_apply_template(char *path,
int path_max_length,
const VariableMap &template_variables)
/**
* Evaluates the path template in `in_path` and writes the result to `out_path`
* if provided.
*
* \param out_path: buffer to write the evaluated path to. May be null, in which
* case writing is skipped, and this function just acts to validate the
* templating in the path.
*
* \param out_path_max_length: The maximum length that template expansion is
* allowed to make the template-expanded path (in bytes), including the null
* terminator. In general, this should be the size of the underlying allocation
* of `out_path`.
*
* \param template_variables: map of variables and their values to use during
* template substitution.
*
* \return An empty vector on success, or a vector of templating errors on
* failure. Note that even if there are errors, `out_path` may get modified, and
* it should be treated as bogus data in that case.
*/
static blender::Vector<Error> eval_template(char *out_path,
const int out_path_max_length,
blender::StringRef in_path,
const VariableMap &template_variables)
{
const blender::Vector<Token> tokens = parse_template(path);
if (out_path) {
in_path.copy_utf8_truncated(out_path, out_path_max_length);
}
const blender::Vector<Token> tokens = parse_template(in_path);
if (tokens.is_empty()) {
/* No tokens found, so nothing to do. */
@@ -648,15 +735,6 @@ blender::Vector<Error> BKE_path_apply_template(char *path,
/* Accumulates errors as we process the tokens. */
blender::Vector<Error> errors;
/* We work on a copy of the path, for two reasons:
*
* 1. So that if there are errors we can leave the original unmodified.
* 2. So that the contents of the StringRefs in the Token structs don't change
* out from under us while we're generating the modified path.*/
blender::Vector<char> path_buffer(path_max_length);
char *path_modified = path_buffer.data();
strcpy(path_modified, path);
/* Tracks the change in string length due to the modifications as we go. We
* need this to properly map the token byte ranges to the being-modified
* string. */
@@ -701,7 +779,7 @@ blender::Vector<Error> BKE_path_apply_template(char *path,
errors.append({ErrorType::FORMAT_SPECIFIER, token.byte_range});
continue;
}
strcpy(replacement_string, string_value->c_str());
BLI_strncpy(replacement_string, string_value->c_str(), sizeof(replacement_string));
break;
}
@@ -726,24 +804,47 @@ blender::Vector<Error> BKE_path_apply_template(char *path,
}
}
/* We're off the end of the available space. */
if (token.byte_range.start() + length_diff >= path_max_length) {
break;
/* Perform the actual substitution with the expanded value. */
if (out_path) {
/* We're off the end of the available space. */
if (token.byte_range.start() + length_diff >= out_path_max_length) {
break;
}
BLI_string_replace_range(out_path,
out_path_max_length,
token.byte_range.start() + length_diff,
token.byte_range.one_after_last() + length_diff,
replacement_string);
length_diff -= token.byte_range.size();
length_diff += strlen(replacement_string);
}
BLI_string_replace_range(path_modified,
path_max_length,
token.byte_range.start() + length_diff,
token.byte_range.one_after_last() + length_diff,
replacement_string);
length_diff -= token.byte_range.size();
length_diff += strlen(replacement_string);
}
return errors;
}
blender::Vector<Error> BKE_path_validate_template(
blender::StringRef path, const blender::bke::path_templates::VariableMap &template_variables)
{
return eval_template(nullptr, 0, path, template_variables);
}
blender::Vector<Error> BKE_path_apply_template(char *path,
int path_max_length,
const VariableMap &template_variables)
{
BLI_assert(path != nullptr);
blender::Vector<char> path_buffer(path_max_length);
const blender::Vector<Error> errors = eval_template(
path_buffer.data(), path_buffer.size(), path, template_variables);
if (errors.is_empty()) {
/* No errors, so copy the modified path back to the original. */
strcpy(path, path_modified);
BLI_strncpy(path, path_buffer.data(), path_max_length);
}
return errors;
}

View File

@@ -12,7 +12,7 @@ namespace blender::bke::tests {
using namespace blender::bke::path_templates;
[[maybe_unused]] static void debug_print_error(const Error &error)
static std::string error_to_string(const Error &error)
{
const char *type;
switch (error.type) {
@@ -29,17 +29,35 @@ using namespace blender::bke::path_templates;
type = "UNKNOWN_VARIABLE";
break;
}
fmt::print("({}, ({}, {}))", type, error.byte_range.start(), error.byte_range.size());
std::string s;
fmt::format_to(std::back_inserter(s),
"({}, ({}, {}))",
type,
error.byte_range.start(),
error.byte_range.size());
return s;
}
[[maybe_unused]] static void debug_print_errors(Span<Error> errors)
static std::string errors_to_string(Span<Error> errors)
{
fmt::print("[");
std::string s;
fmt::format_to(std::back_inserter(s), "[");
bool is_first = true;
for (const Error &error : errors) {
debug_print_error(error);
fmt::print(", ");
if (is_first) {
is_first = false;
}
else {
fmt::format_to(std::back_inserter(s), ", ");
}
fmt::format_to(std::back_inserter(s), "{}", error_to_string(error));
}
fmt::print("]\n");
fmt::format_to(std::back_inserter(s), "]");
return s;
}
TEST(path_templates, VariableMap)
@@ -107,7 +125,13 @@ TEST(path_templates, VariableMap)
EXPECT_FALSE(map.remove("what"));
}
TEST(path_templates, path_apply_variables)
struct PathTemplateTestCase {
char path_in[FILE_MAX];
char path_result[FILE_MAX];
Vector<Error> expected_errors;
};
TEST(path_templates, validate_and_apply_template)
{
VariableMap variables;
{
@@ -126,218 +150,205 @@ TEST(path_templates, path_apply_variables)
variables.add_float("tiny", 0.000000000000000000000000000000002);
}
/* Simple case, testing all variables. */
{
char path[FILE_MAX] =
"{hi}_{bye}_{the_answer}_{prime}_{i_negative}_{pi}_{e}_{ntsc}_{two}_{f_negative}_{huge}_{"
"tiny}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<PathTemplateTestCase> test_cases = {
/* Simple case, testing all variables. */
{
"{hi}_{bye}_{the_answer}_{prime}_{i_negative}_{pi}_{e}_{ntsc}_{two}_{f_negative}_{huge}_"
"{tiny}",
"hello_goodbye_42_7_-7_3.141592653589793_2.718281828459045_29.970029970029973_2.0_-3."
"141592653589793_2e+32_2e-33",
{},
},
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(blender::StringRef(path),
"hello_goodbye_42_7_-7_3.141592653589793_2.718281828459045_29.970029970029973_2.0_-"
"3.141592653589793_2e+32_2e-33");
}
/* Integer formatting. */
{
"{the_answer:#}_{the_answer:##}_{the_answer:####}_{i_negative:####}",
"42_42_0042_-007",
{},
},
/* Integer formatting. */
{
char path[FILE_MAX] = "{the_answer:#}_{the_answer:##}_{the_answer:####}_{i_negative:####}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
/* Integer formatting as float. */
{
"{the_answer:.###}_{the_answer:#.##}_{the_answer:###.##}_{i_negative:###.####}",
"42.000_42.00_042.00_-07.0000",
{},
},
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(blender::StringRef(path), "42_42_0042_-007");
}
/* Float formatting: specify fractional digits only. */
{
"{pi:.####}_{e:.###}_{ntsc:.########}_{two:.##}_{f_negative:.##}_{huge:.##}_{tiny:.##}",
"3.1416_2.718_29.97002997_2.00_-3.14_200000000000000010732324408786944.00_0.00",
{},
},
/* Integer formatting as float. */
{
char path[FILE_MAX] =
"{the_answer:.###}_{the_answer:#.##}_{the_answer:###.##}_{i_negative:###.####}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
/* Float formatting: specify both integer and fractional digits. */
{
"{pi:##.####}_{e:####.###}_{ntsc:#.########}_{two:###.##}_{f_negative:###.##}_{huge:###."
"##}_{tiny:###.##}",
"03.1416_0002.718_29.97002997_002.00_-03.14_200000000000000010732324408786944.00_000.00",
{},
},
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(blender::StringRef(path), "42.000_42.00_042.00_-07.0000");
}
/* Float formatting: format as integer. */
{
"{pi:##}_{e:####}_{ntsc:#}_{two:###}",
"03_0003_30_002",
{},
},
/* Float formatting: specify fractional digits only. */
{
char path[FILE_MAX] =
"{pi:.####}_{e:.###}_{ntsc:.########}_{two:.##}_{f_negative:.##}_{huge:.##}_{tiny:.##}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
/* Escaping. "{{" and "}}" are the escape codes for literal "{" and "}". */
{
"{hi}_{{hi}}_{{{bye}}}_{bye}",
"hello_{hi}_{goodbye}_goodbye",
{},
},
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(blender::StringRef(path),
"3.1416_2.718_29.97002997_2.00_-3.14_200000000000000010732324408786944.00_0.00");
}
/* Error: string variables do not support format specifiers. */
{
"{hi:##}_{bye:#}",
"{hi:##}_{bye:#}",
{
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 7)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(8, 7)},
},
},
/* Float formatting: specify both integer and fractional digits. */
{
char path[FILE_MAX] =
"{pi:##.####}_{e:####.###}_{ntsc:#.########}_{two:###.##}_{f_negative:###.##}_{huge:###.##"
"}_{tiny:###.##}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
/* Error: float formatting: specifying integer digits only (but still wanting
* it printed as a float) is currently not supported. */
{
"{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}",
"{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}",
{
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 8)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(9, 9)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(19, 9)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(29, 10)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(40, 17)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(58, 11)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(70, 11)},
},
},
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(
blender::StringRef(path),
"03.1416_0002.718_29.97002997_002.00_-03.14_200000000000000010732324408786944.00_000.00");
}
/* Error: missing variable. */
{
"{hi}_{missing}_{bye}",
"{hi}_{missing}_{bye}",
{
{ErrorType::UNKNOWN_VARIABLE, IndexRange(5, 9)},
},
},
/* Float formatting: format as integer. */
{
char path[FILE_MAX] = "{pi:##}_{e:####}_{ntsc:#}_{two:###}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
/* Error: incomplete variable expression. */
{
"foo{hi",
"foo{hi",
{
{ErrorType::VARIABLE_SYNTAX, IndexRange(3, 3)},
},
},
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(blender::StringRef(path), "03_0003_30_002");
}
/* Error: incomplete variable expression after complete one. */
{
"foo{bye}{hi",
"foo{bye}{hi",
{
{ErrorType::VARIABLE_SYNTAX, IndexRange(8, 3)},
},
},
/* Escaping. "{{" and "}}" are the escape codes for literal "{" and "}". */
{
char path[FILE_MAX] = "{hi}_{{hi}}_{{{bye}}}_{bye}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
/* Error: invalid format specifiers. */
{
"{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}",
"{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}",
{
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 8)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(9, 9)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(19, 13)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(33, 11)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(45, 12)},
},
},
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(blender::StringRef(path), "hello_{hi}_{goodbye}_goodbye");
}
/* Error: unclosed variable. */
{
"{hi_{hi}_{bye}",
"{hi_{hi}_{bye}",
{
{ErrorType::VARIABLE_SYNTAX, IndexRange(0, 4)},
},
},
/* Error: string variables do not support format specifiers. */
{
char path[FILE_MAX] = "{hi:##}_{bye:#}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<Error> expected_errors = {
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 7)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(8, 7)},
};
/* Error: escaped braces inside variable. */
{
"{hi_{{hi}}_{bye}",
"{hi_{{hi}}_{bye}",
{
{ErrorType::VARIABLE_SYNTAX, IndexRange(0, 4)},
},
},
EXPECT_EQ(errors, expected_errors);
EXPECT_EQ(blender::StringRef(path), "{hi:##}_{bye:#}");
}
/* Test what happens when the path would expand to a string that's longer than
* `FILE_MAX`.
*
* We don't care so much about any kind of "correctness" here, we just want to
* ensure that it still results in a valid null-terminated string that fits in
* `FILE_MAX` bytes.
*
* NOTE: this test will have to be updated if `FILE_MAX` is ever changed. */
{
"___{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}",
/* Error: float formatting: specifying integer digits only (but still wanting
* it printed as a float) is currently not supported. */
{
char path[FILE_MAX] =
"{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<Error> expected_errors = {
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 8)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(9, 9)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(19, 9)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(29, 10)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(40, 17)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(58, 11)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(70, 11)},
};
"___This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
"by",
EXPECT_EQ(errors, expected_errors);
EXPECT_EQ(blender::StringRef(path),
"{pi:##.}_{e:####.}_{ntsc:#.}_{two:###.}_{f_negative:###.}_{huge:###.}_{tiny:###.}");
}
{},
},
};
/* Error: missing variable. */
{
char path[FILE_MAX] = "{hi}_{missing}_{bye}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<Error> expected_errors = {
{ErrorType::UNKNOWN_VARIABLE, IndexRange(5, 9)},
};
for (const PathTemplateTestCase &test_case : test_cases) {
char path[FILE_MAX];
BLI_strncpy(path, test_case.path_in, FILE_MAX);
EXPECT_EQ(errors, expected_errors);
EXPECT_EQ(blender::StringRef(path), "{hi}_{missing}_{bye}");
}
/* Do validation first, which shouldn't modify the path. */
const Vector<Error> validation_errors = BKE_path_validate_template(path, variables);
EXPECT_EQ(validation_errors, test_case.expected_errors)
<< " Template errors: " << errors_to_string(validation_errors) << std::endl
<< " Expected errors: " << errors_to_string(test_case.expected_errors) << std::endl
<< " Note: test_case.path_in = " << test_case.path_in << std::endl;
EXPECT_EQ(blender::StringRef(path), test_case.path_in)
<< " Note: test_case.path_in = " << test_case.path_in << std::endl;
/* Error: incomplete variable expression. */
{
char path[FILE_MAX] = "foo{hi";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<Error> expected_errors = {
{ErrorType::VARIABLE_SYNTAX, IndexRange(3, 3)},
};
EXPECT_EQ(errors, expected_errors);
EXPECT_EQ(blender::StringRef(path), "foo{hi");
}
/* Error: invalid format specifiers. */
{
char path[FILE_MAX] = "{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<Error> expected_errors = {
{ErrorType::FORMAT_SPECIFIER, IndexRange(0, 8)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(9, 9)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(19, 13)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(33, 11)},
{ErrorType::FORMAT_SPECIFIER, IndexRange(45, 12)},
};
EXPECT_EQ(errors, expected_errors);
EXPECT_EQ(blender::StringRef(path),
"{prime:}_{prime:.}_{prime:#.#.#}_{prime:sup}_{prime::sup}_{prime}");
}
/* Error: unclosed variable. */
{
char path[FILE_MAX] = "{hi_{hi}_{bye}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<Error> expected_errors = {
{ErrorType::VARIABLE_SYNTAX, IndexRange(0, 4)},
};
EXPECT_EQ(errors, expected_errors);
EXPECT_EQ(blender::StringRef(path), "{hi_{hi}_{bye}");
}
/* Error: escaped braces inside variable. */
{
char path[FILE_MAX] = "{hi_{{hi}}_{bye}";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
const Vector<Error> expected_errors = {
{ErrorType::VARIABLE_SYNTAX, IndexRange(0, 4)},
};
EXPECT_EQ(errors, expected_errors);
EXPECT_EQ(blender::StringRef(path), "{hi_{{hi}}_{bye}");
}
/* Test what happens when the path would expand to a string that's longer than
* `FILE_MAX`.
*
* We don't care so much about any kind of "correctness" here, we just want to
* ensure that it still results in a valid null-terminated string that fits in
* `FILE_MAX` bytes.
*
* NOTE: this test will have to be updated if `FILE_MAX` is ever changed. */
{
char path[FILE_MAX] =
"___{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}"
"{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{"
"long}{long}{long}{long}{long}{long}{long}{long}{long}{long}{long}";
const char result[FILE_MAX] =
"___This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 "
"bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This string is "
"exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 bytes.This "
"string is exactly 32 bytes.This string is exactly 32 bytes.This string is exactly 32 by";
const Vector<Error> errors = BKE_path_apply_template(path, FILE_MAX, variables);
EXPECT_TRUE(errors.is_empty());
EXPECT_EQ(blender::StringRef(path), blender::StringRef(result));
/* Then do application, which should modify the path. */
const Vector<Error> application_errors = BKE_path_apply_template(path, FILE_MAX, variables);
EXPECT_EQ(application_errors, test_case.expected_errors)
<< " Template errors: " << errors_to_string(application_errors) << std::endl
<< " Expected errors: " << errors_to_string(test_case.expected_errors) << std::endl
<< " Note: test_case.path_in = " << test_case.path_in << std::endl;
EXPECT_EQ(blender::StringRef(path), test_case.path_result)
<< " Note: test_case.path_in = " << test_case.path_in << std::endl;
}
}

View File

@@ -1122,8 +1122,15 @@ static uiBut *ui_item_with_label(uiLayout *layout,
if (ELEM(subtype, PROP_FILEPATH, PROP_DIRPATH, PROP_NONE)) {
if ((RNA_property_flag(prop) & PROP_PATH_SUPPORTS_TEMPLATES) != 0) {
const std::string path = RNA_property_string_get(ptr, prop);
if (!BKE_validate_template_syntax(path.c_str()).is_empty()) {
UI_but_flag_enable(but, UI_BUT_REDALERT);
if (BKE_path_contains_template_syntax(path)) {
const std::optional<blender::bke::path_templates::VariableMap> variables =
BKE_build_template_variables_for_prop(
static_cast<const bContext *>(block->evil_C), ptr, prop);
BLI_assert(variables.has_value());
if (!BKE_path_validate_template(path, *variables).is_empty()) {
UI_but_flag_enable(but, UI_BUT_REDALERT);
}
}
}
}

View File

@@ -1071,16 +1071,22 @@ static std::unique_ptr<uiTooltipData> ui_tooltip_data_from_button_or_extra_icon(
/* Template parse errors, for paths that support it. */
if ((RNA_property_flag(rnaprop) & PROP_PATH_SUPPORTS_TEMPLATES) != 0) {
const std::string path = RNA_property_string_get(&but->rnapoin, rnaprop);
const blender::Vector<blender::bke::path_templates::Error> errors =
BKE_validate_template_syntax(path);
if (BKE_path_contains_template_syntax(path)) {
const std::optional<blender::bke::path_templates::VariableMap> variables =
BKE_build_template_variables_for_prop(C, &but->rnapoin, rnaprop);
BLI_assert(variables.has_value());
if (!errors.is_empty()) {
std::string error_message("Syntax error(s):");
for (const blender::bke::path_templates::Error &error : errors) {
error_message += "\n - " + BKE_path_template_error_to_string(error, path);
const blender::Vector<blender::bke::path_templates::Error> errors =
BKE_path_validate_template(path, *variables);
if (!errors.is_empty()) {
std::string error_message("Path template error(s):");
for (const blender::bke::path_templates::Error &error : errors) {
error_message += "\n - " + BKE_path_template_error_to_string(error, path);
}
UI_tooltip_text_field_add(
*data, error_message, {}, UI_TIP_STYLE_NORMAL, UI_TIP_LC_ALERT);
}
UI_tooltip_text_field_add(
*data, error_message, {}, UI_TIP_STYLE_NORMAL, UI_TIP_LC_ALERT);
}
}
}

View File

@@ -418,8 +418,8 @@ static void screen_opengl_render_write(OGLRender *oglrender)
rr = RE_AcquireResultRead(oglrender->re);
const char *relbase = BKE_main_blendfile_path(oglrender->bmain);
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
&scene->r);
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(relbase, &scene->r);
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
filepath,
scene->r.pic,
@@ -1048,8 +1048,8 @@ static void write_result(TaskPool *__restrict pool, WriteTaskData *task_data)
*/
char filepath[FILE_MAX];
const char *relbase = BKE_main_blendfile_path(oglrender->bmain);
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
&scene->r);
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(relbase, &scene->r);
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
filepath,
scene->r.pic,
@@ -1149,8 +1149,8 @@ static bool screen_opengl_render_anim_step(OGLRender *oglrender)
if (!is_movie) {
const char *relbase = BKE_main_blendfile_path(oglrender->bmain);
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
&scene->r);
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(relbase, &scene->r);
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
filepath,
scene->r.pic,

View File

@@ -304,11 +304,12 @@ static wmOperatorStatus file_browse_invoke(bContext *C, wmOperator *op, const wm
path = RNA_property_string_get_alloc(&ptr, prop, nullptr, 0, nullptr);
if ((RNA_property_flag(prop) & PROP_PATH_SUPPORTS_TEMPLATES) != 0) {
const Scene *scene = CTX_data_scene(C);
const std::optional<blender::bke::path_templates::VariableMap> variables =
BKE_build_template_variables_for_prop(C, &ptr, prop);
BLI_assert(variables.has_value());
const blender::Vector<blender::bke::path_templates::Error> errors = BKE_path_apply_template(
path,
FILE_MAX,
BKE_build_template_variables(BKE_main_blendfile_path_from_global(), &scene->r));
path, FILE_MAX, *variables);
if (!errors.is_empty()) {
BKE_report_path_template_errors(op->reports, RPT_ERROR, path, errors);
return OPERATOR_CANCELLED;

View File

@@ -1311,7 +1311,9 @@ static bool ffmpeg_filepath_get(MovieWriter *context,
BLI_strncpy(filepath, rd->pic, FILE_MAX);
const blender::Vector<blender::bke::path_templates::Error> errors = BKE_path_apply_template(
filepath, FILE_MAX, BKE_build_template_variables(BKE_main_blendfile_path_from_global(), rd));
filepath,
FILE_MAX,
BKE_build_template_variables_for_render_path(BKE_main_blendfile_path_from_global(), rd));
if (!errors.is_empty()) {
BKE_report_path_template_errors(reports, RPT_ERROR, filepath, errors);
return false;

View File

@@ -223,6 +223,7 @@ int RNA_property_override_flag(PropertyRNA *prop);
* the only way to set tags. Hence, at this point we assume the tag bit-field to be valid.
*/
int RNA_property_tags(PropertyRNA *prop);
PropertyPathTemplateType RNA_property_path_template_type(PropertyRNA *prop);
bool RNA_property_builtin(PropertyRNA *prop);
void *RNA_property_py_data_get(PropertyRNA *prop);

View File

@@ -582,6 +582,8 @@ void RNA_def_parameter_flags(PropertyRNA *prop,
void RNA_def_parameter_clear_flags(PropertyRNA *prop,
PropertyFlag flag_property,
ParameterFlag flag_parameter);
void RNA_def_property_path_template_type(PropertyRNA *prop,
PropertyPathTemplateType path_template_type);
/* Dynamic Enums
* strings are not freed, assumed pointing to static location. */

View File

@@ -431,6 +431,14 @@ enum PropertyFlag {
/**
* Paths that are evaluated with templating.
*
* Note that this doesn't cause the property to support templating, but rather
* *indicates* to other parts of Blender whether it supports templating.
* Support for templating needs to be manually implemented.
*
* When this is set, the property's `path_template_type` field should also be
* set to something other than `PROP_VARIABLES_NONE`, to indicate which
* template variables it supports.
*/
PROP_PATH_SUPPORTS_TEMPLATES = (1 << 14),
@@ -439,6 +447,17 @@ enum PropertyFlag {
};
ENUM_OPERATORS(PropertyFlag, PROP_TEXTEDIT_UPDATE)
/**
* For properties that support path templates, this indicates which variables
* should be available to them and how those variables should be built.
*
* \see BKE_build_template_variables_for_prop()
*/
enum PropertyPathTemplateType {
PROP_VARIABLES_NONE = 0,
PROP_VARIABLES_RENDER_OUTPUT,
};
/**
* Flags related to comparing and overriding RNA properties.
* Make sure enums are updated with these.

View File

@@ -4350,6 +4350,7 @@ static void rna_generate_property(FILE *f, StructRNA *srna, const char *nest, Pr
prop->flag_parameter,
prop->flag_internal,
prop->tags);
fprintf(f, "PropertyPathTemplateType(%d), ", prop->path_template_type);
rna_print_c_string(f, prop->name);
fprintf(f, ",\n\t");
rna_print_c_string(f, prop->description);

View File

@@ -1210,6 +1210,11 @@ int RNA_property_tags(PropertyRNA *prop)
return rna_ensure_property(prop)->tags;
}
PropertyPathTemplateType RNA_property_path_template_type(PropertyRNA *prop)
{
return rna_ensure_property(prop)->path_template_type;
}
bool RNA_property_builtin(PropertyRNA *prop)
{
return (rna_ensure_property(prop)->flag_internal & PROP_INTERN_BUILTIN) != 0;

View File

@@ -1557,6 +1557,12 @@ void RNA_def_parameter_clear_flags(PropertyRNA *prop,
prop->flag_parameter &= ~flag_parameter;
}
void RNA_def_property_path_template_type(PropertyRNA *prop,
PropertyPathTemplateType path_template_type)
{
prop->path_template_type = path_template_type;
}
void RNA_def_property_subtype(PropertyRNA *prop, PropertySubType subtype)
{
prop->subtype = subtype;

View File

@@ -350,6 +350,15 @@ struct PropertyRNA {
/** The subset of #StructRNA::prop_tag_defines values that applies to this property. */
short tags;
/**
* Indicates which set of template variables this property supports.
*
* Must be set for path properties that are marked as supporting path
* templates (`PROP_PATH_SUPPORTS_TEMPLATES` in `flag`). Is ignored for other
* properties.
*/
PropertyPathTemplateType path_template_type;
/** User readable name. */
const char *name;
/** Single line description, displayed in the tool-tip for example. */

View File

@@ -7829,6 +7829,7 @@ static void rna_def_cmp_output_file_slot_file(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Path", "Subpath used for this slot");
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_EDITOR_FILEBROWSER);
RNA_def_property_flag(prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_TEMPLATES);
RNA_def_property_path_template_type(prop, PROP_VARIABLES_RENDER_OUTPUT);
RNA_def_property_update(prop, NC_NODE | NA_EDITED, nullptr);
}
static void rna_def_cmp_output_file_slot_layer(BlenderRNA *brna)
@@ -7907,6 +7908,7 @@ static void def_cmp_output_file(BlenderRNA *brna, StructRNA *srna)
RNA_def_property_ui_text(prop, "Base Path", "Base output path for the image");
RNA_def_property_flag(
prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE | PROP_PATH_SUPPORTS_TEMPLATES);
RNA_def_property_path_template_type(prop, PROP_VARIABLES_RENDER_OUTPUT);
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
prop = RNA_def_property(srna, "active_input_index", PROP_INT, PROP_NONE);

View File

@@ -7354,13 +7354,13 @@ static void rna_def_scene_render_data(BlenderRNA *brna)
prop = RNA_def_property(srna, "filepath", PROP_STRING, PROP_FILEPATH);
RNA_def_property_string_sdna(prop, nullptr, "pic");
RNA_def_property_flag(prop, PROP_PATH_SUPPORTS_BLEND_RELATIVE);
RNA_def_property_flag(
prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE | PROP_PATH_SUPPORTS_TEMPLATES);
RNA_def_property_path_template_type(prop, PROP_VARIABLES_RENDER_OUTPUT);
RNA_def_property_ui_text(prop,
"Output Path",
"Directory/name to save animations, # characters define the position "
"and padding of frame numbers");
RNA_def_property_flag(
prop, PROP_PATH_OUTPUT | PROP_PATH_SUPPORTS_BLEND_RELATIVE | PROP_PATH_SUPPORTS_TEMPLATES);
RNA_def_property_update(prop, NC_SCENE | ND_RENDER_OPTIONS, nullptr);
/* Render result EXR cache. */

View File

@@ -119,7 +119,7 @@ static void rna_SceneRender_get_frame_path(RenderData *rd,
else {
const char *relbase = BKE_main_blendfile_path(bmain);
const blender::bke::path_templates::VariableMap template_variables =
BKE_build_template_variables(relbase, rd);
BKE_build_template_variables_for_render_path(relbase, rd);
const blender::Vector<blender::bke::path_templates::Error> errors =
BKE_image_path_from_imformat(filepath,

View File

@@ -870,8 +870,9 @@ class FileOutputOperation : public NodeOperation {
*/
bool get_single_layer_image_base_path(const char *base_name, char *r_base_path)
{
const path_templates::VariableMap template_variables = BKE_build_template_variables(
BKE_main_blendfile_path_from_global(), &context().get_render_data());
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(BKE_main_blendfile_path_from_global(),
&context().get_render_data());
/* Do template expansion on the node's base path. */
char node_base_path[FILE_MAX] = "";
@@ -953,8 +954,8 @@ class FileOutputOperation : public NodeOperation {
const RenderData &render_data = context().get_render_data();
const char *suffix = BKE_scene_multiview_view_suffix_get(&render_data, view);
const char *relbase = BKE_main_blendfile_path_from_global();
const path_templates::VariableMap template_variables = BKE_build_template_variables(
relbase, &render_data);
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(relbase, &render_data);
blender::Vector<path_templates::Error> errors = BKE_image_path_from_imtype(
r_image_path,
base_path,

View File

@@ -2097,8 +2097,8 @@ void RE_RenderFrame(Render *re,
else {
char filepath_override[FILE_MAX];
const char *relbase = BKE_main_blendfile_path(bmain);
const path_templates::VariableMap template_variables = BKE_build_template_variables(
relbase, &scene->r);
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(relbase, &scene->r);
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
filepath_override,
rd.pic,
@@ -2329,8 +2329,8 @@ static bool do_write_image_or_movie(
}
else {
const char *relbase = BKE_main_blendfile_path(bmain);
const path_templates::VariableMap template_variables = BKE_build_template_variables(
relbase, &scene->r);
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(relbase, &scene->r);
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
filepath,
scene->r.pic,
@@ -2532,8 +2532,8 @@ void RE_RenderAnim(Render *re,
/* Touch/NoOverwrite options are only valid for image's */
if (is_movie == false && do_write_file) {
const char *relbase = BKE_main_blendfile_path(bmain);
const path_templates::VariableMap template_variables = BKE_build_template_variables(relbase,
&rd);
const path_templates::VariableMap template_variables =
BKE_build_template_variables_for_render_path(relbase, &rd);
const blender::Vector<path_templates::Error> errors = BKE_image_path_from_imformat(
filepath,
rd.pic,