From dec032e12e2f171425f4823e202e7625e0c18fb4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 8 Oct 2025 11:13:24 +0200 Subject: [PATCH] Anim: Indicate Parent Inverse Matrix State in UI Show the Parent Inverse matrix in the Object properties, Transform panel. The matrix is shown decomposed as location/rotation/scale. Pull Request: https://projects.blender.org/blender/blender/pulls/113364 --- scripts/startup/bl_ui/properties_object.py | 22 ++ source/blender/blenlib/BLI_math_matrix.h | 1 + .../blender/blenlib/intern/math_matrix_c.cc | 13 + .../blender/editors/include/UI_interface_c.hh | 1 + .../blender/editors/interface/CMakeLists.txt | 1 + .../templates/interface_template_matrix.cc | 229 ++++++++++++++++++ .../editors/object/object_constraint.cc | 53 +++- source/blender/makesrna/intern/rna_ui_api.cc | 9 + 8 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 source/blender/editors/interface/templates/interface_template_matrix.cc diff --git a/scripts/startup/bl_ui/properties_object.py b/scripts/startup/bl_ui/properties_object.py index 1f7fb798e4a..2d6588be132 100644 --- a/scripts/startup/bl_ui/properties_object.py +++ b/scripts/startup/bl_ui/properties_object.py @@ -108,6 +108,27 @@ class OBJECT_PT_delta_transform(ObjectButtonsPanel, Panel): col.prop(ob, "delta_scale", text="Scale") +class OBJECT_PT_parent_inverse_transform(ObjectButtonsPanel, Panel): + bl_label = "Parent Inverse Transform" + bl_parent_id = "OBJECT_PT_transform" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + ob = context.object + return ob and ob.parent + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + + ob = context.object + layout.template_matrix(ob, "matrix_parent_inverse") + + props = layout.operator("object.parent_clear", text="Clear Parent Inverse Transform") + props.type = 'CLEAR_INVERSE' + + class OBJECT_PT_relations(ObjectButtonsPanel, Panel): bl_label = "Relations" bl_options = {'DEFAULT_CLOSED'} @@ -605,6 +626,7 @@ classes = ( OBJECT_PT_context_object, OBJECT_PT_transform, OBJECT_PT_delta_transform, + OBJECT_PT_parent_inverse_transform, OBJECT_PT_relations, COLLECTION_MT_context_menu, OBJECT_PT_collections, diff --git a/source/blender/blenlib/BLI_math_matrix.h b/source/blender/blenlib/BLI_math_matrix.h index b7c12bb36e2..300e60f9cfd 100644 --- a/source/blender/blenlib/BLI_math_matrix.h +++ b/source/blender/blenlib/BLI_math_matrix.h @@ -297,6 +297,7 @@ bool is_orthogonal_m4(const float m[4][4]); bool is_orthonormal_m3(const float m[3][3]); bool is_orthonormal_m4(const float m[4][4]); +bool is_identity_m4(const float m[4][4]); bool is_uniform_scaled_m3(const float m[3][3]); bool is_uniform_scaled_m4(const float m[4][4]); diff --git a/source/blender/blenlib/intern/math_matrix_c.cc b/source/blender/blenlib/intern/math_matrix_c.cc index cb6bba87771..5c5dbc3f788 100644 --- a/source/blender/blenlib/intern/math_matrix_c.cc +++ b/source/blender/blenlib/intern/math_matrix_c.cc @@ -1672,6 +1672,19 @@ bool is_orthonormal_m4(const float m[4][4]) return false; } +bool is_identity_m4(const float m[4][4]) +{ + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 4; col++) { + if (m[row][col] != (row == col ? 1.0f : 0.0f)) { + return false; + } + } + } + + return true; +} + bool is_uniform_scaled_m3(const float m[3][3]) { const float eps = 1e-7f; diff --git a/source/blender/editors/include/UI_interface_c.hh b/source/blender/editors/include/UI_interface_c.hh index 51378118605..482fedb26ed 100644 --- a/source/blender/editors/include/UI_interface_c.hh +++ b/source/blender/editors/include/UI_interface_c.hh @@ -2307,6 +2307,7 @@ void uiTemplateIDPreview(uiLayout *layout, int cols, int filter = UI_TEMPLATE_ID_FILTER_ALL, bool hide_buttons = false); +void uiTemplateMatrix(uiLayout *layout, PointerRNA *ptr, blender::StringRefNull propname); /** * Version of #uiTemplateID using tabs. */ diff --git a/source/blender/editors/interface/CMakeLists.txt b/source/blender/editors/interface/CMakeLists.txt index d53c6fb62a8..0ce7392bb8e 100644 --- a/source/blender/editors/interface/CMakeLists.txt +++ b/source/blender/editors/interface/CMakeLists.txt @@ -74,6 +74,7 @@ set(SRC templates/interface_template_layers.cc templates/interface_template_light_linking.cc templates/interface_template_list.cc + templates/interface_template_matrix.cc templates/interface_template_modifiers.cc templates/interface_template_node_inputs.cc templates/interface_template_node_tree_interface.cc diff --git a/source/blender/editors/interface/templates/interface_template_matrix.cc b/source/blender/editors/interface/templates/interface_template_matrix.cc new file mode 100644 index 00000000000..caa89f8a30f --- /dev/null +++ b/source/blender/editors/interface/templates/interface_template_matrix.cc @@ -0,0 +1,229 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edinterface + */ + +#include + +#include "BKE_unit.hh" + +#include "BLI_math_matrix.h" +#include "BLI_math_rotation.h" + +#include "BLT_translation.hh" + +#include "RNA_access.hh" +#include "RNA_enum_types.hh" + +#include "interface_intern.hh" + +using blender::StringRef; +using blender::StringRefNull; + +/* Format translation/rotation value as a string based on Blender unit settings. */ +static std::string format_unit_value(float value, PropertySubType subtype, uiLayout *layout) +{ + const UnitSettings *unit = layout->block()->unit; + const int unit_type = RNA_SUBTYPE_UNIT(subtype); + + /* Change negative zero to regular zero, without altering anything else. */ + value += +0.0f; + double value_scaled = BKE_unit_value_scale(*unit, unit_type, value); + + char new_str[UI_MAX_DRAW_STR]; + BKE_unit_value_as_string(new_str, + sizeof(new_str), + value_scaled, + RNA_TRANSLATION_PREC_DEFAULT, + RNA_SUBTYPE_UNIT_VALUE(unit_type), + *unit, + true); + return std::string(new_str); +} + +/* Format unitless value as a string. */ +static std::string format_coefficient(float value) +{ + /* Change negative zero to regular zero, without altering anything else. */ + value += +0.0f; + /* Same precision that we use in `Object.scale`. */ + const int RNA_SCALE_PREC_DEFAULT = 3; + return fmt::format("{:.{}f}", value, RNA_SCALE_PREC_DEFAULT); +} + +/* Static variable to store rotation mode button state at runtime. + * Defaults to XYZ Euler. */ +static int rotation_mode_index = ROT_MODE_EUL; + +static void rotation_mode_menu_callback(bContext *, uiLayout *layout, void *) +{ + for (size_t i = 0; i < RNA_enum_items_count(rna_enum_object_rotation_mode_items); i++) { + const EnumPropertyItem &mode_info = rna_enum_object_rotation_mode_items[i]; + const int yco = -1.5f * UI_UNIT_Y; + const int width = 9 * UI_UNIT_X; + uiBut *but = uiDefButI(layout->block(), + ButType::Row, + 0, + IFACE_(mode_info.name), + 0, + yco, + width / 2, + UI_UNIT_Y, + &rotation_mode_index, + i, + i, + TIP_(mode_info.description)); + UI_but_flag_disable(but, UI_BUT_UNDO); + if (i == rotation_mode_index) { + UI_but_flag_enable(but, UI_SELECT_DRAW); + } + } +} + +static void draw_matrix_template(uiLayout &layout, PointerRNA &ptr, PropertyRNA &prop) +{ + /* Matrix template UI is mirroring Object's Transform UI for better UX. */ + uiLayout *row, *col; + uiLayout *layout_ = &layout.box(); + + float m4[4][4]; + RNA_property_float_get_array(&ptr, &prop, &m4[0][0]); + + /* Show a warning as a matrix with a shear cannot be represented fully + * by a decomposition. + * Use the 3x3 matrix, as shear in the 4x4 homogeneous matrix + * is expected due to the translation component. */ + float m3[3][3]; + copy_m3_m4(m3, m4); + if (!is_orthogonal_m3(m3)) { + layout_->label(RPT_("Matrix has a shear"), ICON_ERROR); + } + + float loc[3], quat[4], size[3]; + mat4_decompose(loc, quat, size, m4); + + /* Translation. */ + col = &layout_->column(true); + col->use_property_split_set(true); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Location X"), ICON_NONE); + row->label(format_unit_value(loc[0], PROP_TRANSLATION, layout_), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Y"), ICON_NONE); + row->label(format_unit_value(loc[1], PROP_TRANSLATION, layout_), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Z"), ICON_NONE); + row->label(format_unit_value(loc[2], PROP_TRANSLATION, layout_), ICON_NONE); + + /* Rotation. */ + float eul[3], axis[3]; + float angle; + const EnumPropertyItem &mode_info = rna_enum_object_rotation_mode_items[rotation_mode_index]; + col = &layout_->column(true); + col->use_property_split_set(true); + + if (mode_info.value == ROT_MODE_QUAT) { + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Rotation W"), ICON_NONE); + row->label(format_coefficient(quat[0]), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("X"), ICON_NONE); + row->label(format_coefficient(quat[1]), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Y"), ICON_NONE); + row->label(format_coefficient(quat[2]), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Z"), ICON_NONE); + row->label(format_coefficient(quat[3]), ICON_NONE); + } + else if (mode_info.value == ROT_MODE_AXISANGLE) { + quat_to_axis_angle(axis, &angle, quat); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Rotation W"), ICON_NONE); + row->label(format_unit_value(angle, PROP_ANGLE, layout_), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("X"), ICON_NONE); + row->label(format_coefficient(axis[0]), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Y"), ICON_NONE); + row->label(format_coefficient(axis[1]), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Z"), ICON_NONE); + row->label(format_coefficient(axis[2]), ICON_NONE); + } + else { /* Euler modes. */ + quat_to_eulO(eul, mode_info.value, quat); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Rotation X"), ICON_NONE); + row->label(format_unit_value(eul[0], PROP_ANGLE, layout_), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Y"), ICON_NONE); + row->label(format_unit_value(eul[1], PROP_ANGLE, layout_), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Z"), ICON_NONE); + row->label(format_unit_value(eul[2], PROP_ANGLE, layout_), ICON_NONE); + } + + /* Mirror RNA enum property dropdown UI - with menu triangle an dropdown items. */ + row = &layout_->row(true); + uiItemL_respect_property_split(row, IFACE_("Mode"), ICON_NONE); + uiBlock *block = row->block(); + uiBut *but = uiDefMenuBut(block, + rotation_mode_menu_callback, + nullptr, + mode_info.name, + 0, + 0, + UI_UNIT_X * 10, + UI_UNIT_Y, + TIP_("Rotation mode.\n\nOnly affects the way " + "rotation is displayed, rotation itself is unaffected.")); + UI_but_type_set_menu_from_pulldown(but); + + /* Scale. */ + col = &layout_->column(true); + col->use_property_split_set(true); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Scale X"), ICON_NONE); + row->label(format_coefficient(size[0]), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Y"), ICON_NONE); + row->label(format_coefficient(size[1]), ICON_NONE); + + row = &col->row(true); + uiItemL_respect_property_split(row, IFACE_("Z"), ICON_NONE); + row->label(format_coefficient(size[2]), ICON_NONE); +} + +void uiTemplateMatrix(uiLayout *layout, PointerRNA *ptr, const StringRefNull propname) +{ + PropertyRNA *prop = RNA_struct_find_property(ptr, propname.c_str()); + + if (!prop || RNA_property_type(prop) != PROP_FLOAT || + RNA_property_subtype(prop) != PROP_MATRIX || RNA_property_array_length(ptr, prop) != 16) + { + RNA_warning("4x4 Matrix property not found: %s.%s", + RNA_struct_identifier(ptr->type), + propname.c_str()); + return; + } + draw_matrix_template(*layout, *ptr, *prop); +} diff --git a/source/blender/editors/object/object_constraint.cc b/source/blender/editors/object/object_constraint.cc index d53d83f415d..9a3a17f8d14 100644 --- a/source/blender/editors/object/object_constraint.cc +++ b/source/blender/editors/object/object_constraint.cc @@ -957,6 +957,34 @@ static wmOperatorStatus childof_clear_inverse_invoke(bContext *C, return OPERATOR_CANCELLED; } +static bool childof_clear_inverse_poll(bContext *C) +{ + if (!edit_constraint_liboverride_allowed_poll(C)) { + return false; + } + + PointerRNA ptr = CTX_data_pointer_get_type(C, "constraint", &RNA_Constraint); + bConstraint *con = static_cast(ptr.data); + + /* Allow workflows with unset context's constraint. + * The constraint can also be provided as an operator's property. */ + if (con == nullptr) { + return true; + } + + if (con->type != CONSTRAINT_TYPE_CHILDOF) { + return false; + } + + bChildOfConstraint *data = static_cast(con->data); + + if (is_identity_m4(data->invmat)) { + CTX_wm_operator_poll_msg_set(C, "No inverse correction is set, so there is nothing to clear"); + return false; + } + return true; +} + void CONSTRAINT_OT_childof_clear_inverse(wmOperatorType *ot) { /* identifiers */ @@ -967,7 +995,7 @@ void CONSTRAINT_OT_childof_clear_inverse(wmOperatorType *ot) /* callbacks */ ot->invoke = childof_clear_inverse_invoke; ot->exec = childof_clear_inverse_exec; - ot->poll = edit_constraint_liboverride_allowed_poll; + ot->poll = childof_clear_inverse_poll; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; @@ -1216,6 +1244,27 @@ static wmOperatorStatus objectsolver_clear_inverse_invoke(bContext *C, return OPERATOR_CANCELLED; } +static bool objectsolver_clear_inverse_poll(bContext *C) +{ + if (!edit_constraint_poll(C)) { + return false; + } + + PointerRNA ptr = CTX_data_pointer_get_type(C, "constraint", &RNA_Constraint); + bConstraint *con = static_cast(ptr.data); + if (con == nullptr) { + return true; + } + + bObjectSolverConstraint *data = (bObjectSolverConstraint *)con->data; + + if (is_identity_m4(data->invmat)) { + CTX_wm_operator_poll_msg_set(C, "No inverse correction is set, so there is nothing to clear"); + return false; + } + return true; +} + void CONSTRAINT_OT_objectsolver_clear_inverse(wmOperatorType *ot) { /* identifiers */ @@ -1226,7 +1275,7 @@ void CONSTRAINT_OT_objectsolver_clear_inverse(wmOperatorType *ot) /* callbacks */ ot->invoke = objectsolver_clear_inverse_invoke; ot->exec = objectsolver_clear_inverse_exec; - ot->poll = edit_constraint_poll; + ot->poll = objectsolver_clear_inverse_poll; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; diff --git a/source/blender/makesrna/intern/rna_ui_api.cc b/source/blender/makesrna/intern/rna_ui_api.cc index 073803344f0..2018b138b41 100644 --- a/source/blender/makesrna/intern/rna_ui_api.cc +++ b/source/blender/makesrna/intern/rna_ui_api.cc @@ -1697,6 +1697,15 @@ void RNA_api_ui_layout(StructRNA *srna) "Optionally limit the items which can be selected"); RNA_def_boolean(func, "hide_buttons", false, "", "Show only list, no buttons"); + func = RNA_def_function(srna, "template_matrix", "uiTemplateMatrix"); + RNA_def_function_ui_description( + func, + "Insert a readonly Matrix UI. " + "The UI displays the matrix components - translation, rotation and scale. " + "The **property** argument must be the identifier of an existing 4x4 float vector " + "property of subtype 'MATRIX'."); + api_ui_item_rna_common(func); + func = RNA_def_function(srna, "template_any_ID", "rna_uiTemplateAnyID"); parm = RNA_def_pointer(func, "data", "AnyType", "", "Data from which to take property"); RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED | PARM_RNAPTR);