Libraries: Support editing linked datablocks from some libraries

For the brush assets, this mechanism makes brush, texture, node tree and
image datablocks editable even when library linked.

This commit should introduce no functional change yet, as the code to
actually tag such libraries as editable will come later.

* These libraries and their datablocks are preserved when loading a new
  blend file, much like the UI can be preserved.
* Operators that create new datablocks to be assigned to such datablocks
  will put the datablocks in the same library immediately. This was
  implemented for datablocks relevant for brush assets.
* RNA does not allow assignment of pointers from such linked datablocks
  to local datablocks.

Co-authored-by: Bastien Montagne <bastien@blender.org>

Pull Request: https://projects.blender.org/blender/blender/pulls/121920
This commit is contained in:
Brecht Van Lommel 2024-05-17 18:00:25 +02:00 committed by Gitea
parent a1b4d5ecc8
commit 5f9f3116db
12 changed files with 301 additions and 12 deletions

@ -247,6 +247,12 @@ void BKE_libblock_copy_in_lib(Main *bmain,
*/
void *BKE_libblock_copy(Main *bmain, const ID *id) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL();
/**
* For newly created IDs, move it into same library as owner ID.
* This assumes the ID is local.
*/
void BKE_id_move_to_same_lib(Main &bmain, ID &id, const ID &owner_id);
/**
* Sets the name of a block to name, suitably adjusted for uniqueness.
*/

@ -306,6 +306,200 @@ static bool reuse_bmain_data_remapper_is_id_remapped(id::IDRemapper &remapper, I
return false;
}
static bool reuse_bmain_move_id(ReuseOldBMainData *reuse_data,
ID *id,
Library *lib,
const bool reuse_existing)
{
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
Main *new_bmain = reuse_data->new_bmain;
Main *old_bmain = reuse_data->old_bmain;
ListBase *new_lb = which_libbase(new_bmain, GS(id->name));
ListBase *old_lb = which_libbase(old_bmain, GS(id->name));
if (reuse_existing) {
/* A 'new' version of the same data may already exist in new_bmain, in the rare case
* that the same asset blend file was linked explicitly into the blend file we are loading.
* Don't move the old linked ID, but remap its usages to the new one instead. */
LISTBASE_FOREACH_BACKWARD (ID *, id_iter, new_lb) {
if (!ELEM(id_iter->lib, id->lib, lib)) {
continue;
}
if (!STREQ(id_iter->name + 2, id->name + 2)) {
continue;
}
remapper.add(id, id_iter);
return false;
}
}
/* If ID is already in the new_bmain, this should not have been called. */
BLI_assert(BLI_findindex(new_lb, id) < 0);
BLI_assert(BLI_findindex(old_lb, id) >= 0);
/* Move from one list to another, and ensure name is valid. */
BLI_remlink_safe(old_lb, id);
BKE_main_namemap_remove_name(old_bmain, id, id->name + 2);
id->lib = lib;
BLI_addtail(new_lb, id);
BKE_id_new_name_validate(new_bmain, new_lb, id, nullptr, true);
BKE_lib_libblock_session_uid_renew(id);
/* Remap to itself, to avoid re-processing this ID again. */
remapper.add(id, id);
return true;
}
static Library *reuse_bmain_data_dependencies_new_library_get(ReuseOldBMainData *reuse_data,
Library *old_lib)
{
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
Library *new_lib = old_lib;
IDRemapperApplyResult result = remapper.apply(reinterpret_cast<ID **>(&new_lib),
ID_REMAP_APPLY_DEFAULT);
switch (result) {
case ID_REMAP_RESULT_SOURCE_UNAVAILABLE: {
/* Move library to new bmain.
* There should be no filepath conflicts, as #reuse_bmain_data_remapper_ensure has
* already remapped existing libraries with matching filepath. */
reuse_bmain_move_id(reuse_data, &old_lib->id, nullptr, false);
return old_lib;
}
case ID_REMAP_RESULT_SOURCE_NOT_MAPPABLE: {
BLI_assert_unreachable();
return nullptr;
}
case ID_REMAP_RESULT_SOURCE_REMAPPED: {
/* Already in new bmain, only transfer flags. */
new_lib->runtime.tag |= old_lib->runtime.tag &
(LIBRARY_ASSET_EDITABLE | LIBRARY_ASSET_FILE_WRITABLE);
return new_lib;
}
case ID_REMAP_RESULT_SOURCE_UNASSIGNED: {
/* Happens when the library is the newly opened blend file. */
return nullptr;
}
}
BLI_assert_unreachable();
return nullptr;
}
static int reuse_editable_asset_bmain_data_dependencies_process_cb(
LibraryIDLinkCallbackData *cb_data)
{
ID *id = *cb_data->id_pointer;
if (id == nullptr) {
return IDWALK_RET_NOP;
}
ReuseOldBMainData *reuse_data = static_cast<ReuseOldBMainData *>(cb_data->user_data);
/* First check if it has already been remapped. */
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
if (reuse_bmain_data_remapper_is_id_remapped(remapper, id)) {
return IDWALK_RET_STOP_RECURSION;
}
if (id->lib == nullptr) {
/* There should be no links to local datablocks from linked editable data. */
remapper.add(id, nullptr);
BLI_assert_unreachable();
return IDWALK_RET_STOP_RECURSION;
}
/* Only preserve specific datablock types. */
if (!ID_TYPE_SUPPORTS_ASSET_EDITABLE(GS(id->name))) {
remapper.add(id, nullptr);
return IDWALK_RET_STOP_RECURSION;
}
/* There may be a new library pointer in new_bmain, matching a library in old_bmain, even
* though pointer values are not the same. So we need to check new linked IDs in new_bmain
* against both potential library pointers. */
Library *old_id_new_lib = reuse_bmain_data_dependencies_new_library_get(reuse_data, id->lib);
/* Happens when the library is the newly opened blend file. */
if (old_id_new_lib == nullptr) {
remapper.add(id, nullptr);
return IDWALK_RET_STOP_RECURSION;
}
/* Move to new main database. */
return reuse_bmain_move_id(reuse_data, id, old_id_new_lib, true) ? IDWALK_RET_STOP_RECURSION :
IDWALK_RET_NOP;
}
static bool reuse_editable_asset_needed(ReuseOldBMainData *reuse_data)
{
Main *old_bmain = reuse_data->old_bmain;
LISTBASE_FOREACH (Library *, lib, &old_bmain->libraries) {
if (lib->runtime.tag & LIBRARY_ASSET_EDITABLE) {
return true;
}
}
return false;
}
/**
* Selectively 'import' data from old Main into new Main, provided it does not conflict with data
* already present in the new Main (name-wise and library-wise).
*
* Dependencies from moved over old data are also imported into the new Main, (unless, in case of
* linked data, a matching linked ID is already available in new Main).
*
* When a conflict is found, usages of the conflicted ID by the old data are stored in the
* `remapper` of `ReuseOldBMainData` to be remapped to the matching data in the new Main later.
*
* NOTE: This function will never remove any original new data from the new Main, it only moves
* (some of) the old data to the new Main.
*/
static void reuse_editable_asset_bmain_data_for_blendfile(ReuseOldBMainData *reuse_data,
const short idcode)
{
Main *new_bmain = reuse_data->new_bmain;
Main *old_bmain = reuse_data->old_bmain;
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
ListBase *old_lb = which_libbase(old_bmain, idcode);
ID *old_id_iter;
FOREACH_MAIN_LISTBASE_ID_BEGIN (old_lb, old_id_iter) {
/* Keep any datablocks from libraries marked as LIBRARY_ASSET_EDITABLE. */
if (!((ID_IS_LINKED(old_id_iter) && old_id_iter->lib->runtime.tag & LIBRARY_ASSET_EDITABLE))) {
continue;
}
Library *old_id_new_lib = reuse_bmain_data_dependencies_new_library_get(reuse_data,
old_id_iter->lib);
/* Happens when the library is the newly opened blend file. */
if (old_id_new_lib == nullptr) {
remapper.add(old_id_iter, nullptr);
continue;
}
if (reuse_bmain_move_id(reuse_data, old_id_iter, old_id_new_lib, true)) {
/* Port over dependencies of re-used ID, unless matching already existing ones in
* new_bmain can be found.
*
* NOTE : No pointers are remapped here, this code only moves dependencies from old_bmain
* to new_bmain if needed, and add necessary remapping rules to the reuse_data.remapper. */
BKE_library_foreach_ID_link(new_bmain,
old_id_iter,
reuse_editable_asset_bmain_data_dependencies_process_cb,
reuse_data,
IDWALK_RECURSE | IDWALK_DO_LIBRARY_POINTER);
}
}
FOREACH_MAIN_LISTBASE_ID_END;
}
/**
* Does a complete replacement of data in `new_bmain` by data from `old_bmain. Original new data
* are moved to the `old_bmain`, and will be freed together with it.
@ -313,8 +507,9 @@ static bool reuse_bmain_data_remapper_is_id_remapped(id::IDRemapper &remapper, I
* WARNING: Currently only expects to work on local data, won't work properly if some of the IDs of
* given type are linked.
*
* NOTE: There is no support at all for potential dependencies of the IDs moved around. This is not
* expected to be necessary for the current use cases (UI-related IDs).
* NOTE: Unlike with #reuse_editable_asset_bmain_data_for_blendfile, there is no support at all for
* potential dependencies of the IDs moved around. This is not expected to be necessary for the
* current use cases (UI-related IDs).
*/
static void swap_old_bmain_data_for_blendfile(ReuseOldBMainData *reuse_data, const short id_code)
{
@ -660,7 +855,6 @@ static void setup_app_data(bContext *C,
{
Main *bmain = G_MAIN;
const bool recover = (G.fileflags & G_FILE_RECOVER_READ) != 0;
const bool is_startup = params->is_startup;
enum {
LOAD_UI = 1,
LOAD_UI_OFF,
@ -728,6 +922,18 @@ static void setup_app_data(bContext *C,
}
BKE_main_idmap_destroy(reuse_data.id_map);
if (!params->is_factory_settings && reuse_editable_asset_needed(&reuse_data)) {
/* Keep linked brush asset data, similar to UI data. Only does a known
* subset know. Could do everything, but that risks dragging along more
* scene data than we want. */
for (short idtype_index = 0; idtype_index < INDEX_ID_MAX; idtype_index++) {
const IDTypeInfo *idtype_info = BKE_idtype_get_info_from_idtype_index(idtype_index);
if (ID_TYPE_SUPPORTS_ASSET_EDITABLE(idtype_info->id_code)) {
reuse_editable_asset_bmain_data_for_blendfile(&reuse_data, idtype_info->id_code);
}
}
}
}
/* Logic for 'track_undo_scene' is to keep using the scene which the active screen has, as long
@ -901,7 +1107,7 @@ static void setup_app_data(bContext *C,
bmain->recovered = false;
/* `startup.blend` or recovered startup. */
if (is_startup) {
if (params->is_startup) {
bmain->filepath[0] = '\0';
}
else if (recover) {

@ -822,6 +822,21 @@ ID *BKE_id_copy_for_use_in_bmain(Main *bmain, const ID *id)
return newid;
}
void BKE_id_move_to_same_lib(Main &bmain, ID &id, const ID &owner_id)
{
BLI_assert(id.lib == nullptr);
if (owner_id.lib == nullptr) {
return;
}
id.lib = owner_id.lib;
id.tag |= LIB_TAG_INDIRECT;
BKE_main_namemap_remove_name(&bmain, &id, id.name + 2);
ListBase *lb = which_libbase(&bmain, GS(id.name));
BKE_id_new_name_validate(&bmain, lb, &id, id.name + 2, true);
}
static void id_embedded_swap(ID **embedded_id_a,
ID **embedded_id_b,
const bool do_full_id,

@ -89,6 +89,7 @@ struct BlendFileReadWMSetupData {
struct BlendFileReadParams {
uint skip_flags : 3; /* #eBLOReadSkip */
uint is_startup : 1;
uint is_factory_settings : 1;
/** Whether we are reading the memfile for an undo or a redo. */
int undo_direction; /* #eUndoStepDir */

@ -1382,7 +1382,7 @@ static void template_ID(const bContext *C,
template_id_workspace_pin_extra_icon(template_ui, but);
if (!hide_buttons) {
if (!hide_buttons && !(idfrom && ID_IS_LINKED(idfrom))) {
if (ID_IS_LINKED(id)) {
const bool disabled = !BKE_idtype_idcode_is_localizable(GS(id->name));
if (id->tag & LIB_TAG_INDIRECT) {

@ -789,6 +789,10 @@ static int new_material_exec(bContext *C, wmOperator * /*op*/)
* pointer use also increases user, so this compensates it */
id_us_min(&ma->id);
if (ptr.owner_id) {
BKE_id_move_to_same_lib(*bmain, ma->id, *ptr.owner_id);
}
PointerRNA idptr = RNA_id_pointer_create(&ma->id);
RNA_property_pointer_set(&ptr, prop, idptr, nullptr);
RNA_property_update(C, &ptr, prop);
@ -843,6 +847,10 @@ static int new_texture_exec(bContext *C, wmOperator * /*op*/)
* pointer use also increases user, so this compensates it */
id_us_min(&tex->id);
if (ptr.owner_id) {
BKE_id_move_to_same_lib(*bmain, tex->id, *ptr.owner_id);
}
PointerRNA idptr = RNA_id_pointer_create(&tex->id);
RNA_property_pointer_set(&ptr, prop, idptr, nullptr);
RNA_property_update(C, &ptr, prop);
@ -900,6 +908,10 @@ static int new_world_exec(bContext *C, wmOperator * /*op*/)
* pointer use also increases user, so this compensates it */
id_us_min(&wo->id);
if (ptr.owner_id) {
BKE_id_move_to_same_lib(*bmain, wo->id, *ptr.owner_id);
}
PointerRNA idptr = RNA_id_pointer_create(&wo->id);
RNA_property_pointer_set(&ptr, prop, idptr, nullptr);
RNA_property_update(C, &ptr, prop);

@ -1262,14 +1262,20 @@ static void image_open_cancel(bContext * /*C*/, wmOperator *op)
static Image *image_open_single(Main *bmain,
wmOperator *op,
ImageFrameRange *range,
bool use_multiview)
const ImageFrameRange *range,
const bool use_multiview,
const bool check_exists)
{
bool exists = false;
Image *ima = nullptr;
errno = 0;
ima = BKE_image_load_exists_ex(bmain, range->filepath, &exists);
if (check_exists) {
ima = BKE_image_load_exists_ex(bmain, range->filepath, &exists);
}
else {
ima = BKE_image_load(bmain, range->filepath);
}
if (!ima) {
if (op->customdata) {
@ -1338,9 +1344,17 @@ static int image_open_exec(bContext *C, wmOperator *op)
image_open_init(C, op);
}
ImageOpenData *iod = static_cast<ImageOpenData *>(op->customdata);
/* For editable assets always create a new image datablock. We can't assign
* a local datablock to linked asset datablocks. */
const bool check_exists = !(iod->pprop.prop && iod->pprop.ptr.owner_id &&
ID_IS_LINKED(iod->pprop.ptr.owner_id) &&
ID_IS_EDITABLE(iod->pprop.ptr.owner_id));
ListBase ranges = ED_image_filesel_detect_sequences(bmain, op, use_udim);
LISTBASE_FOREACH (ImageFrameRange *, range, &ranges) {
Image *ima_range = image_open_single(bmain, op, range, use_multiview);
Image *ima_range = image_open_single(bmain, op, range, use_multiview, check_exists);
/* take the first image */
if ((ima == nullptr) && ima_range) {
@ -1358,13 +1372,15 @@ static int image_open_exec(bContext *C, wmOperator *op)
}
/* hook into UI */
ImageOpenData *iod = static_cast<ImageOpenData *>(op->customdata);
if (iod->pprop.prop) {
/* when creating new ID blocks, use is already 1, but RNA
* pointer use also increases user, so this compensates it */
id_us_min(&ima->id);
if (iod->pprop.ptr.owner_id) {
BKE_id_move_to_same_lib(*bmain, ima->id, *iod->pprop.ptr.owner_id);
}
PointerRNA imaptr = RNA_id_pointer_create(&ima->id);
RNA_property_pointer_set(&iod->pprop.ptr, iod->pprop.prop, imaptr, nullptr);
RNA_property_update(C, &iod->pprop.ptr, iod->pprop.prop);
@ -2589,6 +2605,10 @@ static int image_new_exec(bContext *C, wmOperator *op)
* pointer use also increases user, so this compensates it */
id_us_min(&ima->id);
if (data->pprop.ptr.owner_id) {
BKE_id_move_to_same_lib(*bmain, ima->id, *data->pprop.ptr.owner_id);
}
PointerRNA imaptr = RNA_id_pointer_create(&ima->id);
RNA_property_pointer_set(&data->pprop.ptr, data->pprop.prop, imaptr, nullptr);
RNA_property_update(C, &data->pprop.ptr, data->pprop.prop);

@ -1071,6 +1071,10 @@ static int new_node_tree_exec(bContext *C, wmOperator *op)
* user. */
id_us_min(&ntree->id);
if (ptr.owner_id) {
BKE_id_move_to_same_lib(*bmain, ntree->id, *ptr.owner_id);
}
PointerRNA idptr = RNA_id_pointer_create(&ntree->id);
RNA_property_pointer_set(&ptr, prop, idptr, nullptr);
RNA_property_update(C, &ptr, prop);

@ -1215,6 +1215,8 @@ static bNode *node_group_make_from_nodes(const bContext &C,
/* New node-tree. */
bNodeTree *ngroup = bke::ntreeAddTree(bmain, "NodeGroup", ntreetype);
BKE_id_move_to_same_lib(*bmain, ngroup->id, ntree.id);
/* make group node */
bNode *gnode = bke::nodeAddNode(&C, &ntree, ntype);
gnode->id = (ID *)ngroup;

@ -590,6 +590,14 @@ typedef struct Library {
enum eLibrary_Tag {
/* Automatic recursive resync was needed when linking/loading data from that library. */
LIBRARY_TAG_RESYNC_REQUIRED = 1 << 0,
/* Datablocks from this library are editable in the UI despite being linked.
* Used for asset that can be temporarily or permantently edited.
* Currently all datablocks from this library will be edited. In the future this
* may need to become per datablock to handle cases where a library is both used
* for editable assets and linked into the blend file for other reasons. */
LIBRARY_ASSET_EDITABLE = 1 << 1,
/* The blend file of this library is writable for asset editing. */
LIBRARY_ASSET_FILE_WRITABLE = 1 << 2,
};
/**
@ -661,7 +669,12 @@ typedef struct PreviewImage {
#define ID_IS_LINKED(_id) (((const ID *)(_id))->lib != NULL)
#define ID_IS_EDITABLE(_id) (((const ID *)(_id))->lib == NULL)
#define ID_TYPE_SUPPORTS_ASSET_EDITABLE(id_type) ELEM(id_type, ID_BR, ID_TE, ID_NT, ID_IM)
#define ID_IS_EDITABLE(_id) \
((((const ID *)(_id))->lib == NULL) || \
((((const ID *)(_id))->lib->runtime.tag & LIBRARY_ASSET_EDITABLE) && \
ID_TYPE_SUPPORTS_ASSET_EDITABLE(GS((((const ID *)(_id))->name)))))
/* Note that these are fairly high-level checks, should be used at user interaction level, not in
* BKE_library_override typically (especially due to the check on LIB_TAG_EXTERN). */

@ -2554,6 +2554,14 @@ static void rna_def_library(BlenderRNA *brna)
"current blendfile, and that had to be recursively resynced on load "
"(it is recommended to open and re-save that library blendfile then)");
prop = RNA_def_property(srna, "is_editable", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "runtime.tag", LIBRARY_ASSET_EDITABLE);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(prop,
"Editable",
"Datablocks in this library are editable despite being linked. Used by "
"brush assets and their dependencies");
func = RNA_def_function(srna, "reload", "rna_Library_reload");
RNA_def_function_flag(func, FUNC_USE_REPORTS | FUNC_USE_CONTEXT);
RNA_def_function_ui_description(func, "Reload this library and all its linked data-blocks");

@ -1360,6 +1360,7 @@ void wm_homefile_read_ex(bContext *C,
if (BLI_access(filepath_startup, R_OK) == 0) {
BlendFileReadParams params{};
params.is_startup = true;
params.is_factory_settings = use_factory_settings;
params.skip_flags = skip_flags | BLO_READ_SKIP_USERDEF;
BlendFileReadReport bf_reports{};
bf_reports.reports = reports;
@ -1402,6 +1403,7 @@ void wm_homefile_read_ex(bContext *C,
if (success == false) {
BlendFileReadParams read_file_params{};
read_file_params.is_startup = true;
read_file_params.is_factory_settings = use_factory_settings;
read_file_params.skip_flags = skip_flags;
BlendFileData *bfd = BKE_blendfile_read_from_memory(
datatoc_startup_blend, datatoc_startup_blend_size, &read_file_params, nullptr);