Drivers: implement fallback values for RNA path based variables.

As discussed in #105407, it can be useful to support returning
a fallback value specified by the user instead of failing the driver
if a driver variable cannot resolve its RNA path. This especially
applies to context variables referencing custom properties, since
when the object with the driver is linked into another scene, the
custom property can easily not exist there.

This patch adds an optional fallback value setting to properties
based on RNA path (including ordinary Single Property variables
due to shared code and similarity). When enabled, RNA path lookup
failures (including invalid array index) cause the fallback value
to be used instead of marking the driver invalid.

A flag is added to track when this happens for UI use. It is
also exposed to python for lint type scripts.

When the fallback value is used, the input field containing
the property RNA path that failed to resolve is highlighted in red
(identically to the case without a fallback), and the driver
can be included in the With Errors filter of the Drivers editor.
However, the channel name is not underlined in red, because
the driver as a whole evaluates successfully.

Pull Request: https://projects.blender.org/blender/blender/pulls/110135
This commit is contained in:
Alexander Gavrilov 2023-11-14 18:14:01 +02:00 committed by Gitea
parent bbd7872680
commit d0ef66ddff
13 changed files with 350 additions and 7 deletions

@ -93,6 +93,7 @@ class GRAPH_PT_filters(DopesheetFilterPopoverBase, Panel):
def draw(self, context):
layout = self.layout
st = context.space_data
DopesheetFilterPopoverBase.draw_generic_filters(context, layout)
layout.separator()
@ -100,6 +101,12 @@ class GRAPH_PT_filters(DopesheetFilterPopoverBase, Panel):
layout.separator()
DopesheetFilterPopoverBase.draw_standard_filters(context, layout)
if st.mode == 'DRIVERS':
layout.separator()
col = layout.column(align=True)
col.label(text="Drivers:")
col.prop(st.dopesheet, "show_driver_fallback_as_error")
class GRAPH_PT_snapping(Panel):
bl_space_type = 'GRAPH_EDITOR'

