When pasting nodes with the shortcut or the context menu, place the center of the selected nodes at the same position as the mouse cursor. This should save time, and is more intuitive because the new nodes are actually visible. Based on a patch by Juanfran Matheu (@jfmatheu). Differential Revision: https://developer.blender.org/D10787
347 lines
9.9 KiB
C++
347 lines
9.9 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#include "DNA_space_types.h"
|
|
|
|
#include "BKE_context.h"
|
|
#include "BKE_global.h"
|
|
#include "BKE_lib_id.h"
|
|
#include "BKE_main.h"
|
|
#include "BKE_node.h"
|
|
#include "BKE_node_runtime.hh"
|
|
#include "BKE_node_tree_update.h"
|
|
#include "BKE_report.h"
|
|
|
|
#include "ED_node.h"
|
|
#include "ED_node.hh"
|
|
#include "ED_render.h"
|
|
#include "ED_screen.h"
|
|
|
|
#include "RNA_access.h"
|
|
#include "RNA_define.h"
|
|
|
|
#include "DEG_depsgraph_build.h"
|
|
|
|
#include "node_intern.hh"
|
|
|
|
namespace blender::ed::space_node {
|
|
|
|
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 node on creation. Otherwise we may reference missing data. */
|
|
ID *id;
|
|
std::string id_name;
|
|
std::string library_name;
|
|
};
|
|
|
|
struct NodeClipboard {
|
|
Vector<NodeClipboardItem> nodes;
|
|
Vector<bNodeLink> links;
|
|
|
|
void clear()
|
|
{
|
|
for (NodeClipboardItem &item : this->nodes) {
|
|
bke::node_free_node(nullptr, item.node);
|
|
}
|
|
this->nodes.clear_and_shrink();
|
|
this->links.clear_and_shrink();
|
|
}
|
|
|
|
/**
|
|
* Replace node IDs that are no longer available in the current file. Return false when one or
|
|
* more IDs are lost.
|
|
*/
|
|
bool validate()
|
|
{
|
|
bool ok = true;
|
|
|
|
for (NodeClipboardItem &item : this->nodes) {
|
|
bNode &node = *item.node;
|
|
/* Reassign each loop since we may clear, open a new file where the ID is valid, and paste
|
|
* again. */
|
|
node.id = item.id;
|
|
|
|
if (node.id) {
|
|
const ListBase *lb = which_libbase(G_MAIN, GS(item.id_name.c_str()));
|
|
if (BLI_findindex(lb, item.id) == -1) {
|
|
/* May assign null. */
|
|
node.id = static_cast<ID *>(
|
|
BLI_findstring(lb, item.id_name.c_str() + 2, offsetof(ID, name) + 2));
|
|
if (!node.id) {
|
|
ok = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
void add_node(const bNode &node,
|
|
Map<const bNode *, bNode *> &node_map,
|
|
Map<const bNodeSocket *, bNodeSocket *> &socket_map)
|
|
{
|
|
/* No ID refcounting, 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);
|
|
|
|
NodeClipboardItem item;
|
|
item.draw_rect = node.runtime->totr;
|
|
item.node = new_node;
|
|
item.id = new_node->id;
|
|
if (item.id) {
|
|
item.id_name = new_node->id->name;
|
|
if (ID_IS_LINKED(new_node->id)) {
|
|
item.library_name = new_node->id->lib->filepath_abs;
|
|
}
|
|
}
|
|
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;
|
|
|
|
for (const bNode *node : tree.all_nodes()) {
|
|
if (node->flag & SELECT) {
|
|
clipboard.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 {
|
|
nodeDetachNode(&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) {
|
|
bNodeLink new_link{};
|
|
new_link.flag = link->flag;
|
|
new_link.tonode = node_map.lookup(link->tonode);
|
|
new_link.tosock = socket_map.lookup(link->tosock);
|
|
new_link.fromnode = node_map.lookup(link->fromnode);
|
|
new_link.fromsock = socket_map.lookup(link->fromsock);
|
|
new_link.multi_input_socket_index = link->multi_input_socket_index;
|
|
clipboard.links.append(new_link);
|
|
}
|
|
}
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_clipboard_copy(wmOperatorType *ot)
|
|
{
|
|
ot->name = "Copy to Clipboard";
|
|
ot->description = "Copies selected nodes to the 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)
|
|
{
|
|
SpaceNode &snode = *CTX_wm_space_node(C);
|
|
bNodeTree &tree = *snode.edittree;
|
|
NodeClipboard &clipboard = get_node_clipboard();
|
|
|
|
const bool is_valid = clipboard.validate();
|
|
|
|
if (clipboard.nodes.is_empty()) {
|
|
BKE_report(op->reports, RPT_ERROR, "Clipboard is empty");
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
if (!is_valid) {
|
|
BKE_report(op->reports,
|
|
RPT_WARNING,
|
|
"Some nodes references 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)) {
|
|
bNode *new_node = bke::node_copy_with_mapping(
|
|
&tree, node, LIB_ID_COPY_DEFAULT, true, socket_map);
|
|
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()) {
|
|
nodeSetSelected(new_node, true);
|
|
|
|
/* 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();
|
|
center /= UI_DPI_FAC;
|
|
|
|
float2 mouse_location;
|
|
RNA_property_float_get_array(op->ptr, offset_prop, mouse_location);
|
|
const float2 offset = mouse_location - center;
|
|
|
|
for (bNode *new_node : node_map.values()) {
|
|
new_node->locx += offset.x;
|
|
new_node->locy += offset.y;
|
|
}
|
|
}
|
|
|
|
/* Add links between existing nodes. */
|
|
for (const bNodeLink &link : clipboard.links) {
|
|
const bNode *fromnode = link.fromnode;
|
|
const bNode *tonode = link.tonode;
|
|
if (node_map.lookup_key_ptr(fromnode) && node_map.lookup_key_ptr(tonode)) {
|
|
bNodeLink *new_link = nodeAddLink(&tree,
|
|
node_map.lookup(fromnode),
|
|
socket_map.lookup(link.fromsock),
|
|
node_map.lookup(tonode),
|
|
socket_map.lookup(link.tosock));
|
|
new_link->multi_input_socket_index = link.multi_input_socket_index;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
Main *bmain = CTX_data_main(C);
|
|
ED_node_tree_propagate_change(C, bmain, &tree);
|
|
/* 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(®ion->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 = "Pastes nodes from the 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.validate();
|
|
clipboard.clear();
|
|
}
|