From b9074381e5d1947cfd5aeebbd48e0cef9d6163f8 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sat, 29 Jul 2023 15:47:04 +0300 Subject: [PATCH 1/3] Drivers Editor: apply red underline to drivers that failed evaluation. F-Curves with a broken rna path are highlighted with a red underline in the channel list of the animation editors. I think it makes sense to also apply this to drivers that failed to evaluate and were disabled. Otherwise, it is not apparent which drivers are broken without checking every one manually, or applying the errors filter. --- .../animation/anim_channels_defines.cc | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/source/blender/editors/animation/anim_channels_defines.cc b/source/blender/editors/animation/anim_channels_defines.cc index 129dd6f3aef..566a2aba370 100644 --- a/source/blender/editors/animation/anim_channels_defines.cc +++ b/source/blender/editors/animation/anim_channels_defines.cc @@ -4559,6 +4559,23 @@ 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. */ +static bool achannel_is_broken(const bAnimListElem *ale) +{ + switch (ale->type) { + case ANIMTYPE_FCURVE: + case ANIMTYPE_NLACURVE: { + const FCurve *fcu = static_cast(ale->data); + + /* The channel is disabled (has a bad rna path), or it's a driver that failed to evaluate. */ + return (ale->flag & FCURVE_DISABLED) || + (fcu->driver != nullptr && (fcu->driver->flag & DRIVER_FLAG_INVALID)); + } + default: + return false; + } +} + float ANIM_UI_get_keyframe_scale_factor() { bTheme *btheme = UI_GetTheme(); @@ -4742,7 +4759,7 @@ void ANIM_channel_draw( UI_fontstyle_draw_simple(fstyle, offset, ytext, name, col); /* draw red underline if channel is disabled */ - if (ELEM(ale->type, ANIMTYPE_FCURVE, ANIMTYPE_NLACURVE) && (ale->flag & FCURVE_DISABLED)) { + if (achannel_is_broken(ale)) { uint pos = GPU_vertformat_attr_add( immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); -- 2.30.2 From bbd7872680a44e5580b5c05dbc78e0de37dedb0a Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sat, 15 Jul 2023 14:03:17 +0300 Subject: [PATCH 2/3] Drivers: refactor driver_get_variable_property to return an enum. In order to prepare for introduction of fallbacks, refactor the function to return its status as an enumeration value. Also, move the index range check inside the function from python-specific code. --- source/blender/blenkernel/BKE_fcurve_driver.h | 27 ++++++--- .../blenkernel/intern/fcurve_driver.cc | 42 +++++++++---- .../blender/python/intern/bpy_rna_driver.cc | 60 +++++++++---------- 3 files changed, 79 insertions(+), 50 deletions(-) diff --git a/source/blender/blenkernel/BKE_fcurve_driver.h b/source/blender/blenkernel/BKE_fcurve_driver.h index 644c9540b95..5bca2abb1e3 100644 --- a/source/blender/blenkernel/BKE_fcurve_driver.h +++ b/source/blender/blenkernel/BKE_fcurve_driver.h @@ -134,16 +134,29 @@ struct DriverVar *driver_add_new_variable(struct ChannelDriver *driver); float driver_get_variable_value(const struct AnimationEvalContext *anim_eval_context, struct ChannelDriver *driver, struct DriverVar *dvar); + +typedef enum eDriverVariablePropertyResult { + /** The property reference has been succesfully resolved and can be accessed. */ + DRIVER_VAR_PROPERTY_SUCCESS, + /** The target property could not be resolved. */ + DRIVER_VAR_PROPERTY_INVALID, + /** The property was resolved (output parameters are set), + * but the array index is out of bounds. */ + DRIVER_VAR_PROPERTY_INVALID_INDEX +} eDriverVariablePropertyResult; + /** * Same as 'dtar_get_prop_val'. but get the RNA property. */ -bool driver_get_variable_property(const struct AnimationEvalContext *anim_eval_context, - struct ChannelDriver *driver, - struct DriverVar *dvar, - struct DriverTarget *dtar, - struct PointerRNA *r_ptr, - struct PropertyRNA **r_prop, - int *r_index); +eDriverVariablePropertyResult driver_get_variable_property( + const struct AnimationEvalContext *anim_eval_context, + struct ChannelDriver *driver, + struct DriverVar *dvar, + struct DriverTarget *dtar, + bool allow_no_index, + struct PointerRNA *r_ptr, + struct PropertyRNA **r_prop, + int *r_index); /** * Check if the expression in the driver conforms to the simple subset. diff --git a/source/blender/blenkernel/intern/fcurve_driver.cc b/source/blender/blenkernel/intern/fcurve_driver.cc index 458f42e9f0a..4cfc79501bf 100644 --- a/source/blender/blenkernel/intern/fcurve_driver.cc +++ b/source/blender/blenkernel/intern/fcurve_driver.cc @@ -253,13 +253,15 @@ static float dtar_get_prop_val(const AnimationEvalContext *anim_eval_context, return value; } -bool driver_get_variable_property(const AnimationEvalContext *anim_eval_context, - ChannelDriver *driver, - DriverVar *dvar, - DriverTarget *dtar, - PointerRNA *r_ptr, - PropertyRNA **r_prop, - int *r_index) +eDriverVariablePropertyResult driver_get_variable_property( + const AnimationEvalContext *anim_eval_context, + ChannelDriver *driver, + DriverVar *dvar, + DriverTarget *dtar, + const bool allow_no_index, + PointerRNA *r_ptr, + PropertyRNA **r_prop, + int *r_index) { PointerRNA ptr; PropertyRNA *prop; @@ -267,7 +269,7 @@ bool driver_get_variable_property(const AnimationEvalContext *anim_eval_context, /* Sanity check. */ if (ELEM(nullptr, driver, dtar)) { - return false; + return DRIVER_VAR_PROPERTY_INVALID; } /* Get RNA-pointer for the data-block given in target. */ @@ -281,7 +283,7 @@ bool driver_get_variable_property(const AnimationEvalContext *anim_eval_context, driver->flag |= DRIVER_FLAG_INVALID; dtar->flag |= DTAR_FLAG_INVALID; - return false; + return DRIVER_VAR_PROPERTY_INVALID; } /* Get property to read from, and get value as appropriate. */ @@ -307,16 +309,34 @@ bool driver_get_variable_property(const AnimationEvalContext *anim_eval_context, driver->flag |= DRIVER_FLAG_INVALID; dtar->flag |= DTAR_FLAG_INVALID; - return false; + return DRIVER_VAR_PROPERTY_INVALID; } *r_ptr = ptr; *r_prop = prop; *r_index = index; + /* 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)) { + /* Out of bounds. */ + if (G.debug & G_DEBUG) { + CLOG_ERROR(&LOG, + "Driver Evaluation Error: array index is out of bounds for %s -> %s (%d)", + ptr.owner_id->name, + dtar->rna_path, + index); + } + + driver->flag |= DRIVER_FLAG_INVALID; + dtar->flag |= DTAR_FLAG_INVALID; + return DRIVER_VAR_PROPERTY_INVALID_INDEX; + } + } + /* If we're still here, we should be ok. */ dtar->flag &= ~DTAR_FLAG_INVALID; - return true; + return DRIVER_VAR_PROPERTY_SUCCESS; } static short driver_check_valid_targets(ChannelDriver *driver, DriverVar *dvar) diff --git a/source/blender/python/intern/bpy_rna_driver.cc b/source/blender/python/intern/bpy_rna_driver.cc index 2397712459b..33dab4b7f2d 100644 --- a/source/blender/python/intern/bpy_rna_driver.cc +++ b/source/blender/python/intern/bpy_rna_driver.cc @@ -27,45 +27,41 @@ PyObject *pyrna_driver_get_variable_value(const AnimationEvalContext *anim_eval_ DriverVar *dvar, DriverTarget *dtar) { - PyObject *driver_arg = nullptr; PointerRNA ptr; PropertyRNA *prop = nullptr; int index; - if (driver_get_variable_property(anim_eval_context, driver, dvar, dtar, &ptr, &prop, &index)) { - if (prop) { - if (index != -1) { - if (index < RNA_property_array_length(&ptr, prop) && index >= 0) { - /* object, property & index */ - driver_arg = pyrna_array_index(&ptr, prop, index); - } - else { - /* out of range, pass */ - } - } - else { - /* object & property */ - const PropertyType type = RNA_property_type(prop); - if (type == PROP_ENUM) { - /* Note that enum's are converted to strings by default, - * we want to avoid that, see: #52213 */ - driver_arg = PyLong_FromLong(RNA_property_enum_get(&ptr, prop)); - } - else { - driver_arg = pyrna_prop_to_py(&ptr, prop); - } - } - } - else { + switch (driver_get_variable_property( + anim_eval_context, driver, dvar, dtar, true, &ptr, &prop, &index)) + { + case DRIVER_VAR_PROPERTY_SUCCESS: /* object only */ - driver_arg = pyrna_struct_CreatePyObject(&ptr); - } - } - else { - /* can't resolve path, pass */ + if (!prop) { + return pyrna_struct_CreatePyObject(&ptr); + } + + /* object, property & index */ + if (index >= 0) { + return pyrna_array_index(&ptr, prop, index); + } + + /* object & property (enum) */ + if (RNA_property_type(prop) == PROP_ENUM) { + /* Note that enum's are converted to strings by default, + * we want to avoid that, see: #52213 */ + return PyLong_FromLong(RNA_property_enum_get(&ptr, prop)); + } + + /* object & property */ + return pyrna_prop_to_py(&ptr, prop); + + case DRIVER_VAR_PROPERTY_INVALID: + case DRIVER_VAR_PROPERTY_INVALID_INDEX: + /* can't resolve path, pass */ + return nullptr; } - return driver_arg; + return nullptr; } PyObject *pyrna_driver_self_from_anim_rna(PathResolvedRNA *anim_rna) -- 2.30.2 From 4ccd1bec833b995b0c5d84b2b45760311152430b Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 14 Nov 2023 18:14:01 +0200 Subject: [PATCH 3/3] 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. --- scripts/startup/bl_ui/space_graph.py | 7 + source/blender/blenkernel/BKE_fcurve_driver.h | 2 + .../blenkernel/intern/fcurve_driver.cc | 38 ++++ .../animation/anim_channels_defines.cc | 2 +- .../blender/editors/animation/anim_filter.cc | 10 +- .../editors/space_graph/graph_buttons.cc | 24 +- source/blender/makesdna/DNA_action_types.h | 3 + source/blender/makesdna/DNA_anim_types.h | 16 +- source/blender/makesrna/intern/rna_action.cc | 10 + source/blender/makesrna/intern/rna_fcurve.cc | 22 ++ .../blender/python/intern/bpy_rna_driver.cc | 5 + tests/python/CMakeLists.txt | 7 + tests/python/bl_animation_drivers.py | 211 ++++++++++++++++++ 13 files changed, 350 insertions(+), 7 deletions(-) create mode 100644 tests/python/bl_animation_drivers.py diff --git a/scripts/startup/bl_ui/space_graph.py b/scripts/startup/bl_ui/space_graph.py index 7df04028b55..0bb4bbacd70 100644 --- a/scripts/startup/bl_ui/space_graph.py +++ b/scripts/startup/bl_ui/space_graph.py @@ -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' diff --git a/source/blender/blenkernel/BKE_fcurve_driver.h b/source/blender/blenkernel/BKE_fcurve_driver.h index 5bca2abb1e3..c2787dde064 100644 --- a/source/blender/blenkernel/BKE_fcurve_driver.h +++ b/source/blender/blenkernel/BKE_fcurve_driver.h @@ -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), diff --git a/source/blender/blenkernel/intern/fcurve_driver.cc b/source/blender/blenkernel/intern/fcurve_driver.cc index 4cfc79501bf..f7c02eabe9a 100644 --- a/source/blender/blenkernel/intern/fcurve_driver.cc +++ b/source/blender/blenkernel/intern/fcurve_driver.cc @@ -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, diff --git a/source/blender/editors/animation/anim_channels_defines.cc b/source/blender/editors/animation/anim_channels_defines.cc index 566a2aba370..19c66cd485a 100644 --- a/source/blender/editors/animation/anim_channels_defines.cc +++ b/source/blender/editors/animation/anim_channels_defines.cc @@ -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) { diff --git a/source/blender/editors/animation/anim_filter.cc b/source/blender/editors/animation/anim_filter.cc index f0e482afce4..c7811d5ebb4 100644 --- a/source/blender/editors/animation/anim_filter.cc +++ b/source/blender/editors/animation/anim_filter.cc @@ -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; } } diff --git a/source/blender/editors/space_graph/graph_buttons.cc b/source/blender/editors/space_graph/graph_buttons.cc index 9417537a1a0..533728ff483 100644 --- a/source/blender/editors/space_graph/graph_buttons.cc +++ b/source/blender/editors/space_graph/graph_buttons.cc @@ -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); } /* ----------------------------------------------------------------- */ diff --git a/source/blender/makesdna/DNA_action_types.h b/source/blender/makesdna/DNA_action_types.h index a1fac99e157..84cf5f62898 100644 --- a/source/blender/makesdna/DNA_action_types.h +++ b/source/blender/makesdna/DNA_action_types.h @@ -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 */ diff --git a/source/blender/makesdna/DNA_anim_types.h b/source/blender/makesdna/DNA_anim_types.h index 0e4fa90e127..0f45ff46eab 100644 --- a/source/blender/makesdna/DNA_anim_types.h +++ b/source/blender/makesdna/DNA_anim_types.h @@ -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 */ diff --git a/source/blender/makesrna/intern/rna_action.cc b/source/blender/makesrna/intern/rna_action.cc index fddf3f048eb..4bbb93dfa54 100644 --- a/source/blender/makesrna/intern/rna_action.cc +++ b/source/blender/makesrna/intern/rna_action.cc @@ -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) diff --git a/source/blender/makesrna/intern/rna_fcurve.cc b/source/blender/makesrna/intern/rna_fcurve.cc index 5b49911c8d6..3ded98004dd 100644 --- a/source/blender/makesrna/intern/rna_fcurve.cc +++ b/source/blender/makesrna/intern/rna_fcurve.cc @@ -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) diff --git a/source/blender/python/intern/bpy_rna_driver.cc b/source/blender/python/intern/bpy_rna_driver.cc index 33dab4b7f2d..8db248cb3cd 100644 --- a/source/blender/python/intern/bpy_rna_driver.cc +++ b/source/blender/python/intern/bpy_rna_driver.cc @@ -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 */ diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 203494e58c2..6df6b903a0c 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -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 diff --git a/tests/python/bl_animation_drivers.py b/tests/python/bl_animation_drivers.py new file mode 100644 index 00000000000..caca840f194 --- /dev/null +++ b/tests/python/bl_animation_drivers.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() -- 2.30.2