@ -138,6 +138,8 @@ float driver_get_variable_value(const struct AnimationEvalContext *anim_eval_con
typedef enum eDriverVariablePropertyResult {
/** The property reference has been succesfully resolved and can be accessed. */
DRIVER_VAR_PROPERTY_SUCCESS,
/** Evaluation should use the fallback value. */
DRIVER_VAR_PROPERTY_FALLBACK,
/** The target property could not be resolved. */
DRIVER_VAR_PROPERTY_INVALID,
/** The property was resolved (output parameters are set),

@ -146,6 +146,21 @@ bool driver_get_target_property(const DriverTargetContext *driver_target_context
return true;
}
/**
* Checks if the fallback value can be used, and if so, sets dtar flags to signal its usage.
* The caller is expected to immediately return the fallback value if this returns true.
*/
static bool dtar_try_use_fallback(DriverTarget *dtar)
{
if ((dtar->options & DTAR_OPTION_USE_FALLBACK) == 0) {
return false;
}
dtar->flag &= ~DTAR_FLAG_INVALID;
dtar->flag |= DTAR_FLAG_FALLBACK_USED;
return true;
}
/**
* Helper function to obtain a value using RNA from the specified source
* (for evaluating drivers).
@ -160,6 +175,8 @@ static float dtar_get_prop_val(const AnimationEvalContext *anim_eval_context,
return 0.0f;
}
dtar->flag &= ~DTAR_FLAG_FALLBACK_USED;
/* Get property to resolve the target from.
* Naming is a bit confusing, but this is what is exposed as "Prop" or "Context Property" in
* interface. */
@ -184,6 +201,10 @@ static float dtar_get_prop_val(const AnimationEvalContext *anim_eval_context,
if (!RNA_path_resolve_property_full(
&property_ptr, dtar->rna_path, &value_ptr, &value_prop, &index))
{
if (dtar_try_use_fallback(dtar)) {
return dtar->fallback_value;
}
/* Path couldn't be resolved. */
if (G.debug & G_DEBUG) {
CLOG_ERROR(&LOG,
@ -200,6 +221,10 @@ static float dtar_get_prop_val(const AnimationEvalContext *anim_eval_context,
if (RNA_property_array_check(value_prop)) {
/* Array. */
if (index < 0 || index >= RNA_property_array_length(&value_ptr, value_prop)) {
if (dtar_try_use_fallback(dtar)) {
return dtar->fallback_value;
}
/* Out of bounds. */
if (G.debug & G_DEBUG) {
CLOG_ERROR(&LOG,
@ -272,6 +297,8 @@ eDriverVariablePropertyResult driver_get_variable_property(
return DRIVER_VAR_PROPERTY_INVALID;
}
dtar->flag &= ~DTAR_FLAG_FALLBACK_USED;
/* Get RNA-pointer for the data-block given in target. */
const DriverTargetContext driver_target_context = driver_target_context_from_animation_context(
anim_eval_context);
@ -295,6 +322,13 @@ eDriverVariablePropertyResult driver_get_variable_property(
/* OK. */
}
else {
if (dtar_try_use_fallback(dtar)) {
ptr = PointerRNA_NULL;
*r_prop = nullptr;
*r_index = -1;
return DRIVER_VAR_PROPERTY_FALLBACK;
}
/* Path couldn't be resolved. */
if (G.debug & G_DEBUG) {
CLOG_ERROR(&LOG,
@ -319,6 +353,10 @@ eDriverVariablePropertyResult driver_get_variable_property(
/* Verify the array index and apply fallback if appropriate. */
if (prop && RNA_property_array_check(prop)) {
if ((index < 0 && !allow_no_index) || index >= RNA_property_array_length(&ptr, prop)) {
if (dtar_try_use_fallback(dtar)) {
return DRIVER_VAR_PROPERTY_FALLBACK;
}
/* Out of bounds. */
if (G.debug & G_DEBUG) {
CLOG_ERROR(&LOG,

@ -4559,7 +4559,7 @@ static bool achannel_is_being_renamed(const bAnimContext *ac,
return false;
}
/** Check if the animation channel name should be underlined in red due to fatal errors. */
/** Check if the animation channel name should be underlined in red due to errors. */
static bool achannel_is_broken(const bAnimListElem *ale)
{
switch (ale->type) {

@ -1212,7 +1212,7 @@ static bool skip_fcurve_with_name(
*
* \return true if F-Curve has errors/is disabled
*/
static bool fcurve_has_errors(const FCurve *fcu)
static bool fcurve_has_errors(const FCurve *fcu, bDopeSheet *ads)
{
/* F-Curve disabled (path evaluation error). */
if (fcu->flag & FCURVE_DISABLED) {
@ -1238,6 +1238,12 @@ static bool fcurve_has_errors(const FCurve *fcu)
if (dtar->flag & DTAR_FLAG_INVALID) {
return true;
}
if ((dtar->flag & DTAR_FLAG_FALLBACK_USED) &&
(ads->filterflag2 & ADS_FILTER_DRIVER_FALLBACK_AS_ERROR))
{
return true;
}
}
DRIVER_TARGETS_LOOPER_END;
}
@ -1305,7 +1311,7 @@ static FCurve *animfilter_fcurve_next(bDopeSheet *ads,
/* error-based filtering... */
if ((ads) && (ads->filterflag & ADS_FILTER_ONLY_ERRORS)) {
/* skip if no errors... */
if (fcurve_has_errors(fcu) == false) {
if (!fcurve_has_errors(fcu, ads)) {
continue;
}
}

@ -740,6 +740,20 @@ static bool graph_panel_drivers_poll(const bContext *C, PanelType * /*pt*/)
return graph_panel_context(C, nullptr, nullptr);
}
static void graph_panel_driverVar_fallback(uiLayout *layout,
const DriverTarget *dtar,
PointerRNA *dtar_ptr)
{
if (dtar->options & DTAR_OPTION_USE_FALLBACK) {
uiLayout *row = uiLayoutRow(layout, true);
uiItemR(row, dtar_ptr, "use_fallback_value", UI_ITEM_NONE, "", ICON_NONE);
uiItemR(row, dtar_ptr, "fallback_value", UI_ITEM_NONE, nullptr, ICON_NONE);
}
else {
uiItemR(layout, dtar_ptr, "use_fallback_value", UI_ITEM_NONE, nullptr, ICON_NONE);
}
}
/* settings for 'single property' driver variable type */
static void graph_panel_driverVar__singleProp(uiLayout *layout, ID *id, DriverVar *dvar)
{
@ -761,12 +775,15 @@ static void graph_panel_driverVar__singleProp(uiLayout *layout, ID *id, DriverVa
/* rna path */
col = uiLayoutColumn(layout, true);
uiLayoutSetRedAlert(col, (dtar->flag & DTAR_FLAG_INVALID));
uiLayoutSetRedAlert(col, (dtar->flag & (DTAR_FLAG_INVALID | DTAR_FLAG_FALLBACK_USED)));
uiTemplatePathBuilder(col,
&dtar_ptr,
"data_path",
&root_ptr,
CTX_IFACE_(BLT_I18NCONTEXT_EDITOR_FILEBROWSER, "Path"));
/* Default value. */
graph_panel_driverVar_fallback(layout, dtar, &dtar_ptr);
}
}
@ -904,13 +921,16 @@ static void graph_panel_driverVar__contextProp(uiLayout *layout, ID *id, DriverV
/* Target Path */
{
uiLayout *col = uiLayoutColumn(layout, true);
uiLayoutSetRedAlert(col, (dtar->flag & DTAR_FLAG_INVALID));
uiLayoutSetRedAlert(col, (dtar->flag & (DTAR_FLAG_INVALID | DTAR_FLAG_FALLBACK_USED)));
uiTemplatePathBuilder(col,
&dtar_ptr,
"data_path",
nullptr,
CTX_IFACE_(BLT_I18NCONTEXT_EDITOR_FILEBROWSER, "Path"));
}
/* Default value. */
graph_panel_driverVar_fallback(layout, dtar, &dtar_ptr);
}
/* ----------------------------------------------------------------- */

@ -827,6 +827,9 @@ typedef enum eDopeSheet_FilterFlag2 {
ADS_FILTER_NOHAIR = (1 << 3),
ADS_FILTER_NOPOINTCLOUD = (1 << 4),
ADS_FILTER_NOVOLUME = (1 << 5),
/** Include working drivers with variables using their fallback values into Only Show Errors. */
ADS_FILTER_DRIVER_FALLBACK_AS_ERROR = (1 << 6),
} eDopeSheet_FilterFlag2;
/* DopeSheet general flags */

@ -312,13 +312,15 @@ typedef struct DriverTarget {
/** Rotation channel calculation type. */
char rotation_mode;
char _pad[7];
char _pad[5];
/**
* Flags for the validity of the target
* (NOTE: these get reset every time the types change).
*/
short flag;
/** Single-bit user-visible toggles (not reset on type change) from eDriverTarget_Options. */
short options;
/** Type of ID-block that this target can use. */
int idtype;
@ -327,9 +329,16 @@ typedef struct DriverTarget {
* This is a value of enumerator #eDriverTarget_ContextProperty. */
int context_property;
int _pad1;
/* Fallback value to use with DTAR_OPTION_USE_FALLBACK. */
float fallback_value;
} DriverTarget;
/** Driver Target options. */
typedef enum eDriverTarget_Options {
/** Use the fallback value when the target is invalid (rna_path cannot be resolved). */
DTAR_OPTION_USE_FALLBACK = (1 << 0),
} eDriverTarget_Options;
/** Driver Target flags. */
typedef enum eDriverTarget_Flag {
/** used for targets that use the pchan_name instead of RNA path
@ -346,6 +355,9 @@ typedef enum eDriverTarget_Flag {
/** error flags */
DTAR_FLAG_INVALID = (1 << 4),
/** the fallback value was actually used */
DTAR_FLAG_FALLBACK_USED = (1 << 5),
} eDriverTarget_Flag;
/* Transform Channels for Driver Targets */

@ -655,6 +655,16 @@ static void rna_def_dopesheet(BlenderRNA *brna)
prop, "Display Movie Clips", "Include visualization of movie clip related animation data");
RNA_def_property_ui_icon(prop, ICON_TRACKER, 0);
RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, nullptr);
prop = RNA_def_property(srna, "show_driver_fallback_as_error", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "filterflag2", ADS_FILTER_DRIVER_FALLBACK_AS_ERROR);
RNA_def_property_ui_text(
prop,
"Variable Fallback As Error",
"Include drivers that relied on any fallback values for their evaluation "
"in the Only Show Errors filter, even if the driver evaluation succeeded");
RNA_def_property_ui_icon(prop, ICON_RNA, 0);
RNA_def_property_update(prop, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, nullptr);
}
static void rna_def_action_group(BlenderRNA *brna)

@ -1974,6 +1974,28 @@ static void rna_def_drivertarget(BlenderRNA *brna)
RNA_def_property_ui_text(
prop, "Context Property", "Type of a context-dependent data-block to access property from");
RNA_def_property_update(prop, 0, "rna_DriverTarget_update_data");
prop = RNA_def_property(srna, "use_fallback_value", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "options", DTAR_OPTION_USE_FALLBACK);
RNA_def_property_ui_text(prop,
"Use Fallback",
"Use the fallback value if the data path can't be resolved, instead of "
"failing to evaluate the driver");
RNA_def_property_update(prop, 0, "rna_DriverTarget_update_data");
prop = RNA_def_property(srna, "fallback_value", PROP_FLOAT, PROP_NONE);
RNA_def_property_float_sdna(prop, nullptr, "fallback_value");
RNA_def_property_ui_text(
prop, "Fallback", "The value to use if the data path can't be resolved");
RNA_def_property_update(prop, 0, "rna_DriverTarget_update_data");
prop = RNA_def_property(srna, "is_fallback_used", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", DTAR_FLAG_FALLBACK_USED);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(
prop,
"Is Fallback Used",
"Indicates that the most recent variable evaluation used the fallback value");
}
static void rna_def_drivervar(BlenderRNA *brna)

@ -12,6 +12,8 @@
#include "MEM_guardedalloc.h"
#include "DNA_anim_types.h"
#include "BLI_utildefines.h"
#include "BKE_fcurve_driver.h"
@ -55,6 +57,9 @@ PyObject *pyrna_driver_get_variable_value(const AnimationEvalContext *anim_eval_
/* object & property */
return pyrna_prop_to_py(&ptr, prop);
case DRIVER_VAR_PROPERTY_FALLBACK:
return PyFloat_FromDouble(dtar->fallback_value);
case DRIVER_VAR_PROPERTY_INVALID:
case DRIVER_VAR_PROPERTY_INVALID_INDEX:
/* can't resolve path, pass */

@ -364,6 +364,13 @@ add_blender_test(
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_armature.py
)
add_blender_test(
bl_animation_drivers
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_drivers.py
--
--testdir "${TEST_SRC_DIR}/animation"
)
add_blender_test(
bl_animation_fcurves
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_fcurves.py

@ -0,0 +1,211 @@
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import unittest
import bpy
import pathlib
import sys
from rna_prop_ui import rna_idprop_quote_path
"""
blender -b -noaudio --factory-startup --python tests/python/bl_animation_drivers.py -- --testdir /path/to/lib/tests/animation
"""
class AbstractEmptyDriverTest:
def setUp(self):
super().setUp()
bpy.ops.wm.read_homefile(use_factory_startup=True)
self.obj = bpy.data.objects['Cube']
def assertPropValue(self, prop_name, value):
self.assertEqual(self.obj[prop_name], value)
def _make_context_driver(obj, prop_name, ctx_type, ctx_path, index=None, fallback=None, force_python=False):
obj[prop_name] = 0
fcu = obj.driver_add(rna_idprop_quote_path(prop_name), -1)
drv = fcu.driver
if force_python:
# Expression that requires full python interpreter
drv.type = 'SCRIPTED'
drv.expression = '[var][0]'
else:
drv.type = 'SUM'
var = drv.variables.new()
var.name = "var"
var.type = 'CONTEXT_PROP'
tgt = var.targets[0]
tgt.context_property = ctx_type
tgt.data_path = rna_idprop_quote_path(ctx_path) + (f"[{index}]" if index is not None else "")
if fallback is not None:
tgt.use_fallback_value = True
tgt.fallback_value = fallback
return fcu
def _is_fallback_used(fcu):
return fcu.driver.variables[0].targets[0].is_fallback_used
class ContextSceneDriverTest(AbstractEmptyDriverTest, unittest.TestCase):
""" Ensure keying things by name or with a keying set adds the right keys. """
def setUp(self):
super().setUp()
bpy.context.scene["test_property"] = 123
def test_context_valid(self):
fcu = _make_context_driver(
self.obj, 'test_valid', 'ACTIVE_SCENE', 'test_property')
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertPropValue('test_valid', 123)
def test_context_invalid(self):
fcu = _make_context_driver(
self.obj, 'test_invalid', 'ACTIVE_SCENE', 'test_property_bad')
bpy.context.view_layer.update()
self.assertFalse(fcu.driver.is_valid)
def test_context_fallback(self):
fcu = _make_context_driver(
self.obj, 'test_fallback', 'ACTIVE_SCENE', 'test_property_bad', fallback=321)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertTrue(_is_fallback_used(fcu))
self.assertPropValue('test_fallback', 321)
def test_context_fallback_valid(self):
fcu = _make_context_driver(
self.obj, 'test_fallback_valid', 'ACTIVE_SCENE', 'test_property', fallback=321)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertFalse(_is_fallback_used(fcu))
self.assertPropValue('test_fallback_valid', 123)
def test_context_fallback_python(self):
fcu = _make_context_driver(
self.obj, 'test_fallback_py', 'ACTIVE_SCENE', 'test_property_bad', fallback=321, force_python=True)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertTrue(_is_fallback_used(fcu))
self.assertPropValue('test_fallback_py', 321)
class ContextSceneArrayDriverTest(AbstractEmptyDriverTest, unittest.TestCase):
""" Ensure keying things by name or with a keying set adds the right keys. """
def setUp(self):
super().setUp()
bpy.context.scene["test_property"] = [123, 456]
def test_context_valid(self):
fcu = _make_context_driver(
self.obj, 'test_valid', 'ACTIVE_SCENE', 'test_property', index=0)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertPropValue('test_valid', 123)
def test_context_invalid(self):
fcu = _make_context_driver(
self.obj, 'test_invalid', 'ACTIVE_SCENE', 'test_property', index=2)
bpy.context.view_layer.update()
self.assertFalse(fcu.driver.is_valid)
def test_context_fallback(self):
fcu = _make_context_driver(
self.obj, 'test_fallback', 'ACTIVE_SCENE', 'test_property', index=2, fallback=321)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertTrue(_is_fallback_used(fcu))
self.assertPropValue('test_fallback', 321)
def test_context_fallback_valid(self):
fcu = _make_context_driver(
self.obj, 'test_fallback_valid', 'ACTIVE_SCENE', 'test_property', index=0, fallback=321)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertFalse(_is_fallback_used(fcu))
self.assertPropValue('test_fallback_valid', 123)
def test_context_fallback_python(self):
fcu = _make_context_driver(
self.obj, 'test_fallback_py', 'ACTIVE_SCENE', 'test_property', index=2, fallback=321, force_python=True)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertTrue(_is_fallback_used(fcu))
self.assertPropValue('test_fallback_py', 321)
def _select_view_layer(index):
bpy.context.window.view_layer = bpy.context.scene.view_layers[index]
class ContextViewLayerDriverTest(AbstractEmptyDriverTest, unittest.TestCase):
""" Ensure keying things by name or with a keying set adds the right keys. """
def setUp(self):
super().setUp()
bpy.ops.scene.view_layer_add(type='COPY')
scene = bpy.context.scene
scene.view_layers[0]['test_property'] = 123
scene.view_layers[1]['test_property'] = 456
_select_view_layer(0)
def test_context_valid(self):
fcu = _make_context_driver(
self.obj, 'test_valid', 'ACTIVE_VIEW_LAYER', 'test_property')
_select_view_layer(0)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertPropValue('test_valid', 123)
_select_view_layer(1)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertPropValue('test_valid', 456)
def test_context_fallback(self):
del bpy.context.scene.view_layers[1]['test_property']
fcu = _make_context_driver(
self.obj, 'test_fallback', 'ACTIVE_VIEW_LAYER', 'test_property', fallback=321)
_select_view_layer(0)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertFalse(_is_fallback_used(fcu))
self.assertPropValue('test_fallback', 123)
_select_view_layer(1)
bpy.context.view_layer.update()
self.assertTrue(fcu.driver.is_valid)
self.assertTrue(_is_fallback_used(fcu))
self.assertPropValue('test_fallback', 321)
def main():
global args
import argparse
if '--' in sys.argv:
argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
else:
argv = sys.argv
parser = argparse.ArgumentParser()
parser.add_argument('--testdir', required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
if __name__ == "__main__":
main()