Files
test2/source/blender/editors/space_node/clipboard.cc
Brecht Van Lommel c7a33a62a2 Cleanup: Directly include DNA_userdef_types.h and BLI_listbase.h
Instead of relying on them being included indirectly.

Pull Request: https://projects.blender.org/blender/blender/pulls/134406
2025-02-12 23:01:08 +01:00

531 lines
17 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_space_types.h"
#include "BLI_listbase.h"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_lib_id.hh"
#include "BKE_lib_query.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_main_idmap.hh"
#include "BKE_main_invariants.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "BKE_report.hh"
#include "ED_node.hh"
#include "ED_render.hh"
#include "ED_screen.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "DEG_depsgraph_build.hh"
#include "node_intern.hh"
namespace blender::ed::space_node {
struct NodeClipboardItemIDInfo {
/** Name of the referenced ID. */
std::string id_name;
/**
* Library filepath of the referenced ID, together with its name it forms a unique identifier.
*
* \note Library reference is stored as an absolute path. Since the Node clipboard is runtime
* data, persistent over new blend-files opening, this should guarantee that identical IDs from
* identical libraries can be matched accordingly, even across several blend-files.
*/
std::string library_path;
/** The validated ID pointer (may be the same as the original one, or a new one). */
std::optional<ID *> new_id = {};
};
struct NodeClipboardItem {
bNode *node;
/**
* The offset and size of the node from when it was drawn. Stored here since it doesn't remain
* valid for the nodes in the clipboard.
*/
rctf draw_rect;
/* Extra info to validate the IDs used by the node on creation. Otherwise it may reference
* missing data. */
ID *id;
std::string id_name;
std::string library_name;
};
struct ClipboardLink {
const bNode *from_node = nullptr;
const bNode *to_node = nullptr;
std::string from_socket;
std::string to_socket;
int flag = 0;
int multi_input_sort_id = 0;
};
struct NodeClipboard {
Vector<NodeClipboardItem> nodes;
Vector<ClipboardLink> links;
/* A mapping of all ID references from nodes in the clipboard, to information allowing to find
* their valid matching counterpart in current Main data when pasting the nodes back. Entries are
* added when adding nodes to the clipboard, and they are updated when pasting the nodes back
* into current Main. */
Map<ID *, NodeClipboardItemIDInfo> old_ids_to_idinfo;
/** Completely empty the current clipboard content. */
void clear()
{
for (NodeClipboardItem &item : this->nodes) {
bke::node_free_node(nullptr, item.node);
}
this->nodes.clear_and_shrink();
this->links.clear_and_shrink();
this->old_ids_to_idinfo.clear();
}
/**
* Find valid current pointers for all IDs used by the nodes in the clipboard.
*
* DO NOT update the nodes' pointers here though, as this would affect the clipboard content,
* which is no desired here. It should remain as in original state, such that e.g. one can copy
* nodes in file `A.blend`, open file `B.blend`, paste nodes (and lose some of the invalid ID
* references in file `B.blend`), and then open again file `A.blend`, paste nodes, and lose no ID
* references.
*/
bool paste_validate_id_references(Main &bmain)
{
bool is_valid = true;
IDNameLib_Map *bmain_id_map = nullptr;
/* Clear any potentially previously found `new_id` valid pointers in #old_ids_to_idinfo values,
* and populate a temporary mapping from absolute library paths to existing Library IDs in
* given Main. */
Map<std::string, Library *> libraries_path_to_id;
for (NodeClipboardItemIDInfo &id_info : this->old_ids_to_idinfo.values()) {
id_info.new_id.reset();
if (!id_info.library_path.empty() && !libraries_path_to_id.contains(id_info.library_path)) {
libraries_path_to_id.add(
id_info.library_path,
blender::bke::library::search_filepath_abs(&bmain.libraries, id_info.library_path));
}
}
/* Find a new valid ID pointer for all ID usages in given node.
*
* NOTE: Due to the fact that the clipboard survives file loading, only name (including IDType)
* and library-path pairs can be used here.
* - UID cannot be trusted across file load.
* - ID pointer itself cannot be trusted across undo/redo and file-load. */
auto validate_id_fn = [this, &is_valid, &bmain, &bmain_id_map, &libraries_path_to_id](
LibraryIDLinkCallbackData *cb_data) -> int {
ID *old_id = *(cb_data->id_pointer);
if (!old_id) {
return IDWALK_RET_NOP;
}
if (!this->old_ids_to_idinfo.contains(old_id)) {
BLI_assert_msg(
0, "Missing entry in the old ID data of the node clipboard, should not happen");
is_valid = false;
return IDWALK_RET_NOP;
}
NodeClipboardItemIDInfo &id_info = this->old_ids_to_idinfo.lookup(old_id);
if (!id_info.new_id) {
if (!bmain_id_map) {
bmain_id_map = BKE_main_idmap_create(&bmain, false, nullptr, MAIN_IDMAP_TYPE_NAME);
}
Library *new_id_lib = libraries_path_to_id.lookup_default(id_info.library_path, nullptr);
if (id_info.library_path.empty() || new_id_lib) {
id_info.new_id = BKE_main_idmap_lookup_name(
bmain_id_map, GS(id_info.id_name.c_str()), id_info.id_name.c_str() + 2, new_id_lib);
}
else {
/* No matching library found, so there is no possible matching ID either. */
id_info.new_id = nullptr;
}
}
if (*(id_info.new_id) == nullptr) {
is_valid = false;
}
return IDWALK_RET_NOP;
};
for (NodeClipboardItem &item : this->nodes) {
BKE_library_foreach_subdata_id(
&bmain,
nullptr,
nullptr,
[&item](LibraryForeachIDData *data) { bke::node_node_foreach_id(item.node, data); },
validate_id_fn,
nullptr,
IDWALK_READONLY);
}
if (bmain_id_map) {
BKE_main_idmap_destroy(bmain_id_map);
bmain_id_map = nullptr;
}
return is_valid;
}
/**
* Ensure that a newly pasted copy of a node from the clipboard has valid ID references, as
* ensured by #paste_validate_id_references.
*/
void paste_update_node_id_references(bNode &node)
{
/* Update all old ID pointers in given node by new, valid ones. */
auto update_id_fn = [this](LibraryIDLinkCallbackData *cb_data) -> int {
ID *old_id = *(cb_data->id_pointer);
if (!old_id) {
return IDWALK_RET_NOP;
}
if (!this->old_ids_to_idinfo.contains(old_id)) {
BLI_assert_msg(
0, "Missing entry in the old ID data of the node clipboard, should not happen");
*(cb_data->id_pointer) = nullptr;
return IDWALK_RET_NOP;
}
NodeClipboardItemIDInfo &id_info = this->old_ids_to_idinfo.lookup(old_id);
if (!id_info.new_id) {
BLI_assert_msg(
0,
"Unset new ID value for an old ID reference in the node clipboard, should not happen");
*(cb_data->id_pointer) = nullptr;
return IDWALK_RET_NOP;
}
*(cb_data->id_pointer) = *(id_info.new_id);
if (cb_data->cb_flag & IDWALK_CB_USER) {
id_us_plus(*(cb_data->id_pointer));
}
return IDWALK_RET_NOP;
};
BKE_library_foreach_subdata_id(
nullptr,
nullptr,
nullptr,
[&node](LibraryForeachIDData *data) { bke::node_node_foreach_id(&node, data); },
update_id_fn,
nullptr,
IDWALK_NOP);
}
/** Add a new node to the clipboard. */
void copy_add_node(const bNode &node,
Map<const bNode *, bNode *> &node_map,
Map<const bNodeSocket *, bNodeSocket *> &socket_map)
{
/* No ID reference-counting, this node is virtual,
* detached from any actual Blender data currently. */
bNode *new_node = bke::node_copy_with_mapping(
nullptr, node, LIB_ID_CREATE_NO_USER_REFCOUNT | LIB_ID_CREATE_NO_MAIN, false, socket_map);
node_map.add_new(&node, new_node);
/* Find a new valid ID pointer for all ID usages in given node. */
auto ensure_id_info_fn = [this](LibraryIDLinkCallbackData *cb_data) -> int {
ID *old_id = *(cb_data->id_pointer);
if (!old_id) {
}
if (this->old_ids_to_idinfo.contains(old_id)) {
return IDWALK_RET_NOP;
}
NodeClipboardItemIDInfo id_info;
if (old_id) {
id_info.id_name = old_id->name;
if (ID_IS_LINKED(old_id)) {
id_info.library_path = old_id->lib->runtime->filepath_abs;
}
}
this->old_ids_to_idinfo.add(old_id, std::move(id_info));
return IDWALK_RET_NOP;
};
BKE_library_foreach_subdata_id(
nullptr,
nullptr,
nullptr,
[&node](LibraryForeachIDData *data) {
bke::node_node_foreach_id(const_cast<bNode *>(&node), data);
},
ensure_id_info_fn,
nullptr,
IDWALK_READONLY);
NodeClipboardItem item;
item.draw_rect = node.runtime->draw_bounds;
item.node = new_node;
this->nodes.append(std::move(item));
}
};
static NodeClipboard &get_node_clipboard()
{
static NodeClipboard clipboard;
return clipboard;
}
/* -------------------------------------------------------------------- */
/** \name Copy
* \{ */
static int node_clipboard_copy_exec(bContext *C, wmOperator * /*op*/)
{
SpaceNode &snode = *CTX_wm_space_node(C);
bNodeTree &tree = *snode.edittree;
NodeClipboard &clipboard = get_node_clipboard();
clipboard.clear();
Map<const bNode *, bNode *> node_map;
Map<const bNodeSocket *, bNodeSocket *> socket_map;
node_select_paired(tree);
for (const bNode *node : tree.all_nodes()) {
if (node->flag & SELECT) {
clipboard.copy_add_node(*node, node_map, socket_map);
}
}
for (bNode *new_node : node_map.values()) {
/* Parent pointer must be redirected to new node or detached if parent is not copied. */
if (new_node->parent) {
if (node_map.contains(new_node->parent)) {
new_node->parent = node_map.lookup(new_node->parent);
}
else {
bke::node_detach_node(&tree, new_node);
}
}
}
/* Copy links between selected nodes. */
LISTBASE_FOREACH (bNodeLink *, link, &tree.links) {
BLI_assert(link->tonode);
BLI_assert(link->fromnode);
if (link->tonode->flag & NODE_SELECT && link->fromnode->flag & NODE_SELECT) {
clipboard.links.append({});
ClipboardLink &new_link = clipboard.links.last();
new_link.flag = link->flag;
new_link.to_node = node_map.lookup(link->tonode);
new_link.from_node = node_map.lookup(link->fromnode);
new_link.to_socket = link->tosock->identifier;
new_link.from_socket = link->fromsock->identifier;
new_link.multi_input_sort_id = link->multi_input_sort_id;
}
}
return OPERATOR_FINISHED;
}
void NODE_OT_clipboard_copy(wmOperatorType *ot)
{
ot->name = "Copy to Clipboard";
ot->description = "Copy the selected nodes to the internal clipboard";
ot->idname = "NODE_OT_clipboard_copy";
ot->exec = node_clipboard_copy_exec;
ot->poll = ED_operator_node_active;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Paste
* \{ */
static int node_clipboard_paste_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
SpaceNode &snode = *CTX_wm_space_node(C);
bNodeTree &tree = *snode.edittree;
NodeClipboard &clipboard = get_node_clipboard();
if (clipboard.nodes.is_empty()) {
BKE_report(op->reports, RPT_ERROR, "The internal clipboard is empty");
return OPERATOR_CANCELLED;
}
if (!clipboard.paste_validate_id_references(*bmain)) {
BKE_report(op->reports,
RPT_WARNING,
"Some nodes references to other IDs could not be restored, will be left empty");
}
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
node_deselect_all(tree);
Map<const bNode *, bNode *> node_map;
Map<const bNodeSocket *, bNodeSocket *> socket_map;
/* copy valid nodes from clipboard */
for (NodeClipboardItem &item : clipboard.nodes) {
const bNode &node = *item.node;
const char *disabled_hint = nullptr;
if (node.typeinfo->poll_instance && node.typeinfo->poll_instance(&node, &tree, &disabled_hint))
{
/* Do not access referenced ID pointers here, as they are still the old ones, which may be
* invalid. */
bNode *new_node = bke::node_copy_with_mapping(
&tree, node, LIB_ID_CREATE_NO_USER_REFCOUNT, true, socket_map);
/* Update the newly copied node's ID references. */
clipboard.paste_update_node_id_references(*new_node);
/* Reset socket shape in case a node is copied to a different tree type. */
LISTBASE_FOREACH (bNodeSocket *, socket, &new_node->inputs) {
socket->display_shape = SOCK_DISPLAY_SHAPE_CIRCLE;
}
LISTBASE_FOREACH (bNodeSocket *, socket, &new_node->outputs) {
socket->display_shape = SOCK_DISPLAY_SHAPE_CIRCLE;
}
node_map.add_new(&node, new_node);
}
else {
if (disabled_hint) {
BKE_reportf(op->reports,
RPT_ERROR,
"Cannot add node %s into node tree %s: %s",
node.name,
tree.id.name + 2,
disabled_hint);
}
else {
BKE_reportf(op->reports,
RPT_ERROR,
"Cannot add node %s into node tree %s",
node.name,
tree.id.name + 2);
}
}
}
for (bNode *new_node : node_map.values()) {
bke::node_set_selected(new_node, true);
new_node->flag &= ~NODE_ACTIVE;
/* The parent pointer must be redirected to new node. */
if (new_node->parent) {
if (node_map.contains(new_node->parent)) {
new_node->parent = node_map.lookup(new_node->parent);
}
}
}
PropertyRNA *offset_prop = RNA_struct_find_property(op->ptr, "offset");
if (RNA_property_is_set(op->ptr, offset_prop)) {
float2 center(0);
for (NodeClipboardItem &item : clipboard.nodes) {
center.x += BLI_rctf_cent_x(&item.draw_rect);
center.y += BLI_rctf_cent_y(&item.draw_rect);
}
/* DPI factor needs to be removed when computing a View2D offset from drawing rects. */
center /= clipboard.nodes.size();
float2 mouse_location;
RNA_property_float_get_array(op->ptr, offset_prop, mouse_location);
const float2 offset = (mouse_location - center) / UI_SCALE_FAC;
for (bNode *new_node : node_map.values()) {
/* Skip the offset for parented nodes since the location is in parent space. */
if (new_node->parent == nullptr) {
new_node->location[0] += offset.x;
new_node->location[1] += offset.y;
}
}
}
remap_node_pairing(tree, node_map);
for (bNode *new_node : node_map.values()) {
bke::node_declaration_ensure(&tree, new_node);
}
/* Add links between existing nodes. */
for (const ClipboardLink &link : clipboard.links) {
bNode *from_node = node_map.lookup_default(link.from_node, nullptr);
bNode *to_node = node_map.lookup_default(link.to_node, nullptr);
if (!from_node || !to_node) {
continue;
}
bNodeSocket *from = bke::node_find_socket(from_node, SOCK_OUT, link.from_socket.c_str());
bNodeSocket *to = bke::node_find_socket(to_node, SOCK_IN, link.to_socket.c_str());
if (!from || !to) {
continue;
}
bNodeLink *new_link = bke::node_add_link(&tree, from_node, from, to_node, to);
new_link->multi_input_sort_id = link.multi_input_sort_id;
}
tree.ensure_topology_cache();
for (bNode *new_node : node_map.values()) {
/* Update multi input socket indices in case all connected nodes weren't copied. */
update_multi_input_indices_for_removed_links(*new_node);
}
BKE_main_ensure_invariants(*bmain);
/* Pasting nodes can create arbitrary new relations because nodes can reference IDs. */
DEG_relations_tag_update(bmain);
return OPERATOR_FINISHED;
}
static int node_clipboard_paste_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
const ARegion *region = CTX_wm_region(C);
float2 cursor;
UI_view2d_region_to_view(&region->v2d, event->mval[0], event->mval[1], &cursor.x, &cursor.y);
RNA_float_set_array(op->ptr, "offset", cursor);
return node_clipboard_paste_exec(C, op);
}
void NODE_OT_clipboard_paste(wmOperatorType *ot)
{
ot->name = "Paste from Clipboard";
ot->description = "Paste nodes from the internal clipboard to the active node tree";
ot->idname = "NODE_OT_clipboard_paste";
ot->invoke = node_clipboard_paste_invoke;
ot->exec = node_clipboard_paste_exec;
ot->poll = ED_operator_node_editable;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
PropertyRNA *prop = RNA_def_float_array(
ot->srna,
"offset",
2,
nullptr,
-FLT_MAX,
FLT_MAX,
"Location",
"The 2D view location for the center of the new nodes, or unchanged if not set",
-FLT_MAX,
FLT_MAX);
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
RNA_def_property_flag(prop, PROP_HIDDEN);
}
/** \} */
} // namespace blender::ed::space_node
void ED_node_clipboard_free()
{
using namespace blender::ed::space_node;
NodeClipboard &clipboard = get_node_clipboard();
clipboard.clear();
}