UI: Improvements to Confirmation of Apply Modifier #117156

Merged
Harley Acheson merged 12 commits from Harley/blender:ConfirmApplyModifier into main 2024-02-24 01:38:42 +01:00
22 changed files with 995 additions and 747 deletions
Showing only changes of commit a9435f3bc5 - Show all commits

View File

@ -164,8 +164,6 @@ def draw_kmi(display_keymaps, kc, km, kmi, layout, level):
if km.is_modal:
sub.prop(kmi, "propvalue", text="")
else:
# One day...
# sub.prop_search(kmi, "idname", bpy.context.window_manager, "operators_all", text="")
sub.prop(kmi, "idname", text="")
if map_type not in {'TEXTINPUT', 'TIMER'}:

View File

@ -1246,6 +1246,14 @@ def km_outliner(params):
{"properties": [("extend_range", True), ("deselect_all", not params.legacy)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True, "shift": True},
{"properties": [("extend", True), ("extend_range", True), ("deselect_all", not params.legacy)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK'},
{"properties": [("recurse", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True},
{"properties": [("recurse", True), ("extend", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "shift": True},
{"properties": [("recurse", True), ("extend_range", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True, "shift": True},
{"properties": [("recurse", True), ("extend", True), ("extend_range", True), ("deselect_all", True)]}),
("outliner.select_box", {"type": 'B', "value": 'PRESS'}, None),
("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("tweak", True)]}),
("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True},

View File

@ -486,6 +486,14 @@ def km_outliner(params):
{"properties": [("extend", False), ("extend_range", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True, "shift": True},
{"properties": [("extend", True), ("extend_range", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK'},
{"properties": [("recurse", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True},
{"properties": [("recurse", True), ("extend", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "shift": True},
{"properties": [("recurse", True), ("extend_range", True), ("deselect_all", True)]}),
("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True, "shift": True},
{"properties": [("recurse", True), ("extend", True), ("extend_range", True), ("deselect_all", True)]}),
("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("tweak", True)]}),
("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True},
{"properties": [("tweak", True), ("mode", 'ADD')]}),

View File

@ -237,35 +237,8 @@ class TOPBAR_MT_file_cleanup(Menu):
def draw(self, _context):
layout = self.layout
layout.separator()
props = layout.operator("outliner.orphans_purge", text="Unused Data-Blocks")
props.do_local_ids = True
props.do_linked_ids = True
props.do_recursive = False
props = layout.operator("outliner.orphans_purge", text="Recursive Unused Data-Blocks")
props.do_local_ids = True
props.do_linked_ids = True
props.do_recursive = True
layout.separator()
props = layout.operator("outliner.orphans_purge", text="Unused Linked Data-Blocks")
props.do_local_ids = False
props.do_linked_ids = True
props.do_recursive = False
props = layout.operator("outliner.orphans_purge", text="Recursive Unused Linked Data-Blocks")
props.do_local_ids = False
props.do_linked_ids = True
props.do_recursive = True
layout.separator()
props = layout.operator("outliner.orphans_purge", text="Unused Local Data-Blocks")
props.do_local_ids = True
props.do_linked_ids = False
props.do_recursive = False
props = layout.operator("outliner.orphans_purge", text="Recursive Unused Local Data-Blocks")
props.do_local_ids = True
props.do_linked_ids = False
props.do_recursive = True
layout.operator("outliner.orphans_cleanup")
layout.operator("outliner.orphans_manage")
class TOPBAR_MT_file(Menu):

View File

@ -17,7 +17,7 @@ namespace blender::ed::curves {
/**
* Merges copy intervals at curve endings to minimize number of copy operations.
* For example above intervals [0, 3, 4, 4, 4] became [0, 4, 4].
* For example given in function 'extrude_curves' intervals [0, 3, 4, 4, 4] became [0, 4, 4].
* Leading to only two copy operations.
*/
static Span<int> compress_intervals(const Span<IndexRange> curve_interval_ranges,
@ -263,7 +263,8 @@ static void extrude_curves(Curves &curves_id)
Array<IndexRange> curve_interval_ranges(curves_num);
/* Per curve boolean indicating if first interval in a curve is selected.
* Other can be calculated as in a curve two adjacent intervals can have same selection state. */
* Other can be calculated as in a curve two adjacent intervals can not have same selection
* state. */
Array<bool> is_first_selected(curves_num);
calc_curves_extrusion(extruded_points,
@ -276,7 +277,11 @@ static void extrude_curves(Curves &curves_id)
new_curves.resize(new_offsets.last(), new_curves.curves_num());
const bke::AttributeAccessor src_attributes = curves.attributes();
const GVArraySpan src_selection = *src_attributes.lookup(".selection", bke::AttrDomain::Point);
GVArray src_selection_array = *src_attributes.lookup(".selection", bke::AttrDomain::Point);
if (!src_selection_array) {
src_selection_array = VArray<bool>::ForSingle(true, curves.points_num());
}
const GVArraySpan src_selection = src_selection_array;
const CPPType &src_selection_type = src_selection.type();
bke::GSpanAttributeWriter dst_selection = ensure_selection_attribute(
new_curves,

View File

@ -13,6 +13,8 @@
#include <cstddef> /* `offsetof()` */
#include <cstring>
#include <fmt/format.h>
#include "MEM_guardedalloc.h"
#include "DNA_object_types.h"
@ -466,7 +468,7 @@ void ui_block_bounds_calc(uiBlock *block)
/* hardcoded exception... but that one is annoying with larger safety */
uiBut *bt = static_cast<uiBut *>(block->buttons.first);
const int xof = ((bt && STRPREFIX(bt->str, "ERROR")) ? 10 : 40) * UI_SCALE_FAC;
const int xof = ((bt && STRPREFIX(bt->str.c_str(), "ERROR")) ? 10 : 40) * UI_SCALE_FAC;
block->safety.xmin = block->rect.xmin - xof;
block->safety.ymin = block->rect.ymin - xof;
@ -918,22 +920,7 @@ static void ui_but_update_old_active_from_new(uiBut *oldbut, uiBut *but)
/* move/copy string from the new button to the old */
/* needed for alt+mouse wheel over enums */
if (but->str != but->strdata) {
if (oldbut->str != oldbut->strdata) {
std::swap(but->str, oldbut->str);
}
else {
oldbut->str = but->str;
but->str = but->strdata;
}
}
else {
if (oldbut->str != oldbut->strdata) {
MEM_freeN(oldbut->str);
oldbut->str = oldbut->strdata;
}
STRNCPY(oldbut->strdata, but->strdata);
}
std::swap(but->str, oldbut->str);
if (but->dragpoin) {
std::swap(but->dragpoin, oldbut->dragpoin);
@ -1161,11 +1148,11 @@ static void ui_menu_block_set_keyaccels(uiBlock *block)
continue;
}
if (but->str == nullptr || but->str[0] == '\0') {
if (but->str.empty()) {
continue;
}
const char *str_pt = but->str;
const char *str_pt = but->str.c_str();
uchar menu_key;
do {
menu_key = tolower(*str_pt);
@ -1214,9 +1201,9 @@ static void ui_menu_block_set_keyaccels(uiBlock *block)
void ui_but_add_shortcut(uiBut *but, const char *shortcut_str, const bool do_strip)
{
if (do_strip && (but->flag & UI_BUT_HAS_SEP_CHAR)) {
char *cpoin = strrchr(but->str, UI_SEP_CHAR);
if (cpoin) {
*cpoin = '\0';
const size_t sep_index = but->str.find_first_of(UI_SEP_CHAR);
if (sep_index != std::string::npos) {
but->str = but->str.substr(0, sep_index);
}
but->flag &= ~UI_BUT_HAS_SEP_CHAR;
}
@ -1226,16 +1213,7 @@ void ui_but_add_shortcut(uiBut *but, const char *shortcut_str, const bool do_str
return;
}
char *butstr_orig;
if (but->str != but->strdata) {
butstr_orig = but->str; /* free after using as source buffer */
}
else {
butstr_orig = BLI_strdup(but->str);
}
SNPRINTF(but->strdata, "%s" UI_SEP_CHAR_S "%s", butstr_orig, shortcut_str);
MEM_freeN(butstr_orig);
but->str = but->strdata;
but->str = fmt::format("{}" UI_SEP_CHAR_S "{}", but->str, shortcut_str);
but->flag |= UI_BUT_HAS_SEP_CHAR;
ui_but_update(but);
}
@ -3138,27 +3116,7 @@ bool ui_but_string_eval_number(bContext *C, const uiBut *but, const char *str, d
static void ui_but_string_set_internal(uiBut *but, const char *str, size_t str_len)
{
BLI_assert(str_len == strlen(str));
BLI_assert(but->str == nullptr);
str_len += 1;
if (str_len > UI_MAX_NAME_STR) {
but->str = static_cast<char *>(MEM_mallocN(str_len, "ui_def_but str"));
}
else {
but->str = but->strdata;
}
memcpy(but->str, str, str_len);
}
static void ui_but_string_free_internal(uiBut *but)
{
if (but->str) {
if (but->str != but->strdata) {
MEM_freeN(but->str);
}
/* must call 'ui_but_string_set_internal' after */
but->str = nullptr;
}
but->str = std::string(str, str_len);
}
bool ui_but_string_set(bContext *C, uiBut *but, const char *str)
@ -3509,9 +3467,6 @@ static void ui_but_free(const bContext *C, uiBut *but)
}
}
}
if (but->str && but->str != but->strdata) {
MEM_freeN(but->str);
}
if ((but->type == UI_BTYPE_IMAGE) && but->poin) {
IMB_freeImBuf((ImBuf *)but->poin);
@ -3736,7 +3691,7 @@ void UI_block_set_search_only(uiBlock *block, bool search_only)
static void ui_but_build_drawstr_float(uiBut *but, double value)
{
size_t slen = 0;
STR_CONCAT(but->drawstr, slen, but->str);
STR_CONCAT(but->drawstr, slen, but->str.c_str());
PropertySubType subtype = PROP_NONE;
if (but->rnaprop) {
@ -3784,7 +3739,7 @@ static void ui_but_build_drawstr_float(uiBut *but, double value)
static void ui_but_build_drawstr_int(uiBut *but, int value)
{
size_t slen = 0;
STR_CONCAT(but->drawstr, slen, but->str);
STR_CONCAT(but->drawstr, slen, but->str.c_str());
PropertySubType subtype = PROP_NONE;
if (but->rnaprop) {
@ -3878,13 +3833,13 @@ static void ui_but_update_ex(uiBut *but, const bool validate)
&item))
{
const size_t slen = strlen(item.name);
ui_but_string_free_internal(but);
but->str.clear();
ui_but_string_set_internal(but, item.name, slen);
but->icon = item.icon;
}
}
}
STRNCPY(but->drawstr, but->str);
STRNCPY(but->drawstr, but->str.c_str());
}
break;
@ -3906,10 +3861,10 @@ static void ui_but_update_ex(uiBut *but, const bool validate)
if (ui_but_is_float(but)) {
UI_GET_BUT_VALUE_INIT(but, value);
const int prec = ui_but_calc_float_precision(but, value);
SNPRINTF(but->drawstr, "%s%.*f", but->str, prec, value);
SNPRINTF(but->drawstr, "%s%.*f", but->str.c_str(), prec, value);
}
else {
STRNCPY(but->drawstr, but->str);
STRNCPY(but->drawstr, but->str.c_str());
}
break;
@ -3920,7 +3875,7 @@ static void ui_but_update_ex(uiBut *but, const bool validate)
char str[UI_MAX_DRAW_STR];
ui_but_string_get(but, str, UI_MAX_DRAW_STR);
SNPRINTF(but->drawstr, "%s%s", but->str, str);
SNPRINTF(but->drawstr, "%s%s", but->str.c_str(), str);
}
break;
@ -3933,7 +3888,7 @@ static void ui_but_update_ex(uiBut *but, const bool validate)
UI_GET_BUT_VALUE_INIT(but, value);
str = WM_key_event_string(short(value), false);
}
SNPRINTF(but->drawstr, "%s%s", but->str, str);
SNPRINTF(but->drawstr, "%s%s", but->str.c_str(), str);
break;
}
case UI_BTYPE_HOTKEY_EVENT:
@ -3954,7 +3909,7 @@ static void ui_but_update_ex(uiBut *but, const bool validate)
}
}
else {
STRNCPY_UTF8(but->drawstr, but->str);
STRNCPY_UTF8(but->drawstr, but->str.c_str());
}
break;
@ -3963,7 +3918,7 @@ static void ui_but_update_ex(uiBut *but, const bool validate)
case UI_BTYPE_HSVCIRCLE:
break;
default:
STRNCPY(but->drawstr, but->str);
STRNCPY(but->drawstr, but->str.c_str());
break;
}
@ -4082,7 +4037,6 @@ uiBut *ui_but_change_type(uiBut *but, eButType new_type)
const uiBut *old_but_ptr = but;
/* Button may have pointer to a member within itself, this will have to be updated. */
const bool has_str_ptr_to_self = but->str == but->strdata;
const bool has_poin_ptr_to_self = but->poin == (char *)but;
/* Copy construct button with the new type. */
@ -4090,9 +4044,6 @@ uiBut *ui_but_change_type(uiBut *but, eButType new_type)
*but = *old_but_ptr;
/* We didn't mean to override this :) */
but->type = new_type;
if (has_str_ptr_to_self) {
but->str = but->strdata;
}
if (has_poin_ptr_to_self) {
but->poin = (char *)but;
}
@ -4201,18 +4152,16 @@ static uiBut *ui_def_but(uiBlock *block,
but->pos = -1; /* cursor invisible */
if (ELEM(but->type, UI_BTYPE_NUM, UI_BTYPE_NUM_SLIDER)) { /* add a space to name */
/* slen remains unchanged from previous assignment, ensure this stays true */
if (slen > 0 && slen < UI_MAX_NAME_STR - 2) {
if (but->str[slen - 1] != ' ') {
but->str[slen] = ' ';
but->str[slen + 1] = 0;
if (but->str[but->str.size() - 1] != ' ') {
but->str += ' ';
}
}
}
if (block->flag & UI_BLOCK_RADIAL) {
but->drawflag |= UI_BUT_TEXT_LEFT;
if (but->str && but->str[0]) {
if (!but->str.empty()) {
but->drawflag |= UI_BUT_ICON_LEFT;
}
}
@ -4296,7 +4245,7 @@ void ui_def_but_icon(uiBut *but, const int icon, const int flag)
but->icon = icon;
but->flag |= flag;
if (but->str && but->str[0]) {
if (!but->str.empty()) {
but->drawflag |= UI_BUT_ICON_LEFT;
}
}
@ -6628,28 +6577,24 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
va_start(args, but);
while ((si = (uiStringInfo *)va_arg(args, void *))) {
uiStringInfoType type = si->type;
char *tmp = nullptr;
std::optional<std::string> tmp;
if (type == BUT_GET_TIP_LABEL) {
if (but->tip_label_func) {
const std::string tooltip_label = but->tip_label_func(but);
tmp = BLI_strdupn(tooltip_label.c_str(), tooltip_label.size());
tmp = but->tip_label_func(but);
}
}
if (type == BUT_GET_LABEL) {
if (but->str && but->str[0]) {
const char *str_sep;
size_t str_len;
if ((but->flag & UI_BUT_HAS_SEP_CHAR) && (str_sep = strrchr(but->str, UI_SEP_CHAR))) {
str_len = (str_sep - but->str);
if (!but->str.empty()) {
size_t str_len = but->str.size();
if (but->flag & UI_BUT_HAS_SEP_CHAR) {
const size_t sep_index = but->str.find_first_of(UI_SEP_CHAR);
if (sep_index != std::string::npos) {
str_len = sep_index;
}
}
else {
str_len = strlen(but->str);
}
tmp = BLI_strdupn(but->str, str_len);
tmp = but->str.substr(0, str_len);
}
else {
type = BUT_GET_RNA_LABEL; /* Fail-safe solution... */
@ -6660,7 +6605,7 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
tmp = but->tip_func(C, but->tip_arg, but->tip);
}
else if (but->tip && but->tip[0]) {
tmp = BLI_strdup(but->tip);
tmp = but->tip;
}
else {
type = BUT_GET_RNA_TIP; /* Fail-safe solution... */
@ -6669,49 +6614,49 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
if (type == BUT_GET_RNAPROP_IDENTIFIER) {
if (but->rnaprop) {
tmp = BLI_strdup(RNA_property_identifier(but->rnaprop));
tmp = RNA_property_identifier(but->rnaprop);
}
}
else if (type == BUT_GET_RNASTRUCT_IDENTIFIER) {
if (but->rnaprop && but->rnapoin.data) {
tmp = BLI_strdup(RNA_struct_identifier(but->rnapoin.type));
tmp = RNA_struct_identifier(but->rnapoin.type);
}
else if (but->optype) {
tmp = BLI_strdup(but->optype->idname);
tmp = but->optype->idname;
}
else if (ELEM(but->type, UI_BTYPE_MENU, UI_BTYPE_PULLDOWN)) {
MenuType *mt = UI_but_menutype_get(but);
if (mt) {
tmp = BLI_strdup(mt->idname);
tmp = mt->idname;
}
}
else if (but->type == UI_BTYPE_POPOVER) {
PanelType *pt = UI_but_paneltype_get(but);
if (pt) {
tmp = BLI_strdup(pt->idname);
tmp = pt->idname;
}
}
}
else if (ELEM(type, BUT_GET_RNA_LABEL, BUT_GET_RNA_TIP)) {
if (but->rnaprop) {
if (type == BUT_GET_RNA_LABEL) {
tmp = BLI_strdup(RNA_property_ui_name(but->rnaprop));
tmp = RNA_property_ui_name(but->rnaprop);
}
else {
const char *t = RNA_property_ui_description(but->rnaprop);
if (t && t[0]) {
tmp = BLI_strdup(t);
tmp = t;
}
}
}
else if (but->optype) {
if (type == BUT_GET_RNA_LABEL) {
tmp = BLI_strdup(WM_operatortype_name(but->optype, opptr).c_str());
tmp = WM_operatortype_name(but->optype, opptr).c_str();
}
else {
const bContextStore *previous_ctx = CTX_store_get(C);
CTX_store_set(C, but->context);
tmp = BLI_strdup(WM_operatortype_description(C, but->optype, opptr).c_str());
tmp = WM_operatortype_description(C, but->optype, opptr).c_str();
CTX_store_set(C, previous_ctx);
}
}
@ -6720,37 +6665,37 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
MenuType *mt = UI_but_menutype_get(but);
if (mt) {
if (type == BUT_GET_RNA_LABEL) {
tmp = BLI_strdup(CTX_TIP_(mt->translation_context, mt->label));
tmp = CTX_TIP_(mt->translation_context, mt->label);
}
else {
/* Not all menus are from Python. */
if (mt->rna_ext.srna) {
const char *t = RNA_struct_ui_description(mt->rna_ext.srna);
if (t && t[0]) {
tmp = BLI_strdup(t);
tmp = t;
}
}
}
}
}
if (tmp == nullptr) {
if (!tmp) {
wmOperatorType *ot = UI_but_operatortype_get_from_enum_menu(but, nullptr);
if (ot) {
if (type == BUT_GET_RNA_LABEL) {
tmp = BLI_strdup(WM_operatortype_name(ot, nullptr).c_str());
tmp = WM_operatortype_name(ot, nullptr).c_str();
}
else {
tmp = BLI_strdup(WM_operatortype_description(C, ot, nullptr).c_str());
tmp = WM_operatortype_description(C, ot, nullptr).c_str();
}
}
}
if (tmp == nullptr) {
if (!tmp) {
PanelType *pt = UI_but_paneltype_get(but);
if (pt) {
if (type == BUT_GET_RNA_LABEL) {
tmp = BLI_strdup(CTX_TIP_(pt->translation_context, pt->label));
tmp = CTX_TIP_(pt->translation_context, pt->label);
}
else {
/* Not all panels are from Python. */
@ -6779,7 +6724,7 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
if (BLT_is_default_context(_tmp)) {
_tmp = BLT_I18NCONTEXT_DEFAULT_BPYRNA;
}
tmp = BLI_strdup(_tmp);
tmp = _tmp;
}
else if (ELEM(type, BUT_GET_RNAENUM_IDENTIFIER, BUT_GET_RNAENUM_LABEL, BUT_GET_RNAENUM_TIP)) {
PointerRNA *ptr = nullptr;
@ -6828,13 +6773,13 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
}
if (item && item->identifier) {
if (type == BUT_GET_RNAENUM_IDENTIFIER) {
tmp = BLI_strdup(item->identifier);
tmp = item->identifier;
}
else if (type == BUT_GET_RNAENUM_LABEL) {
tmp = BLI_strdup(item->name);
tmp = item->name;
}
else if (item->description && item->description[0]) {
tmp = BLI_strdup(item->description);
tmp = item->description;
}
}
}
@ -6846,7 +6791,7 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
if (!(ui_block_is_menu(but->block) && !ui_block_is_pie_menu(but->block))) {
char buf[128];
if (ui_but_event_operator_string(C, but, buf, sizeof(buf))) {
tmp = BLI_strdup(buf);
tmp = buf;
}
}
}
@ -6854,12 +6799,12 @@ void UI_but_string_info_get(bContext *C, uiBut *but, ...)
if (!(ui_block_is_menu(but->block) && !ui_block_is_pie_menu(but->block))) {
char buf[128];
if (ui_but_event_property_operator_string(C, but, buf, sizeof(buf))) {
tmp = BLI_strdup(buf);
tmp = buf;
}
}
}
si->strinfo = tmp;
si->strinfo = tmp ? BLI_strdupn(tmp->c_str(), tmp->size()) : nullptr;
}
va_end(args);
@ -6878,19 +6823,19 @@ void UI_but_extra_icon_string_info_get(bContext *C, uiButExtraOpIcon *extra_icon
va_start(args, extra_icon);
while ((si = (uiStringInfo *)va_arg(args, void *))) {
char *tmp = nullptr;
std::string tmp;
switch (si->type) {
case BUT_GET_LABEL:
tmp = BLI_strdup(WM_operatortype_name(optype, opptr).c_str());
tmp = WM_operatortype_name(optype, opptr);
break;
case BUT_GET_TIP:
tmp = BLI_strdup(WM_operatortype_description(C, optype, opptr).c_str());
tmp = WM_operatortype_description(C, optype, opptr);
break;
case BUT_GET_OP_KEYMAP: {
char buf[128];
if (ui_but_extra_icon_event_operator_string(C, extra_icon, buf, sizeof(buf))) {
tmp = BLI_strdup(buf);
tmp = buf;
}
break;
}
@ -6900,7 +6845,7 @@ void UI_but_extra_icon_string_info_get(bContext *C, uiButExtraOpIcon *extra_icon
break;
}
si->strinfo = tmp;
si->strinfo = BLI_strdupn(tmp.c_str(), tmp.size());
}
va_end(args);
}

View File

@ -178,8 +178,8 @@ struct uiBut {
short bit = 0, bitnr = 0, retval = 0, strwidth = 0, alignnr = 0;
short ofs = 0, pos = 0, selsta = 0, selend = 0;
char *str = nullptr;
char strdata[UI_MAX_NAME_STR] = "";
std::string str;
char drawstr[UI_MAX_DRAW_STR] = "";
char *placeholder = nullptr;

View File

@ -2536,7 +2536,7 @@ void uiItemFullR(uiLayout *layout,
/* ensure text isn't added to icon_only buttons */
if (but && icon_only) {
BLI_assert(but->str[0] == '\0');
BLI_assert(but->str.empty());
}
}
@ -2863,7 +2863,7 @@ uiBut *ui_but_add_search(uiBut *but,
if (RNA_property_type(prop) == PROP_ENUM) {
/* XXX, this will have a menu string,
* but in this case we just want the text */
but->str[0] = 0;
but->str.clear();
}
UI_but_func_search_set_results_are_suggestions(but, results_are_suggestions);
@ -3602,7 +3602,7 @@ static int menu_item_enum_opname_menu_active(bContext *C, uiBut *but, MenuItemLe
WM_operator_properties_sanitize(&ptr, false);
PropertyRNA *prop = RNA_struct_find_property(&ptr, lvl->propname);
RNA_property_enum_items_gettexted(C, &ptr, prop, &item_array, &totitem, &free);
int active = RNA_enum_from_name(item_array, but->str);
int active = RNA_enum_from_name(item_array, but->str.c_str());
if (free) {
MEM_freeN((void *)item_array);
}
@ -5405,7 +5405,7 @@ static bool block_search_panel_label_matches(const uiBlock *block, const char *s
static bool button_matches_search_filter(uiBut *but, const char *search_filter)
{
/* Do the shorter checks first for better performance in case there is a match. */
if (BLI_strcasestr(but->str, search_filter)) {
if (BLI_strcasestr(but->str.c_str(), search_filter)) {
return true;
}
@ -5891,7 +5891,7 @@ void ui_layout_add_but(uiLayout *layout, uiBut *but)
ui_item_size((uiItem *)bitem, &w, &h);
/* XXX uiBut hasn't scaled yet
* we can flag the button as not expandable, depending on its size */
if (w <= 2 * UI_UNIT_X && (!but->str || but->str[0] == '\0')) {
if (w <= 2 * UI_UNIT_X && but->str.empty()) {
bitem->item.flag |= UI_ITEM_FIXED_SIZE;
}
@ -6166,7 +6166,7 @@ static bool ui_layout_has_panel_label(const uiLayout *layout, const PanelType *p
if (subitem->type == ITEM_BUTTON) {
uiButtonItem *bitem = (uiButtonItem *)subitem;
if (!(bitem->but->flag & UI_HIDDEN) &&
STREQ(bitem->but->str, CTX_IFACE_(pt->translation_context, pt->label)))
STREQ(bitem->but->str.c_str(), CTX_IFACE_(pt->translation_context, pt->label)))
{
return true;
}

View File

@ -199,7 +199,7 @@ static void ui_update_color_picker_buts_rgb(uiBut *from_but,
* push, so disable it on RNA buttons in the color picker block */
UI_but_flag_disable(bt, UI_BUT_UNDO);
}
else if (STREQ(bt->str, "Hex:")) {
else if (bt->str == "Hex:") {
float rgb_hex[3];
uchar rgb_hex_uchar[3];
char col[16];

View File

@ -180,7 +180,7 @@ uiPieMenu *UI_pie_menu_begin(bContext *C, const char *title, int icon, const wmE
}
/* do not align left */
but->drawflag &= ~UI_BUT_TEXT_LEFT;
pie->block_radial->pie_data.title = but->str;
pie->block_radial->pie_data.title = but->str.c_str();
pie->block_radial->pie_data.icon = icon;
}

View File

@ -71,7 +71,7 @@ int ui_but_menu_step(uiBut *but, int direction)
direction);
}
printf("%s: cannot cycle button '%s'\n", __func__, but->str);
printf("%s: cannot cycle button '%s'\n", __func__, but->str.c_str());
return 0;
}
@ -124,7 +124,7 @@ static uiBut *ui_popup_menu_memory__internal(uiBlock *block, uiBut *but)
if (but) {
/* set */
mem[hash_mod] = ui_popup_string_hash(but->str, but->flag & UI_BUT_HAS_SEP_CHAR);
mem[hash_mod] = ui_popup_string_hash(but->str.c_str(), but->flag & UI_BUT_HAS_SEP_CHAR);
return nullptr;
}
@ -136,7 +136,8 @@ static uiBut *ui_popup_menu_memory__internal(uiBlock *block, uiBut *but)
if (ELEM(but_iter->type, UI_BTYPE_LABEL, UI_BTYPE_SEPR, UI_BTYPE_SEPR_LINE)) {
continue;
}
if (mem[hash_mod] == ui_popup_string_hash(but_iter->str, but_iter->flag & UI_BUT_HAS_SEP_CHAR))
if (mem[hash_mod] ==
ui_popup_string_hash(but_iter->str.c_str(), but_iter->flag & UI_BUT_HAS_SEP_CHAR))
{
return but_iter;
}

View File

@ -298,7 +298,6 @@ static bool menu_items_to_ui_button(MenuSearch_Item *item, uiBut *but)
}
but->icon = item->icon;
but->str = but->strdata;
}
return changed;

View File

@ -1342,7 +1342,7 @@ static void widget_draw_icon(
if (but->drawflag & UI_BUT_ICON_LEFT) {
/* special case - icon_only pie buttons */
if (ui_block_is_pie_menu(but->block) && !ELEM(but->type, UI_BTYPE_MENU, UI_BTYPE_POPOVER) &&
but->str && but->str[0] == '\0')
but->str.empty())
{
xs = rect->xmin + 2.0f * ofs;
}
@ -4960,7 +4960,8 @@ void ui_draw_but(const bContext *C, ARegion *region, uiStyle *style, uiBut *but,
/* We could use a flag for this, but for now just check size,
* add up/down arrows if there is room. */
if ((!but->str[0] && but->icon && (BLI_rcti_size_x(rect) < BLI_rcti_size_y(rect) + 2)) ||
if ((but->str.empty() && but->icon &&
(BLI_rcti_size_x(rect) < BLI_rcti_size_y(rect) + 2)) ||
/* disable for brushes also */
(but->flag & UI_BUT_ICON_PREVIEW))
{

View File

@ -12,6 +12,7 @@ set(INC
../../makesrna
../../sequencer
../../windowmanager
../../../../extern/fmtlib/include
# RNA_prototypes.h
${CMAKE_BINARY_DIR}/source/blender/makesrna
@ -137,6 +138,7 @@ set(LIB
bf_editor_undo
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
extern_fmtlib
)

View File

@ -65,6 +65,10 @@
#include "tree/tree_element_rna.hh"
#include "tree/tree_iterator.hh"
#include "wm_window.hh"
#include <fmt/format.h>
using namespace blender::ed::outliner;
namespace blender::ed::outliner {
@ -413,7 +417,7 @@ static int outliner_item_rename_invoke(bContext *C, wmOperator *op, const wmEven
TreeElement *te = use_active ? outliner_item_rename_find_active(space_outliner, op->reports) :
outliner_item_rename_find_hovered(space_outliner, region, event);
if (!te) {
return OPERATOR_CANCELLED;
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
}
/* Force element into view. */
@ -2140,7 +2144,7 @@ static int outliner_orphans_purge_invoke(bContext *C, wmOperator *op, const wmEv
RNA_int_set(op->ptr, "num_deleted", num_tagged[INDEX_ID_NULL]);
if (num_tagged[INDEX_ID_NULL] == 0) {
BKE_report(op->reports, RPT_INFO, "No orphaned data-blocks to purge");
BKE_report(op->reports, RPT_INFO, "No unused data-blocks to purge");
return OPERATOR_CANCELLED;
}
@ -2188,7 +2192,7 @@ static int outliner_orphans_purge_exec(bContext *C, wmOperator *op)
bmain, LIB_TAG_DOIT, do_local_ids, do_linked_ids, do_recursive_cleanup, num_tagged);
if (num_tagged[INDEX_ID_NULL] == 0) {
BKE_report(op->reports, RPT_INFO, "No orphaned data-blocks to purge");
BKE_report(op->reports, RPT_INFO, "No unused data-blocks to purge");
return OPERATOR_CANCELLED;
}
}
@ -2219,7 +2223,7 @@ void OUTLINER_OT_orphans_purge(wmOperatorType *ot)
/* identifiers */
ot->idname = "OUTLINER_OT_orphans_purge";
ot->name = "Purge All";
ot->description = "Clear all orphaned data-blocks without any users from the file";
ot->description = "Remove all unused data-blocks without any users from the file";
/* callbacks */
ot->invoke = outliner_orphans_purge_invoke;
@ -2248,10 +2252,267 @@ void OUTLINER_OT_orphans_purge(wmOperatorType *ot)
"do_recursive",
false,
"Recursive Delete",
"Recursively check for indirectly unused data-blocks, ensuring that no orphaned "
"Recursively check for indirectly unused data-blocks, ensuring that no unused "
"data-blocks remain after execution");
}
static void wm_block_orphans_cancel(bContext *C, void *arg_block, void * /*arg_data*/)
{
UI_popup_block_close(C, CTX_wm_window(C), (uiBlock *)arg_block);
}
static void wm_block_orphans_cancel_button(uiBlock *block)
{
uiBut *but = uiDefIconTextBut(
block, UI_BTYPE_BUT, 0, 0, IFACE_("Cancel"), 0, 0, 0, UI_UNIT_Y, 0, 0, 0, 0, 0, "");
UI_but_func_set(but, wm_block_orphans_cancel, block, nullptr);
UI_but_drawflag_disable(but, UI_BUT_TEXT_LEFT);
UI_but_flag_enable(but, UI_BUT_ACTIVE_DEFAULT);
}
struct OrphansPurgeData {
wmOperator *op = nullptr;
char local = true;
char linked = false;
char recursive = false;
};
static void wm_block_orphans_purge(bContext *C, void *arg_block, void *arg_data)
{
OrphansPurgeData *purge_data = (OrphansPurgeData *)arg_data;
wmOperator *op = purge_data->op;
Main *bmain = CTX_data_main(C);
int num_tagged[INDEX_ID_MAX] = {0};
/* Tag all IDs to delete. */
BKE_lib_query_unused_ids_tag(bmain,
LIB_TAG_DOIT,
purge_data->local,
purge_data->linked,
purge_data->recursive,
num_tagged);
if (num_tagged[INDEX_ID_NULL] == 0) {
BKE_report(op->reports, RPT_INFO, "No unused data to remove");
}
else {
BKE_id_multi_tagged_delete(bmain);
BKE_reportf(op->reports, RPT_INFO, "Deleted %d data block(s)", num_tagged[INDEX_ID_NULL]);
DEG_relations_tag_update(bmain);
WM_event_add_notifier(C, NC_ID | NA_REMOVED, nullptr);
/* Force full redraw of the UI. */
WM_main_add_notifier(NC_WINDOW, nullptr);
}
UI_popup_block_close(C, CTX_wm_window(C), (uiBlock *)arg_block);
}
static void wm_block_orphans_purge_button(uiBlock *block, OrphansPurgeData *purge_data)
{
uiBut *but = uiDefIconTextBut(
block, UI_BTYPE_BUT, 0, 0, IFACE_("Delete"), 0, 0, 0, UI_UNIT_Y, 0, 0, 0, 0, 0, "");
UI_but_func_set(but, wm_block_orphans_purge, block, purge_data);
UI_but_drawflag_disable(but, UI_BUT_TEXT_LEFT);
}
static std::string orphan_desc(const bContext *C, const bool local, bool linked, bool recursive)
{
Main *bmain = CTX_data_main(C);
int num_tagged[INDEX_ID_MAX] = {0};
std::string desc;
BKE_lib_query_unused_ids_tag(bmain, LIB_TAG_DOIT, local, linked, recursive, num_tagged);
bool is_first = true;
for (int i = 0; i < INDEX_ID_MAX - 2; i++) {
if (num_tagged[i] != 0) {
desc += fmt::format(
"{}{} {}",
(is_first) ? "" : ", ",
num_tagged[i],
(num_tagged[i] > 1) ?
TIP_(BKE_idtype_idcode_to_name_plural(BKE_idtype_idcode_from_index(i))) :
TIP_(BKE_idtype_idcode_to_name(BKE_idtype_idcode_from_index(i))));
is_first = false;
}
}
if (desc.empty()) {
desc = "Nothing";
}
return desc;
}
static uiBlock *wm_block_create_orphans_cleanup(bContext *C, ARegion *region, void *arg)
{
OrphansPurgeData *purge_data = static_cast<OrphansPurgeData *>(arg);
uiBlock *block = UI_block_begin(C, region, "orphans_remove_popup", UI_EMBOSS);
UI_block_flag_enable(
block, UI_BLOCK_KEEP_OPEN | UI_BLOCK_LOOP | UI_BLOCK_NO_WIN_CLIP | UI_BLOCK_NUMSELECT);
UI_block_theme_style_set(block, UI_BLOCK_THEME_STYLE_POPUP);
uiLayout *layout = uiItemsAlertBox(block, 40, ALERT_ICON_WARNING);
/* Title. */
uiItemL_ex(layout, TIP_("Purge Unused Data From This File"), 0, true, false);
uiItemS(layout);
std::string desc = "Local data: " + orphan_desc(C, true, false, false);
uiDefButBitC(block,
UI_BTYPE_CHECKBOX,
1,
0,
desc.c_str(),
0,
0,
0,
UI_UNIT_Y,
&purge_data->local,
0,
0,
0,
0,
"Delete unused local data");
desc = "Linked data: " + orphan_desc(C, false, true, false);
uiDefButBitC(block,
UI_BTYPE_CHECKBOX,
1,
0,
desc.c_str(),
0,
0,
0,
UI_UNIT_Y,
&purge_data->linked,
0,
0,
0,
0,
"Delete unused linked data");
uiDefButBitC(
block,
UI_BTYPE_CHECKBOX,
1,
0,
"Include indirect data",
0,
0,
0,
UI_UNIT_Y,
&purge_data->recursive,
0,
0,
0,
0,
"Recursively check for indirectly unused data, ensuring that no unused data remains");
uiItemS_ex(layout, 2.0f);
/* Buttons. */
#ifdef _WIN32
const bool windows_layout = true;
#else
const bool windows_layout = false;
#endif
uiLayout *split = uiLayoutSplit(layout, 0.0f, true);
uiLayoutSetScaleY(split, 1.2f);
if (windows_layout) {
/* Windows standard layout. */
uiLayoutColumn(split, false);
wm_block_orphans_purge_button(block, purge_data);
uiLayoutColumn(split, false);
wm_block_orphans_cancel_button(block);
}
else {
/* Non-Windows layout (macOS and Linux). */
uiLayoutColumn(split, false);
wm_block_orphans_cancel_button(block);
uiLayoutColumn(split, false);
wm_block_orphans_purge_button(block, purge_data);
}
UI_block_bounds_set_centered(block, 14 * UI_SCALE_FAC);
return block;
}
static int outliner_orphans_cleanup_exec(bContext *C, wmOperator *op)
{
OrphansPurgeData *purge_data = MEM_new<OrphansPurgeData>(__func__);
purge_data->op = op;
UI_popup_block_invoke(C, wm_block_create_orphans_cleanup, purge_data, MEM_freeN);
return OPERATOR_FINISHED;
}
void OUTLINER_OT_orphans_cleanup(wmOperatorType *ot)
{
/* identifiers */
ot->idname = "OUTLINER_OT_orphans_cleanup";
ot->name = "Purge Unused Data...";
ot->description = "Remove unused data from this file";
/* callbacks */
ot->exec = outliner_orphans_cleanup_exec;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
static int outliner_orphans_manage_exec(bContext *C, wmOperator * /*op*/)
{
int width = 800;
int height = 500;
if (wm_get_screensize(&width, &height)) {
width /= 2;
height /= 2;
}
const rcti window_rect = {
/*xmin*/ 0,
/*xmax*/ width,
/*ymin*/ 0,
/*ymax*/ height,
};
if (WM_window_open(C,
IFACE_("Manage Unused Data"),
&window_rect,
SPACE_OUTLINER,
false,
false,
true,
WIN_ALIGN_PARENT_CENTER,
nullptr,
nullptr) != nullptr)
{
SpaceOutliner *soutline = (SpaceOutliner *)CTX_wm_area(C)->spacedata.first;
soutline->outlinevis = SO_ID_ORPHANS;
return OPERATOR_FINISHED;
}
return OPERATOR_CANCELLED;
}
void OUTLINER_OT_orphans_manage(wmOperatorType *ot)
{
/* identifiers */
ot->idname = "OUTLINER_OT_orphans_manage";
ot->name = "Manage Unused Data...";
ot->description = "Open a window to manage unused data";
/* callbacks */
ot->exec = outliner_orphans_manage_exec;
/* flags */
ot->flag = OPTYPE_REGISTER;
}
/** \} */
} // namespace blender::ed::outliner

View File

@ -511,7 +511,9 @@ void OUTLINER_OT_keyingset_remove_selected(wmOperatorType *ot);
void OUTLINER_OT_drivers_add_selected(wmOperatorType *ot);
void OUTLINER_OT_drivers_delete_selected(wmOperatorType *ot);
void OUTLINER_OT_orphans_cleanup(wmOperatorType *ot);
void OUTLINER_OT_orphans_purge(wmOperatorType *ot);
void OUTLINER_OT_orphans_manage(wmOperatorType *ot);
/* `outliner_query.cc` */

View File

@ -60,6 +60,8 @@ void outliner_operatortypes()
WM_operatortype_append(OUTLINER_OT_drivers_delete_selected);
WM_operatortype_append(OUTLINER_OT_orphans_purge);
WM_operatortype_append(OUTLINER_OT_orphans_cleanup);
WM_operatortype_append(OUTLINER_OT_orphans_manage);
WM_operatortype_append(OUTLINER_OT_parent_drop);
WM_operatortype_append(OUTLINER_OT_parent_clear);

View File

@ -1569,30 +1569,84 @@ void outliner_item_select(bContext *C,
}
}
static bool can_select_recursive(TreeElement *te, Collection *in_collection)
{
if (te->store_elem->type == TSE_LAYER_COLLECTION) {
return true;
}
if (te->store_elem->type == TSE_SOME_ID && te->idcode == ID_OB) {
/* Only actually select the object if
* 1. We are not restricted to any collection, or
* 2. The object is in fact in the given collection. */
if (!in_collection || BKE_collection_has_object_recursive(
in_collection, reinterpret_cast<Object *>(te->store_elem->id)))
{
return true;
}
}
return false;
}
static void do_outliner_select_recursive(ListBase *lb, bool selecting, Collection *in_collection)
{
LISTBASE_FOREACH (TreeElement *, te, lb) {
TreeStoreElem *tselem = TREESTORE(te);
/* Recursive selection only on collections or objects. */
if (can_select_recursive(te, in_collection)) {
tselem->flag = selecting ? (tselem->flag | TSE_SELECTED) : (tselem->flag & ~TSE_SELECTED);
if (tselem->type == TSE_LAYER_COLLECTION) {
/* Restrict sub-tree selections to this collection. This prevents undesirable behavior in
* the edge-case where there is an object which is part of this collection, but which has
* children that are part of another collection. */
do_outliner_select_recursive(
&te->subtree, selecting, static_cast<LayerCollection *>(te->directdata)->collection);
}
else {
do_outliner_select_recursive(&te->subtree, selecting, in_collection);
}
}
else {
tselem->flag &= ~TSE_SELECTED;
}
}
}
static bool do_outliner_range_select_recursive(ListBase *lb,
TreeElement *active,
TreeElement *cursor,
bool selecting)
bool selecting,
const bool recurse,
Collection *in_collection)
{
LISTBASE_FOREACH (TreeElement *, te, lb) {
TreeStoreElem *tselem = TREESTORE(te);
if (selecting) {
tselem->flag |= TSE_SELECTED;
}
bool can_select = !recurse || can_select_recursive(te, in_collection);
/* Remember if we are selecting before we potentially change the selecting state. */
bool selecting_before = selecting;
/* Set state for selection */
if (ELEM(te, active, cursor)) {
selecting = !selecting;
}
if (selecting) {
if (can_select && (selecting_before || selecting)) {
tselem->flag |= TSE_SELECTED;
}
/* Don't look inside closed elements */
if (!(tselem->flag & TSE_CLOSED)) {
selecting = do_outliner_range_select_recursive(&te->subtree, active, cursor, selecting);
/* Don't look inside closed elements, unless we're forcing the recursion all the way down. */
if (!(tselem->flag & TSE_CLOSED) || recurse) {
/* If this tree element is a collection, then it sets
* the precedent for inclusion of its subobjects. */
Collection *child_collection = in_collection;
if (tselem->type == TSE_LAYER_COLLECTION) {
child_collection = static_cast<LayerCollection *>(te->directdata)->collection;
}
selecting = do_outliner_range_select_recursive(
&te->subtree, active, cursor, selecting, recurse, child_collection);
}
}
@ -1603,7 +1657,9 @@ static bool do_outliner_range_select_recursive(ListBase *lb,
static void do_outliner_range_select(bContext *C,
SpaceOutliner *space_outliner,
TreeElement *cursor,
const bool extend)
const bool extend,
const bool recurse,
Collection *in_collection)
{
TreeElement *active = outliner_find_element_with_flag(&space_outliner->tree, TSE_ACTIVE);
@ -1632,7 +1688,8 @@ static void do_outliner_range_select(bContext *C,
return;
}
do_outliner_range_select_recursive(&space_outliner->tree, active, cursor, false);
do_outliner_range_select_recursive(
&space_outliner->tree, active, cursor, false, recurse, in_collection);
}
static bool outliner_is_co_within_restrict_columns(const SpaceOutliner *space_outliner,
@ -1673,7 +1730,8 @@ static int outliner_item_do_activate_from_cursor(bContext *C,
const int mval[2],
const bool extend,
const bool use_range,
const bool deselect_all)
const bool deselect_all,
const bool recurse)
{
ARegion *region = CTX_wm_region(C);
SpaceOutliner *space_outliner = CTX_wm_space_outliner(C);
@ -1716,21 +1774,73 @@ static int outliner_item_do_activate_from_cursor(bContext *C,
TreeStoreElem *activate_tselem = TREESTORE(activate_te);
/* If we're recursing, we need to know the collection of the selected item in order
* to prevent selecting across collection boundaries. (Object hierarchies might cross
* collection boundaries, i.e., children may be in different collections from their
* parents.) */
Collection *parent_collection = nullptr;
if (recurse) {
if (activate_tselem->type == TSE_LAYER_COLLECTION) {
parent_collection = static_cast<LayerCollection *>(activate_te->directdata)->collection;
}
else if (activate_tselem->type == TSE_SOME_ID && activate_te->idcode == ID_OB) {
parent_collection = BKE_collection_object_find(
CTX_data_main(C),
CTX_data_scene(C),
nullptr,
reinterpret_cast<Object *>(activate_tselem->id));
}
}
/* If we're not recursing (not double clicking), and we are extending or range selecting by
* holding CTRL or SHIFT, ignore events when the cursor is over the icon. This disambiguates
* the case where we are recursing *and* holding CTRL or SHIFT in order to extend or range
* select recursively. */
if (!recurse && (extend || use_range) &&
outliner_item_is_co_over_icon(activate_te, view_mval[0]))
{
return OPERATOR_CANCELLED;
}
if (use_range) {
do_outliner_range_select(C, space_outliner, activate_te, extend);
do_outliner_range_select(C, space_outliner, activate_te, extend, recurse, parent_collection);
if (recurse) {
do_outliner_select_recursive(&activate_te->subtree, true, parent_collection);
}
}
else {
const bool is_over_name_icons = outliner_item_is_co_over_name_icons(activate_te,
view_mval[0]);
/* Always select unless already active and selected */
const bool select = !extend || !(activate_tselem->flag & TSE_ACTIVE &&
activate_tselem->flag & TSE_SELECTED);
/* Always select unless already active and selected. */
bool select = !extend || !(activate_tselem->flag & TSE_ACTIVE) ||
!(activate_tselem->flag & TSE_SELECTED);
/* If we're CTRL+double-clicking and the element is aleady
* selected, skip the activation and go straight to deselection. */
if (extend && recurse && activate_tselem->flag & TSE_SELECTED) {
select = false;
}
const short select_flag = OL_ITEM_ACTIVATE | (select ? OL_ITEM_SELECT : OL_ITEM_DESELECT) |
(is_over_name_icons ? OL_ITEM_SELECT_DATA : 0) |
(extend ? OL_ITEM_EXTEND : 0);
outliner_item_select(C, space_outliner, activate_te, select_flag);
/* The recurse flag is set when the user double-clicks
* to select everything in a collection or hierarchy. */
if (recurse) {
if (outliner_item_is_co_over_icon(activate_te, view_mval[0])) {
/* Select or deselect object hierarchy recursively. */
outliner_item_select(C, space_outliner, activate_te, select_flag);
do_outliner_select_recursive(&activate_te->subtree, select, parent_collection);
}
else {
/* Double-clicked, but it wasn't on the icon. */
return OPERATOR_CANCELLED;
}
}
else {
outliner_item_select(C, space_outliner, activate_te, select_flag);
}
/* Only switch properties editor tabs when icons are selected. */
if (is_over_icon) {
@ -1765,10 +1875,11 @@ static int outliner_item_activate_invoke(bContext *C, wmOperator *op, const wmEv
const bool extend = RNA_boolean_get(op->ptr, "extend");
const bool use_range = RNA_boolean_get(op->ptr, "extend_range");
const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all");
const bool recurse = RNA_boolean_get(op->ptr, "recurse");
int mval[2];
WM_event_drag_start_mval(event, region, mval);
return outliner_item_do_activate_from_cursor(C, mval, extend, use_range, deselect_all);
return outliner_item_do_activate_from_cursor(C, mval, extend, use_range, deselect_all, recurse);
}
void OUTLINER_OT_item_activate(wmOperatorType *ot)
@ -1796,6 +1907,10 @@ void OUTLINER_OT_item_activate(wmOperatorType *ot)
"Deselect On Nothing",
"Deselect all when nothing under the cursor");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
prop = RNA_def_boolean(
ot->srna, "recurse", false, "Recurse", "Select objects recursively from active element");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */

View File

@ -340,7 +340,7 @@ void VIEW3D_OT_render_border(wmOperatorType *ot)
ot->modal = WM_gesture_box_modal;
ot->cancel = WM_gesture_box_cancel;
ot->poll = ED_operator_view3d_active;
ot->poll = ED_operator_region_view3d_active;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

View File

@ -6,7 +6,6 @@
* \ingroup imbuf
*/
#include <array>
#include <type_traits>
#include "BLI_math_color_blend.h"
@ -22,73 +21,39 @@
namespace blender::imbuf::transform {
struct TransformUserData {
/** \brief Source image buffer to read from. */
struct TransformContext {
const ImBuf *src;
/** \brief Destination image buffer to write to. */
ImBuf *dst;
/** \brief UV coordinates at the origin (0,0) in source image space. */
eIMBTransformMode mode;
/* UV coordinates at the destination origin (0,0) in source image space. */
float2 start_uv;
/**
* \brief delta UV coordinates along the source image buffer, when moving a single pixel in the X
* axis of the dst image buffer.
*/
/* Source UV step delta, when moving along one destination pixel in X axis. */
float2 add_x;
/**
* \brief delta UV coordinate along the source image buffer, when moving a single pixel in the Y
* axes of the dst image buffer.
*/
/* Source UV step delta, when moving along one destination pixel in Y axis. */
float2 add_y;
struct {
/**
* Contains per sub-sample a delta to be added to the uv of the source image buffer.
*/
Vector<float2, 9> delta_uvs;
} subsampling;
/* Per-subsample source image delta UVs. */
Vector<float2, 9> subsampling_deltas;
struct {
IndexRange x_range;
IndexRange y_range;
} destination_region;
IndexRange dst_region_x_range;
IndexRange dst_region_y_range;
/**
* \brief Cropping region in source image pixel space.
*/
/* Cropping region in source image pixel space. */
rctf src_crop;
/**
* \brief Initialize the start_uv, add_x and add_y fields based on the given transform matrix.
*/
void init(const float4x4 &transform_matrix,
const int num_subsamples,
const bool do_crop_destination_region)
void init(const float4x4 &transform_matrix, const int num_subsamples, const bool has_source_crop)
{
init_start_uv(transform_matrix);
init_add_x(transform_matrix);
init_add_y(transform_matrix);
start_uv = transform_matrix.location().xy();
add_x = transform_matrix.x_axis().xy();
add_y = transform_matrix.y_axis().xy();
init_subsampling(num_subsamples);
init_destination_region(transform_matrix, do_crop_destination_region);
init_destination_region(transform_matrix, has_source_crop);
}
private:
void init_start_uv(const float4x4 &transform_matrix)
{
start_uv = transform_matrix.location().xy();
}
void init_add_x(const float4x4 &transform_matrix)
{
add_x = transform_matrix.x_axis().xy();
}
void init_add_y(const float4x4 &transform_matrix)
{
add_y = transform_matrix.y_axis().xy();
}
void init_subsampling(const int num_subsamples)
{
float2 subsample_add_x = add_x / num_subsamples;
@ -101,17 +66,16 @@ struct TransformUserData {
float2 delta_uv = offset_x + offset_y;
delta_uv += x * subsample_add_x;
delta_uv += y * subsample_add_y;
subsampling.delta_uvs.append(delta_uv);
subsampling_deltas.append(delta_uv);
}
}
}
void init_destination_region(const float4x4 &transform_matrix,
const bool do_crop_destination_region)
void init_destination_region(const float4x4 &transform_matrix, const bool has_source_crop)
{
if (!do_crop_destination_region) {
destination_region.x_range = IndexRange(dst->x);
destination_region.y_range = IndexRange(dst->y);
if (!has_source_crop) {
dst_region_x_range = IndexRange(dst->x);
dst_region_y_range = IndexRange(dst->y);
return;
}
@ -136,99 +100,28 @@ struct TransformUserData {
rcti dest_rect;
BLI_rcti_init(&dest_rect, 0, dst->x, 0, dst->y);
BLI_rcti_isect(&rect, &dest_rect, &rect);
destination_region.x_range = IndexRange(rect.xmin, BLI_rcti_size_x(&rect));
destination_region.y_range = IndexRange(rect.ymin, BLI_rcti_size_y(&rect));
dst_region_x_range = IndexRange(rect.xmin, BLI_rcti_size_x(&rect));
dst_region_y_range = IndexRange(rect.ymin, BLI_rcti_size_y(&rect));
}
};
/**
* \brief Crop uv-coordinates that are outside the user data src_crop rect.
*/
struct CropSource {
/**
* \brief Should the source pixel at the given uv coordinate be discarded.
*
* Uses user_data.src_crop to determine if the uv coordinate should be skipped.
*/
static bool should_discard(const TransformUserData &user_data, const float2 &uv)
{
return uv.x < user_data.src_crop.xmin || uv.x >= user_data.src_crop.xmax ||
uv.y < user_data.src_crop.ymin || uv.y >= user_data.src_crop.ymax;
}
};
/* Crop uv-coordinates that are outside the user data src_crop rect. */
static bool should_discard(const TransformContext &ctx, const float2 &uv)
{
return uv.x < ctx.src_crop.xmin || uv.x >= ctx.src_crop.xmax || uv.y < ctx.src_crop.ymin ||
uv.y >= ctx.src_crop.ymax;
}
/**
* \brief Discard that does not discard anything.
*/
struct NoDiscard {
/**
* \brief Should the source pixel at the given uv coordinate be discarded.
*
* Will never discard any pixels.
*/
static bool should_discard(const TransformUserData & /*user_data*/, const float2 & /*uv*/)
{
return false;
}
};
template<typename T> static T *init_pixel_pointer(const ImBuf *image, int x, int y);
template<> uchar *init_pixel_pointer(const ImBuf *image, int x, int y)
{
return image->byte_buffer.data + (size_t(y) * image->x + x) * image->channels;
}
template<> float *init_pixel_pointer(const ImBuf *image, int x, int y)
{
return image->float_buffer.data + (size_t(y) * image->x + x) * image->channels;
}
/**
* \brief Pointer to a pixel to write to in serial.
*/
template<
/**
* \brief Kind of buffer.
* Possible options: float, uchar.
*/
typename StorageType = float,
/**
* \brief Number of channels of a single pixel.
*/
int NumChannels = 4>
class PixelPointer {
public:
static const int ChannelLen = NumChannels;
private:
StorageType *pointer;
public:
void init_pixel_pointer(const ImBuf *image_buffer, int2 start_coordinate)
{
const size_t offset = (start_coordinate.y * size_t(image_buffer->x) + start_coordinate.x) *
NumChannels;
if constexpr (std::is_same_v<StorageType, float>) {
pointer = image_buffer->float_buffer.data + offset;
}
else if constexpr (std::is_same_v<StorageType, uchar>) {
pointer = const_cast<uchar *>(
static_cast<const uchar *>(static_cast<const void *>(image_buffer->byte_buffer.data)) +
offset);
}
else {
pointer = nullptr;
}
}
/**
* \brief Get pointer to the current pixel to write to.
*/
StorageType *get_pointer()
{
return pointer;
}
void increase_pixel_pointer()
{
pointer += NumChannels;
}
};
/**
* \brief Repeats UV coordinate.
*/
static float wrap_uv(float value, int size)
{
int x = int(floorf(value));
@ -241,416 +134,246 @@ static float wrap_uv(float value, int size)
return x;
}
/* TODO: should we use math_vectors for this. */
template<typename StorageType, int NumChannels>
class Pixel : public std::array<StorageType, NumChannels> {
public:
void clear()
{
for (int channel_index : IndexRange(NumChannels)) {
(*this)[channel_index] = 0;
template<typename T, int NumChannels>
static void add_subsample(const T *src, T *dst, int sample_number)
{
BLI_STATIC_ASSERT((is_same_any_v<T, uchar, float>), "Only uchar and float channels supported.");
float factor = 1.0 / (sample_number + 1);
if constexpr (std::is_same_v<T, uchar>) {
BLI_STATIC_ASSERT(NumChannels == 4, "Pixels using uchar requires to have 4 channels.");
blend_color_interpolate_byte(dst, dst, src, factor);
}
else if constexpr (std::is_same_v<T, float> && NumChannels == 4) {
blend_color_interpolate_float(dst, dst, src, factor);
}
else if constexpr (std::is_same_v<T, float>) {
for (int i : IndexRange(NumChannels)) {
dst[i] = dst[i] * (1.0f - factor) + src[i] * factor;
}
}
}
void add_subsample(const Pixel<StorageType, NumChannels> other, int sample_number)
{
BLI_STATIC_ASSERT((std::is_same_v<StorageType, uchar>) || (std::is_same_v<StorageType, float>),
"Only uchar and float channels supported.");
template<int NumChannels>
static void sample_nearest_float(const ImBuf *source, float u, float v, float *r_sample)
{
int x1 = int(u);
int y1 = int(v);
float factor = 1.0 / (sample_number + 1);
if constexpr (std::is_same_v<StorageType, uchar>) {
BLI_STATIC_ASSERT(NumChannels == 4, "Pixels using uchar requires to have 4 channels.");
blend_color_interpolate_byte(this->data(), this->data(), other.data(), factor);
}
else if constexpr (std::is_same_v<StorageType, float> && NumChannels == 4) {
blend_color_interpolate_float(this->data(), this->data(), other.data(), factor);
}
else if constexpr (std::is_same_v<StorageType, float>) {
for (int channel_index : IndexRange(NumChannels)) {
(*this)[channel_index] = (*this)[channel_index] * (1.0 - factor) +
other[channel_index] * factor;
}
}
}
};
/**
* \brief Read a sample from an image buffer.
*
* A sampler can read from an image buffer.
*/
template<
/** \brief Interpolation mode to use when sampling. */
eIMBInterpolationFilterMode Filter,
/** \brief storage type of a single pixel channel (uchar or float). */
typename StorageType,
/**
* \brief number of channels if the image to read.
*
* Must match the actual channels of the image buffer that is sampled.
*/
int NumChannels,
/**
* \brief Should UVs wrap
*/
bool UVWrapping>
class Sampler {
public:
using ChannelType = StorageType;
static const int ChannelLen = NumChannels;
using SampleType = Pixel<StorageType, NumChannels>;
void sample(const ImBuf *source, const float2 &uv, SampleType &r_sample)
{
float u = uv.x;
float v = uv.y;
if constexpr (UVWrapping) {
u = wrap_uv(u, source->x);
v = wrap_uv(v, source->y);
}
/* BLI_bilinear_interpolation functions use `floor(uv)` and `floor(uv)+1`
* texels. For proper mapping between pixel and texel spaces, need to
* subtract 0.5. Same for bicubic. */
if constexpr (Filter == IMB_FILTER_BILINEAR || Filter == IMB_FILTER_BICUBIC) {
u -= 0.5f;
v -= 0.5f;
}
if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<StorageType, float> &&
NumChannels == 4)
{
bilinear_interpolation_color_fl(source, r_sample.data(), u, v);
}
else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<StorageType, uchar> &&
NumChannels == 4)
{
nearest_interpolation_color_char(source, r_sample.data(), nullptr, u, v);
}
else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<StorageType, uchar> &&
NumChannels == 4)
{
bilinear_interpolation_color_char(source, r_sample.data(), u, v);
}
else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<StorageType, float>) {
if constexpr (UVWrapping) {
BLI_bilinear_interpolation_wrap_fl(source->float_buffer.data,
r_sample.data(),
source->x,
source->y,
NumChannels,
UNPACK2(uv),
true,
true);
}
else {
BLI_bilinear_interpolation_fl(
source->float_buffer.data, r_sample.data(), source->x, source->y, NumChannels, u, v);
}
}
else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<StorageType, float>) {
sample_nearest_float(source, u, v, r_sample);
}
else if constexpr (Filter == IMB_FILTER_BICUBIC && std::is_same_v<StorageType, float>) {
BLI_bicubic_interpolation_fl(
source->float_buffer.data, r_sample.data(), source->x, source->y, NumChannels, u, v);
}
else if constexpr (Filter == IMB_FILTER_BICUBIC && std::is_same_v<StorageType, uchar> &&
NumChannels == 4)
{
BLI_bicubic_interpolation_char(
source->byte_buffer.data, r_sample.data(), source->x, source->y, u, v);
}
else {
/* Unsupported sampler. */
BLI_assert_unreachable();
}
}
private:
void sample_nearest_float(const ImBuf *source,
const float u,
const float v,
SampleType &r_sample)
{
BLI_STATIC_ASSERT(std::is_same_v<StorageType, float>);
/* ImBuf in must have a valid rect or rect_float, assume this is already checked */
int x1 = int(u);
int y1 = int(v);
/* Break when sample outside image is requested. */
if (x1 < 0 || x1 >= source->x || y1 < 0 || y1 >= source->y) {
for (int i = 0; i < NumChannels; i++) {
r_sample[i] = 0.0f;
}
return;
}
const size_t offset = (size_t(source->x) * y1 + x1) * NumChannels;
const float *dataF = source->float_buffer.data + offset;
/* Break when sample outside image is requested. */
if (x1 < 0 || x1 >= source->x || y1 < 0 || y1 >= source->y) {
for (int i = 0; i < NumChannels; i++) {
r_sample[i] = dataF[i];
}
}
};
/**
* \brief Change the number of channels and store it.
*
* Template class to convert and store a sample in a PixelPointer.
* It supports:
* - 4 channel uchar -> 4 channel uchar.
* - 4 channel float -> 4 channel float.
* - 3 channel float -> 4 channel float.
* - 2 channel float -> 4 channel float.
* - 1 channel float -> 4 channel float.
*/
template<typename StorageType, int SourceNumChannels, int DestinationNumChannels>
class ChannelConverter {
public:
using SampleType = Pixel<StorageType, SourceNumChannels>;
using PixelType = PixelPointer<StorageType, DestinationNumChannels>;
/**
* \brief Convert the number of channels of the given sample to match the pixel pointer and
* store it at the location the pixel_pointer points at.
*/
void convert_and_store(const SampleType &sample, PixelType &pixel_pointer)
{
if constexpr (std::is_same_v<StorageType, uchar>) {
BLI_STATIC_ASSERT(SourceNumChannels == 4, "Unsigned chars always have 4 channels.");
BLI_STATIC_ASSERT(DestinationNumChannels == 4, "Unsigned chars always have 4 channels.");
copy_v4_v4_uchar(pixel_pointer.get_pointer(), sample.data());
}
else if constexpr (std::is_same_v<StorageType, float> && SourceNumChannels == 4 &&
DestinationNumChannels == 4)
{
copy_v4_v4(pixel_pointer.get_pointer(), sample.data());
}
else if constexpr (std::is_same_v<StorageType, float> && SourceNumChannels == 3 &&
DestinationNumChannels == 4)
{
copy_v4_fl4(pixel_pointer.get_pointer(), sample[0], sample[1], sample[2], 1.0f);
}
else if constexpr (std::is_same_v<StorageType, float> && SourceNumChannels == 2 &&
DestinationNumChannels == 4)
{
copy_v4_fl4(pixel_pointer.get_pointer(), sample[0], sample[1], 0.0f, 1.0f);
}
else if constexpr (std::is_same_v<StorageType, float> && SourceNumChannels == 1 &&
DestinationNumChannels == 4)
{
copy_v4_fl4(pixel_pointer.get_pointer(), sample[0], sample[0], sample[0], 1.0f);
}
else {
BLI_assert_unreachable();
r_sample[i] = 0.0f;
}
return;
}
void mix_and_store(const SampleType &sample, PixelType &pixel_pointer, const float mix_factor)
{
if constexpr (std::is_same_v<StorageType, uchar>) {
BLI_STATIC_ASSERT(SourceNumChannels == 4, "Unsigned chars always have 4 channels.");
BLI_STATIC_ASSERT(DestinationNumChannels == 4, "Unsigned chars always have 4 channels.");
blend_color_interpolate_byte(
pixel_pointer.get_pointer(), pixel_pointer.get_pointer(), sample.data(), mix_factor);
}
else if constexpr (std::is_same_v<StorageType, float> && SourceNumChannels == 4 &&
DestinationNumChannels == 4)
{
blend_color_interpolate_float(
pixel_pointer.get_pointer(), pixel_pointer.get_pointer(), sample.data(), mix_factor);
}
else {
BLI_assert_unreachable();
}
size_t offset = (size_t(source->x) * y1 + x1) * NumChannels;
const float *dataF = source->float_buffer.data + offset;
for (int i = 0; i < NumChannels; i++) {
r_sample[i] = dataF[i];
}
};
/**
* \brief Processor for a scanline.
*/
template<
/**
* \brief Discard functor that implements `should_discard`.
*/
typename Discard,
/**
* \brief Color interpolation function to read from the source buffer.
*/
typename Sampler,
/**
* \brief Kernel to store to the destination buffer.
* Should be an PixelPointer
*/
typename OutputPixelPointer>
class ScanlineProcessor {
Discard discarder;
OutputPixelPointer output;
Sampler sampler;
/**
* \brief Channels sizzling logic to convert between the input image buffer and the output
* image buffer.
*/
ChannelConverter<typename Sampler::ChannelType,
Sampler::ChannelLen,
OutputPixelPointer::ChannelLen>
channel_converter;
public:
/**
* \brief Inner loop of the transformations, processing a full scanline.
*/
void process(const TransformUserData *user_data, int scanline)
{
if (user_data->subsampling.delta_uvs.size() > 1) {
process_with_subsampling(user_data, scanline);
}
else {
process_one_sample_per_pixel(user_data, scanline);
}
}
private:
void process_one_sample_per_pixel(const TransformUserData *user_data, int scanline)
{
/* Note: sample at pixel center for proper filtering. */
float pixel_x = 0.5f;
float pixel_y = scanline + 0.5f;
float2 uv0 = user_data->start_uv + user_data->add_x * pixel_x + user_data->add_y * pixel_y;
output.init_pixel_pointer(user_data->dst,
int2(user_data->destination_region.x_range.first(), scanline));
for (int xi : user_data->destination_region.x_range) {
float2 uv = uv0 + xi * user_data->add_x;
if (!discarder.should_discard(*user_data, uv)) {
typename Sampler::SampleType sample;
sampler.sample(user_data->src, uv, sample);
channel_converter.convert_and_store(sample, output);
}
output.increase_pixel_pointer();
}
}
void process_with_subsampling(const TransformUserData *user_data, int scanline)
{
/* Note: sample at pixel center for proper filtering. */
float pixel_x = 0.5f;
float pixel_y = scanline + 0.5f;
float2 uv0 = user_data->start_uv + user_data->add_x * pixel_x + user_data->add_y * pixel_y;
output.init_pixel_pointer(user_data->dst,
int2(user_data->destination_region.x_range.first(), scanline));
for (int xi : user_data->destination_region.x_range) {
float2 uv = uv0 + xi * user_data->add_x;
typename Sampler::SampleType sample;
sample.clear();
int num_subsamples_added = 0;
for (const float2 &delta_uv : user_data->subsampling.delta_uvs) {
const float2 subsample_uv = uv + delta_uv;
if (!discarder.should_discard(*user_data, subsample_uv)) {
typename Sampler::SampleType sub_sample;
sampler.sample(user_data->src, subsample_uv, sub_sample);
sample.add_subsample(sub_sample, num_subsamples_added);
num_subsamples_added += 1;
}
}
if (num_subsamples_added != 0) {
const float mix_weight = float(num_subsamples_added) /
user_data->subsampling.delta_uvs.size();
channel_converter.mix_and_store(sample, output, mix_weight);
}
output.increase_pixel_pointer();
}
}
};
/**
* \brief callback function for threaded transformation.
*/
template<typename Processor> void transform_scanline_function(void *custom_data, int scanline)
{
const TransformUserData *user_data = static_cast<const TransformUserData *>(custom_data);
Processor processor;
processor.process(user_data, scanline);
}
/* Read a pixel from an image buffer, with filtering/wrapping parameters. */
template<eIMBInterpolationFilterMode Filter, typename T, int NumChannels, bool WrapUV>
static void sample_image(const ImBuf *source, float u, float v, T *r_sample)
{
if constexpr (WrapUV) {
u = wrap_uv(u, source->x);
v = wrap_uv(v, source->y);
}
/* BLI_bilinear_interpolation functions use `floor(uv)` and `floor(uv)+1`
* texels. For proper mapping between pixel and texel spaces, need to
* subtract 0.5. Same for bicubic. */
if constexpr (Filter == IMB_FILTER_BILINEAR || Filter == IMB_FILTER_BICUBIC) {
u -= 0.5f;
v -= 0.5f;
}
if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, float> && NumChannels == 4) {
bilinear_interpolation_color_fl(source, r_sample, u, v);
}
else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<T, uchar> && NumChannels == 4)
{
nearest_interpolation_color_char(source, r_sample, nullptr, u, v);
}
else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, uchar> && NumChannels == 4)
{
bilinear_interpolation_color_char(source, r_sample, u, v);
}
else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, float>) {
if constexpr (WrapUV) {
BLI_bilinear_interpolation_wrap_fl(source->float_buffer.data,
r_sample,
source->x,
source->y,
NumChannels,
u,
v,
true,
true);
}
else {
BLI_bilinear_interpolation_fl(
source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
}
}
else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<T, float>) {
sample_nearest_float<NumChannels>(source, u, v, r_sample);
}
else if constexpr (Filter == IMB_FILTER_BICUBIC && std::is_same_v<T, float>) {
BLI_bicubic_interpolation_fl(
source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
}
else if constexpr (Filter == IMB_FILTER_BICUBIC && std::is_same_v<T, uchar> && NumChannels == 4)
{
BLI_bicubic_interpolation_char(source->byte_buffer.data, r_sample, source->x, source->y, u, v);
}
else {
/* Unsupported sampler. */
BLI_assert_unreachable();
}
}
template<typename T, int SrcChannels> static void store_sample(const T *sample, T *dst)
{
if constexpr (std::is_same_v<T, uchar>) {
BLI_STATIC_ASSERT(SrcChannels == 4, "Unsigned chars always have 4 channels.");
copy_v4_v4_uchar(dst, sample);
}
else if constexpr (std::is_same_v<T, float> && SrcChannels == 4) {
copy_v4_v4(dst, sample);
}
else if constexpr (std::is_same_v<T, float> && SrcChannels == 3) {
copy_v4_fl4(dst, sample[0], sample[1], sample[2], 1.0f);
}
else if constexpr (std::is_same_v<T, float> && SrcChannels == 2) {
copy_v4_fl4(dst, sample[0], sample[1], 0.0f, 1.0f);
}
else if constexpr (std::is_same_v<T, float> && SrcChannels == 1) {
/* Note: single channel sample is stored as grayscale. */
copy_v4_fl4(dst, sample[0], sample[0], sample[0], 1.0f);
}
else {
BLI_assert_unreachable();
}
}
template<typename T, int SrcChannels>
static void mix_and_store_sample(const T *sample, T *dst, const float mix_factor)
{
if constexpr (std::is_same_v<T, uchar>) {
BLI_STATIC_ASSERT(SrcChannels == 4, "Unsigned chars always have 4 channels.");
blend_color_interpolate_byte(dst, dst, sample, mix_factor);
}
else if constexpr (std::is_same_v<T, float> && SrcChannels == 4) {
blend_color_interpolate_float(dst, dst, sample, mix_factor);
}
else {
BLI_assert_unreachable();
}
}
/* Process a block of destination image scanlines. */
template<eIMBInterpolationFilterMode Filter,
typename StorageType,
int SourceNumChannels,
int DestinationNumChannels>
ScanlineThreadFunc get_scanline_function(const eIMBTransformMode mode)
typename T,
int SrcChannels,
bool CropSource,
bool WrapUV>
static void process_scanlines(const TransformContext &ctx, IndexRange y_range)
{
switch (mode) {
case IMB_TRANSFORM_MODE_REGULAR:
return transform_scanline_function<
ScanlineProcessor<NoDiscard,
Sampler<Filter, StorageType, SourceNumChannels, false>,
PixelPointer<StorageType, DestinationNumChannels>>>;
case IMB_TRANSFORM_MODE_CROP_SRC:
return transform_scanline_function<
ScanlineProcessor<CropSource,
Sampler<Filter, StorageType, SourceNumChannels, false>,
PixelPointer<StorageType, DestinationNumChannels>>>;
case IMB_TRANSFORM_MODE_WRAP_REPEAT:
return transform_scanline_function<
ScanlineProcessor<NoDiscard,
Sampler<Filter, StorageType, SourceNumChannels, true>,
PixelPointer<StorageType, DestinationNumChannels>>>;
}
/* Note: sample at pixel center for proper filtering. */
float2 uv_start = ctx.start_uv + ctx.add_x * 0.5f + ctx.add_y * 0.5f;
BLI_assert_unreachable();
return nullptr;
}
if (ctx.subsampling_deltas.size() > 1) {
/* Multiple samples per pixel. */
for (int yi : y_range) {
T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi);
float2 uv_row = uv_start + yi * ctx.add_y;
for (int xi : ctx.dst_region_x_range) {
float2 uv = uv_row + xi * ctx.add_x;
T sample[4] = {};
int num_subsamples_added = 0;
template<eIMBInterpolationFilterMode Filter>
ScanlineThreadFunc get_scanline_function(const TransformUserData *user_data,
const eIMBTransformMode mode)
{
const ImBuf *src = user_data->src;
const ImBuf *dst = user_data->dst;
for (const float2 &delta_uv : ctx.subsampling_deltas) {
const float2 sub_uv = uv + delta_uv;
if (!CropSource || !should_discard(ctx, sub_uv)) {
T sub_sample[4];
sample_image<Filter, T, SrcChannels, WrapUV>(ctx.src, sub_uv.x, sub_uv.y, sub_sample);
add_subsample<T, SrcChannels>(sub_sample, sample, num_subsamples_added);
num_subsamples_added += 1;
}
}
if (src->channels == 4 && dst->channels == 4) {
return get_scanline_function<Filter, float, 4, 4>(mode);
}
if (src->channels == 3 && dst->channels == 4) {
return get_scanline_function<Filter, float, 3, 4>(mode);
}
if (src->channels == 2 && dst->channels == 4) {
return get_scanline_function<Filter, float, 2, 4>(mode);
}
if (src->channels == 1 && dst->channels == 4) {
return get_scanline_function<Filter, float, 1, 4>(mode);
}
return nullptr;
}
template<eIMBInterpolationFilterMode Filter>
static void transform_threaded(TransformUserData *user_data, const eIMBTransformMode mode)
{
ScanlineThreadFunc scanline_func = nullptr;
if (user_data->dst->float_buffer.data && user_data->src->float_buffer.data) {
scanline_func = get_scanline_function<Filter>(user_data, mode);
}
else if (user_data->dst->byte_buffer.data && user_data->src->byte_buffer.data) {
/* Number of channels is always 4 when using uchar buffers (sRGB + straight alpha). */
scanline_func = get_scanline_function<Filter, uchar, 4, 4>(mode);
}
if (scanline_func != nullptr) {
threading::parallel_for(user_data->destination_region.y_range, 8, [&](IndexRange range) {
for (int scanline : range) {
scanline_func(user_data, scanline);
if (num_subsamples_added != 0) {
const float mix_weight = float(num_subsamples_added) / ctx.subsampling_deltas.size();
mix_and_store_sample<T, SrcChannels>(sample, output, mix_weight);
}
output += 4;
}
});
}
}
else {
/* One sample per pixel. */
for (int yi : y_range) {
T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi);
float2 uv_row = uv_start + yi * ctx.add_y;
for (int xi : ctx.dst_region_x_range) {
float2 uv = uv_row + xi * ctx.add_x;
if (!CropSource || !should_discard(ctx, uv)) {
T sample[4];
sample_image<Filter, T, SrcChannels, WrapUV>(ctx.src, uv.x, uv.y, sample);
store_sample<T, SrcChannels>(sample, output);
}
output += 4;
}
}
}
}
template<eIMBInterpolationFilterMode Filter, typename T, int SrcChannels>
static void transform_scanlines(const TransformContext &ctx, IndexRange y_range)
{
switch (ctx.mode) {
case IMB_TRANSFORM_MODE_REGULAR:
process_scanlines<Filter, T, SrcChannels, false, false>(ctx, y_range);
break;
case IMB_TRANSFORM_MODE_CROP_SRC:
process_scanlines<Filter, T, SrcChannels, true, false>(ctx, y_range);
break;
case IMB_TRANSFORM_MODE_WRAP_REPEAT:
process_scanlines<Filter, T, SrcChannels, false, true>(ctx, y_range);
break;
default:
BLI_assert_unreachable();
break;
}
}
template<eIMBInterpolationFilterMode Filter>
static void transform_scanlines_filter(const TransformContext &ctx, IndexRange y_range)
{
int channels = ctx.src->channels;
if (ctx.dst->float_buffer.data && ctx.src->float_buffer.data) {
/* Float images. */
if (channels == 4) {
transform_scanlines<Filter, float, 4>(ctx, y_range);
}
else if (channels == 3) {
transform_scanlines<Filter, float, 3>(ctx, y_range);
}
else if (channels == 2) {
transform_scanlines<Filter, float, 2>(ctx, y_range);
}
else if (channels == 1) {
transform_scanlines<Filter, float, 1>(ctx, y_range);
}
}
else if (ctx.dst->byte_buffer.data && ctx.src->byte_buffer.data) {
/* Byte images. */
if (channels == 4) {
transform_scanlines<Filter, uchar, 4>(ctx, y_range);
}
}
}
@ -659,6 +382,7 @@ static void transform_threaded(TransformUserData *user_data, const eIMBTransform
extern "C" {
using namespace blender::imbuf::transform;
using namespace blender;
void IMB_transform(const ImBuf *src,
ImBuf *dst,
@ -671,25 +395,28 @@ void IMB_transform(const ImBuf *src,
BLI_assert_msg(mode != IMB_TRANSFORM_MODE_CROP_SRC || src_crop != nullptr,
"No source crop rect given, but crop source is requested. Or source crop rect "
"was given, but crop source was not requested.");
BLI_assert_msg(dst->channels == 4, "Destination image must have 4 channels.");
TransformUserData user_data;
user_data.src = src;
user_data.dst = dst;
if (mode == IMB_TRANSFORM_MODE_CROP_SRC) {
user_data.src_crop = *src_crop;
TransformContext ctx;
ctx.src = src;
ctx.dst = dst;
ctx.mode = mode;
bool crop = mode == IMB_TRANSFORM_MODE_CROP_SRC;
if (crop) {
ctx.src_crop = *src_crop;
}
user_data.init(blender::float4x4(transform_matrix),
num_subsamples,
ELEM(mode, IMB_TRANSFORM_MODE_CROP_SRC));
ctx.init(blender::float4x4(transform_matrix), num_subsamples, crop);
if (filter == IMB_FILTER_NEAREST) {
transform_threaded<IMB_FILTER_NEAREST>(&user_data, mode);
}
else if (filter == IMB_FILTER_BILINEAR) {
transform_threaded<IMB_FILTER_BILINEAR>(&user_data, mode);
}
else if (filter == IMB_FILTER_BICUBIC) {
transform_threaded<IMB_FILTER_BICUBIC>(&user_data, mode);
}
threading::parallel_for(ctx.dst_region_y_range, 8, [&](IndexRange y_range) {
if (filter == IMB_FILTER_NEAREST) {
transform_scanlines_filter<IMB_FILTER_NEAREST>(ctx, y_range);
}
else if (filter == IMB_FILTER_BILINEAR) {
transform_scanlines_filter<IMB_FILTER_BILINEAR>(ctx, y_range);
}
else if (filter == IMB_FILTER_BICUBIC) {
transform_scanlines_filter<IMB_FILTER_BICUBIC>(ctx, y_range);
}
});
}
}

View File

@ -265,6 +265,14 @@ add_blender_test(
--run-all-tests
)
add_blender_test(
curves_extrude
${TEST_SRC_DIR}/modeling/curves_extrude.blend
--python ${TEST_PYTHON_DIR}/curves_extrude.py
--
--run-all-tests
)
# ------------------------------------------------------------------------------
# MODIFIERS TESTS
add_blender_test(
@ -816,6 +824,7 @@ set(geo_node_tests
mesh
mesh/extrude
mesh/split_edges
mesh/triangulate
points
texture
utilities

View File

@ -0,0 +1,192 @@
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
from abc import ABC, abstractmethod
import bpy
import os
import sys
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
from modules.mesh_test import RunTest
class CurvesTest(ABC):
def __init__(self, test_object_name, exp_object_name, test_name=None):
self.test_object_name = test_object_name
self.exp_object_name = exp_object_name
self.test_object = bpy.data.objects[self.test_object_name]
self.expected_object = bpy.data.objects[self.exp_object_name]
self.verbose = os.getenv("BLENDER_VERBOSE") is not None
if test_name:
self.test_name = test_name
else:
filepath = bpy.data.filepath
self.test_name = bpy.path.display_name_from_filepath(filepath)
self._failed_tests_list = []
def create_evaluated_object(self):
"""
Creates an evaluated object.
"""
bpy.context.view_layer.objects.active = self.test_object
# Duplicate test object.
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = self.test_object
self.test_object.select_set(True)
bpy.ops.object.duplicate()
self.evaluated_object = bpy.context.active_object
self.evaluated_object.name = "evaluated_object"
@staticmethod
def _print_result(result):
"""
Prints the comparison, selection and validation result.
"""
print("Results:")
for key in result:
print("{} : {}".format(key, result[key][1]))
print()
def run_test(self):
"""
Runs a single test, runs it again if test file is updated.
"""
print("\nSTART {} test.".format(self.test_name))
self.create_evaluated_object()
self.apply_operations()
result = self.compare_objects(self.evaluated_object, self.expected_object)
# Initializing with True to get correct resultant of result_code booleans.
success = True
inside_loop_flag = False
for key in result:
inside_loop_flag = True
success = success and result[key][0]
# Check "success" is actually evaluated and is not the default True value.
if not inside_loop_flag:
success = False
if success:
self.print_passed_test_result(result)
# Clean up.
if self.verbose:
print("Cleaning up...")
# Delete evaluated_test_object.
bpy.ops.object.delete()
return True
else:
self.print_failed_test_result(result)
return False
@abstractmethod
def apply_operations(self, evaluated_test_object_name):
pass
@staticmethod
def compare_curves(evaluated_curves, expected_curves):
if len(evaluated_curves.attributes.items()) != len(expected_curves.attributes.items()):
print("Attribute count doesn't match")
for a_idx, attribute in evaluated_curves.attributes.items():
expected_attribute = expected_curves.attributes[a_idx]
if len(attribute.data.items()) != len(expected_attribute.data.items()):
print("Attribute data length doesn't match")
value_attr_name = ('vector' if attribute.data_type == 'FLOAT_VECTOR'
or attribute.data_type == 'FLOAT2' else
'color' if attribute.data_type == 'FLOAT_COLOR' else 'value')
for v_idx, attribute_value in attribute.data.items():
if getattr(
attribute_value,
value_attr_name) != getattr(
expected_attribute.data[v_idx],
value_attr_name):
print("Attribute '{}' values do not match".format(attribute.name))
return False
return True
def compare_objects(self, evaluated_object, expected_object):
result_codes = {}
equal = self.compare_curves(evaluated_object.data, expected_object.data)
result_codes['Curves Comparison'] = (equal, evaluated_object.data)
return result_codes
def print_failed_test_result(self, result):
"""
Print results for failed test.
"""
print("FAILED {} test with the following: ".format(self.test_name))
def print_passed_test_result(self, result):
"""
Print results for passing test.
"""
print("PASSED {} test successfully.".format(self.test_name))
class CurvesOpTest(CurvesTest):
def __init__(self, test_name, test_object_name, exp_object_name, operators_stack):
super().__init__(test_object_name, exp_object_name, test_name)
self.operators_stack = operators_stack
def apply_operations(self):
for operator_name in self.operators_stack:
bpy.ops.object.mode_set(mode='EDIT')
curves_operator = getattr(bpy.ops.curves, operator_name)
try:
retval = curves_operator()
except AttributeError:
raise AttributeError("bpy.ops.curves has no attribute {}".format(operator_name))
except TypeError as ex:
raise TypeError("Incorrect operator parameters {!r} raised {!r}".format([], ex))
if retval != {'FINISHED'}:
raise RuntimeError("Unexpected operator return value: {}".format(operator_name))
bpy.ops.object.mode_set(mode='OBJECT')
def main():
tests = [
CurvesOpTest("Extrude 1 Point Curve", "a_test1PointCurve", "a_test1PointCurveExpected", ['extrude']),
CurvesOpTest("Extrude Middle Points", "b_testMiddlePoints", "b_testMiddlePointsExpected", ['extrude']),
CurvesOpTest("Extrude End Points", "c_testEndPoints", "c_testEndPointsExpected", ['extrude']),
CurvesOpTest("Extrude Neighbors In Separate Curves", "d_testNeighborsInCurves", "d_testNeighborsInCurvesExpected", ['extrude']),
CurvesOpTest("Extrude Edge Curves", "e_testEdgeCurves", "e_testEdgeCurvesExpected", ['extrude']),
CurvesOpTest("Extrude Middle Curve", "f_testMiddleCurve", "f_testMiddleCurveExpected", ['extrude']),
CurvesOpTest("Extrude All Points", "g_testAllPoints", "g_testAllPointsExpected", ['extrude'])
]
curves_extrude_test = RunTest(tests)
command = list(sys.argv)
for i, cmd in enumerate(command):
if cmd == "--run-all-tests":
curves_extrude_test.do_compare = True
curves_extrude_test.run_all_tests()
break
elif cmd == "--run-test":
curves_extrude_test.do_compare = False
name = command[i + 1]
curves_extrude_test.run_test(name)
break
if __name__ == "__main__":
main()