This implements the update logic for the vizualization of which
sockets pass data or constants directly, and which pass functions.
The socket shapes may still have to be updated. That should be
done separately, because it might be a bit more involved, because
socket shapes are currently linked to keyframe shapes. Currently
the circle and diamond shapes are used with the following meanings:
- Input Sockets:
- Circle: Required to be a single value.
- Diamond: This input supports fields.
- Output Sockets:
- Circle: This output is a single value.
- Diamond: This output may be a field.
Connecting a field to a circle input socket is an error, since a
field cannot be converted to a single value. If the socket shape
is a diamond with a dot in the middle, it means it is currently
a single value, but could be a field.
In addition to socket shapes, the intention is to draw node links
differently based on the field status. However, the exact method for
conveying that isn't decided yet.
Differential Revision: https://developer.blender.org/D12584
2478 lines
72 KiB
C++
2478 lines
72 KiB
C++
/*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* The Original Code is Copyright (C) 2005 Blender Foundation.
|
|
* All rights reserved.
|
|
*/
|
|
|
|
/** \file
|
|
* \ingroup spnode
|
|
*/
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "DNA_anim_types.h"
|
|
#include "DNA_node_types.h"
|
|
|
|
#include "BLI_blenlib.h"
|
|
#include "BLI_easing.h"
|
|
#include "BLI_math.h"
|
|
|
|
#include "BKE_anim_data.h"
|
|
#include "BKE_context.h"
|
|
#include "BKE_curve.h"
|
|
#include "BKE_lib_id.h"
|
|
#include "BKE_main.h"
|
|
#include "BKE_node.h"
|
|
#include "BKE_screen.h"
|
|
|
|
#include "ED_node.h" /* own include */
|
|
#include "ED_render.h"
|
|
#include "ED_screen.h"
|
|
#include "ED_spreadsheet.h"
|
|
#include "ED_util.h"
|
|
|
|
#include "RNA_access.h"
|
|
#include "RNA_define.h"
|
|
|
|
#include "DEG_depsgraph.h"
|
|
|
|
#include "WM_api.h"
|
|
#include "WM_types.h"
|
|
|
|
#include "UI_resources.h"
|
|
#include "UI_view2d.h"
|
|
|
|
#include "BLT_translation.h"
|
|
|
|
#include "node_intern.h" /* own include */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Relations Helpers
|
|
* \{ */
|
|
|
|
static bool ntree_has_drivers(bNodeTree *ntree)
|
|
{
|
|
const AnimData *adt = BKE_animdata_from_id(&ntree->id);
|
|
if (adt == nullptr) {
|
|
return false;
|
|
}
|
|
return !BLI_listbase_is_empty(&adt->drivers);
|
|
}
|
|
|
|
static bool ntree_check_nodes_connected_dfs(bNodeTree *ntree, bNode *from, bNode *to)
|
|
{
|
|
if (from->flag & NODE_TEST) {
|
|
return false;
|
|
}
|
|
from->flag |= NODE_TEST;
|
|
LISTBASE_FOREACH (bNodeLink *, link, &ntree->links) {
|
|
if (link->fromnode == from) {
|
|
if (link->tonode == to) {
|
|
return true;
|
|
}
|
|
|
|
if (ntree_check_nodes_connected_dfs(ntree, link->tonode, to)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool ntree_check_nodes_connected(bNodeTree *ntree, bNode *from, bNode *to)
|
|
{
|
|
if (from == to) {
|
|
return true;
|
|
}
|
|
ntreeNodeFlagSet(ntree, NODE_TEST, false);
|
|
return ntree_check_nodes_connected_dfs(ntree, from, to);
|
|
}
|
|
|
|
static bool node_group_has_output_dfs(bNode *node)
|
|
{
|
|
bNodeTree *ntree = (bNodeTree *)node->id;
|
|
if (ntree->id.tag & LIB_TAG_DOIT) {
|
|
return false;
|
|
}
|
|
ntree->id.tag |= LIB_TAG_DOIT;
|
|
for (bNode *current_node = (bNode *)ntree->nodes.first; current_node != nullptr;
|
|
current_node = current_node->next) {
|
|
if (current_node->type == NODE_GROUP) {
|
|
if (current_node->id && node_group_has_output_dfs(current_node)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (current_node->flag & NODE_DO_OUTPUT && current_node->type != NODE_GROUP_OUTPUT) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool node_group_has_output(Main *bmain, bNode *node)
|
|
{
|
|
BLI_assert(ELEM(node->type, NODE_GROUP, NODE_CUSTOM_GROUP));
|
|
bNodeTree *ntree = (bNodeTree *)node->id;
|
|
if (ntree == nullptr) {
|
|
return false;
|
|
}
|
|
BKE_main_id_tag_listbase(&bmain->nodetrees, LIB_TAG_DOIT, false);
|
|
return node_group_has_output_dfs(node);
|
|
}
|
|
|
|
bool node_connected_to_output(Main *bmain, bNodeTree *ntree, bNode *node)
|
|
{
|
|
/* Special case for drivers: if node tree has any drivers we assume it is
|
|
* always to be tagged for update when node changes. Otherwise we will be
|
|
* doomed to do some deep and nasty deep search of indirect dependencies,
|
|
* which will be too complicated without real benefit.
|
|
*/
|
|
if (ntree_has_drivers(ntree)) {
|
|
return true;
|
|
}
|
|
LISTBASE_FOREACH (bNode *, current_node, &ntree->nodes) {
|
|
/* Special case for group nodes -- if modified node connected to a group
|
|
* with active output inside we consider refresh is needed.
|
|
*
|
|
* We could make check more grained here by taking which socket the node
|
|
* is connected to and so eventually.
|
|
*/
|
|
if (ELEM(current_node->type, NODE_GROUP, NODE_CUSTOM_GROUP)) {
|
|
if (current_node->id != nullptr && ntree_has_drivers((bNodeTree *)current_node->id)) {
|
|
return true;
|
|
}
|
|
if (ntree_check_nodes_connected(ntree, node, current_node) &&
|
|
node_group_has_output(bmain, current_node)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (current_node->flag & NODE_DO_OUTPUT) {
|
|
if (ntree_check_nodes_connected(ntree, node, current_node)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (current_node->type == GEO_NODE_VIEWER) {
|
|
if (ntree_check_nodes_connected(ntree, node, current_node)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Add Node
|
|
* \{ */
|
|
|
|
struct bNodeListItem {
|
|
struct bNodeListItem *next, *prev;
|
|
struct bNode *node;
|
|
};
|
|
|
|
struct NodeInsertOfsData {
|
|
bNodeTree *ntree;
|
|
bNode *insert; /* inserted node */
|
|
bNode *prev, *next; /* prev/next node in the chain */
|
|
bNode *insert_parent;
|
|
|
|
wmTimer *anim_timer;
|
|
|
|
float offset_x; /* offset to apply to node chain */
|
|
};
|
|
|
|
static void clear_picking_highlight(ListBase *links)
|
|
{
|
|
LISTBASE_FOREACH (bNodeLink *, link, links) {
|
|
link->flag &= ~NODE_LINK_TEMP_HIGHLIGHT;
|
|
}
|
|
}
|
|
|
|
static LinkData *create_drag_link(Main *bmain, SpaceNode *snode, bNode *node, bNodeSocket *sock)
|
|
{
|
|
LinkData *linkdata = (LinkData *)MEM_callocN(sizeof(LinkData), "drag link op link data");
|
|
bNodeLink *oplink = (bNodeLink *)MEM_callocN(sizeof(bNodeLink), "drag link op link");
|
|
linkdata->data = oplink;
|
|
if (sock->in_out == SOCK_OUT) {
|
|
oplink->fromnode = node;
|
|
oplink->fromsock = sock;
|
|
}
|
|
else {
|
|
oplink->tonode = node;
|
|
oplink->tosock = sock;
|
|
}
|
|
oplink->flag |= NODE_LINK_VALID;
|
|
oplink->flag &= ~NODE_LINK_TEST;
|
|
if (node_connected_to_output(bmain, snode->edittree, node)) {
|
|
oplink->flag |= NODE_LINK_TEST;
|
|
}
|
|
oplink->flag |= NODE_LINK_DRAGGED;
|
|
return linkdata;
|
|
}
|
|
|
|
static void pick_link(const bContext *C,
|
|
wmOperator *op,
|
|
bNodeLinkDrag *nldrag,
|
|
SpaceNode *snode,
|
|
bNode *node,
|
|
bNodeLink *link_to_pick)
|
|
{
|
|
clear_picking_highlight(&snode->edittree->links);
|
|
RNA_boolean_set(op->ptr, "has_link_picked", true);
|
|
|
|
Main *bmain = CTX_data_main(C);
|
|
LinkData *linkdata = create_drag_link(
|
|
bmain, snode, link_to_pick->fromnode, link_to_pick->fromsock);
|
|
|
|
BLI_addtail(&nldrag->links, linkdata);
|
|
nodeRemLink(snode->edittree, link_to_pick);
|
|
|
|
BLI_assert(nldrag->last_node_hovered_while_dragging_a_link != nullptr);
|
|
|
|
sort_multi_input_socket_links(
|
|
snode, nldrag->last_node_hovered_while_dragging_a_link, nullptr, nullptr);
|
|
|
|
/* Send changed event to original link->tonode. */
|
|
if (node) {
|
|
snode_update(snode, node);
|
|
}
|
|
}
|
|
|
|
static void pick_input_link_by_link_intersect(const bContext *C,
|
|
wmOperator *op,
|
|
bNodeLinkDrag *nldrag,
|
|
const float *cursor)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
const ARegion *region = CTX_wm_region(C);
|
|
const View2D *v2d = ®ion->v2d;
|
|
|
|
float drag_start[2];
|
|
RNA_float_get_array(op->ptr, "drag_start", drag_start);
|
|
bNode *node;
|
|
bNodeSocket *socket;
|
|
node_find_indicated_socket(snode, &node, &socket, drag_start, SOCK_IN);
|
|
|
|
/* Distance to test overlapping of cursor on link. */
|
|
const float cursor_link_touch_distance = 12.5f * UI_DPI_FAC;
|
|
|
|
const int resolution = NODE_LINK_RESOL;
|
|
|
|
bNodeLink *link_to_pick = nullptr;
|
|
clear_picking_highlight(&snode->edittree->links);
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
if (link->tosock == socket) {
|
|
/* Test if the cursor is near a link. */
|
|
float vec[4][2];
|
|
node_link_bezier_handles(v2d, snode, link, vec);
|
|
|
|
float data[NODE_LINK_RESOL * 2 + 2];
|
|
BKE_curve_forward_diff_bezier(
|
|
vec[0][0], vec[1][0], vec[2][0], vec[3][0], data, resolution, sizeof(float[2]));
|
|
BKE_curve_forward_diff_bezier(
|
|
vec[0][1], vec[1][1], vec[2][1], vec[3][1], data + 1, resolution, sizeof(float[2]));
|
|
|
|
for (int i = 0; i < resolution * 2; i += 2) {
|
|
float *l1 = &data[i];
|
|
float *l2 = &data[i + 2];
|
|
float distance = dist_squared_to_line_segment_v2(cursor, l1, l2);
|
|
if (distance < cursor_link_touch_distance) {
|
|
link_to_pick = link;
|
|
nldrag->last_picked_multi_input_socket_link = link_to_pick;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If no linked was picked in this call, try using the one picked in the previous call.
|
|
* Not essential for the basic behavior, but can make interaction feel a bit better if
|
|
* the mouse moves to the right and loses the "selection." */
|
|
if (!link_to_pick) {
|
|
bNodeLink *last_picked_link = nldrag->last_picked_multi_input_socket_link;
|
|
if (last_picked_link) {
|
|
link_to_pick = last_picked_link;
|
|
}
|
|
}
|
|
|
|
if (link_to_pick) {
|
|
/* Highlight is set here and cleared in the next iteration or if the operation finishes. */
|
|
link_to_pick->flag |= NODE_LINK_TEMP_HIGHLIGHT;
|
|
ED_area_tag_redraw(CTX_wm_area(C));
|
|
|
|
if (!node_find_indicated_socket(snode, &node, &socket, cursor, SOCK_IN)) {
|
|
pick_link(C, op, nldrag, snode, node, link_to_pick);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int sort_nodes_locx(const void *a, const void *b)
|
|
{
|
|
const bNodeListItem *nli1 = (const bNodeListItem *)a;
|
|
const bNodeListItem *nli2 = (const bNodeListItem *)b;
|
|
const bNode *node1 = nli1->node;
|
|
const bNode *node2 = nli2->node;
|
|
|
|
if (node1->locx > node2->locx) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static bool socket_is_available(bNodeTree *UNUSED(ntree), bNodeSocket *sock, const bool allow_used)
|
|
{
|
|
if (nodeSocketIsHidden(sock)) {
|
|
return false;
|
|
}
|
|
|
|
if (!allow_used && (sock->flag & SOCK_IN_USE)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bNodeSocket *best_socket_output(bNodeTree *ntree,
|
|
bNode *node,
|
|
bNodeSocket *sock_target,
|
|
const bool allow_multiple)
|
|
{
|
|
/* first look for selected output */
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, &node->outputs) {
|
|
if (!socket_is_available(ntree, sock, allow_multiple)) {
|
|
continue;
|
|
}
|
|
|
|
if (sock->flag & SELECT) {
|
|
return sock;
|
|
}
|
|
}
|
|
|
|
/* try to find a socket with a matching name */
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, &node->outputs) {
|
|
if (!socket_is_available(ntree, sock, allow_multiple)) {
|
|
continue;
|
|
}
|
|
|
|
/* check for same types */
|
|
if (sock->type == sock_target->type) {
|
|
if (STREQ(sock->name, sock_target->name)) {
|
|
return sock;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* otherwise settle for the first available socket of the right type */
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, &node->outputs) {
|
|
if (!socket_is_available(ntree, sock, allow_multiple)) {
|
|
continue;
|
|
}
|
|
|
|
/* check for same types */
|
|
if (sock->type == sock_target->type) {
|
|
return sock;
|
|
}
|
|
}
|
|
|
|
/* Always allow linking to an reroute node. The socket type of the reroute sockets might change
|
|
* after the link has been created. */
|
|
if (node->type == NODE_REROUTE) {
|
|
return (bNodeSocket *)node->outputs.first;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/* this is a bit complicated, but designed to prioritize finding
|
|
* sockets of higher types, such as image, first */
|
|
static bNodeSocket *best_socket_input(bNodeTree *ntree, bNode *node, int num, int replace)
|
|
{
|
|
int maxtype = 0;
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) {
|
|
maxtype = max_ii(sock->type, maxtype);
|
|
}
|
|
|
|
/* find sockets of higher 'types' first (i.e. image) */
|
|
int a = 0;
|
|
for (int socktype = maxtype; socktype >= 0; socktype--) {
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) {
|
|
if (!socket_is_available(ntree, sock, replace)) {
|
|
a++;
|
|
continue;
|
|
}
|
|
|
|
if (sock->type == socktype) {
|
|
/* increment to make sure we don't keep finding
|
|
* the same socket on every attempt running this function */
|
|
a++;
|
|
if (a > num) {
|
|
return sock;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static bool snode_autoconnect_input(SpaceNode *snode,
|
|
bNode *node_fr,
|
|
bNodeSocket *sock_fr,
|
|
bNode *node_to,
|
|
bNodeSocket *sock_to,
|
|
int replace)
|
|
{
|
|
bNodeTree *ntree = snode->edittree;
|
|
|
|
/* then we can connect */
|
|
if (replace) {
|
|
nodeRemSocketLinks(ntree, sock_to);
|
|
}
|
|
|
|
nodeAddLink(ntree, node_fr, sock_fr, node_to, sock_to);
|
|
return true;
|
|
}
|
|
|
|
struct LinkAndPosition {
|
|
struct bNodeLink *link;
|
|
float multi_socket_position[2];
|
|
};
|
|
|
|
static int compare_link_by_y_position(const void *a, const void *b)
|
|
{
|
|
const LinkAndPosition *link_and_position_a = *(const LinkAndPosition **)a;
|
|
const LinkAndPosition *link_and_position_b = *(const LinkAndPosition **)b;
|
|
|
|
BLI_assert(link_and_position_a->link->tosock == link_and_position_b->link->tosock);
|
|
const float link_a_y = link_and_position_a->multi_socket_position[1];
|
|
const float link_b_y = link_and_position_b->multi_socket_position[1];
|
|
return link_a_y > link_b_y ? 1 : -1;
|
|
}
|
|
|
|
void sort_multi_input_socket_links(SpaceNode *snode,
|
|
bNode *node,
|
|
bNodeLink *drag_link,
|
|
float cursor[2])
|
|
{
|
|
LISTBASE_FOREACH (bNodeSocket *, socket, &node->inputs) {
|
|
if (!(socket->flag & SOCK_MULTI_INPUT)) {
|
|
continue;
|
|
}
|
|
/* The total is calculated in #node_update_nodetree, which runs before this draw step. */
|
|
int total_inputs = socket->total_inputs + 1;
|
|
struct LinkAndPosition **input_links = (LinkAndPosition **)MEM_malloc_arrayN(
|
|
total_inputs, sizeof(LinkAndPosition *), __func__);
|
|
|
|
int index = 0;
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
if (link->tosock == socket) {
|
|
struct LinkAndPosition *link_and_position = (LinkAndPosition *)MEM_callocN(
|
|
sizeof(struct LinkAndPosition), __func__);
|
|
link_and_position->link = link;
|
|
node_link_calculate_multi_input_position(link->tosock->locx,
|
|
link->tosock->locy,
|
|
link->multi_input_socket_index,
|
|
link->tosock->total_inputs,
|
|
link_and_position->multi_socket_position);
|
|
input_links[index] = link_and_position;
|
|
index++;
|
|
}
|
|
}
|
|
|
|
if (drag_link) {
|
|
LinkAndPosition *link_and_position = (LinkAndPosition *)MEM_callocN(sizeof(LinkAndPosition),
|
|
__func__);
|
|
link_and_position->link = drag_link;
|
|
copy_v2_v2(link_and_position->multi_socket_position, cursor);
|
|
input_links[index] = link_and_position;
|
|
index++;
|
|
}
|
|
|
|
qsort(input_links, index, sizeof(bNodeLink *), compare_link_by_y_position);
|
|
|
|
for (int i = 0; i < index; i++) {
|
|
input_links[i]->link->multi_input_socket_index = i;
|
|
}
|
|
|
|
for (int i = 0; i < index; i++) {
|
|
if (input_links[i]) {
|
|
MEM_freeN(input_links[i]);
|
|
}
|
|
}
|
|
MEM_freeN(input_links);
|
|
}
|
|
}
|
|
|
|
static void snode_autoconnect(Main *bmain,
|
|
SpaceNode *snode,
|
|
const bool allow_multiple,
|
|
const bool replace)
|
|
{
|
|
bNodeTree *ntree = snode->edittree;
|
|
ListBase *nodelist = (ListBase *)MEM_callocN(sizeof(ListBase), "items_list");
|
|
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (node->flag & NODE_SELECT) {
|
|
bNodeListItem *nli = (bNodeListItem *)MEM_mallocN(sizeof(bNodeListItem),
|
|
"temporary node list item");
|
|
nli->node = node;
|
|
BLI_addtail(nodelist, nli);
|
|
}
|
|
}
|
|
|
|
/* sort nodes left to right */
|
|
BLI_listbase_sort(nodelist, sort_nodes_locx);
|
|
|
|
int numlinks = 0;
|
|
LISTBASE_FOREACH (bNodeListItem *, nli, nodelist) {
|
|
bool has_selected_inputs = false;
|
|
|
|
if (nli->next == nullptr) {
|
|
break;
|
|
}
|
|
|
|
bNode *node_fr = nli->node;
|
|
bNode *node_to = nli->next->node;
|
|
/* corner case: input/output node aligned the wrong way around (T47729) */
|
|
if (BLI_listbase_is_empty(&node_to->inputs) || BLI_listbase_is_empty(&node_fr->outputs)) {
|
|
SWAP(bNode *, node_fr, node_to);
|
|
}
|
|
|
|
/* if there are selected sockets, connect those */
|
|
LISTBASE_FOREACH (bNodeSocket *, sock_to, &node_to->inputs) {
|
|
if (sock_to->flag & SELECT) {
|
|
has_selected_inputs = true;
|
|
|
|
if (!socket_is_available(ntree, sock_to, replace)) {
|
|
continue;
|
|
}
|
|
|
|
/* check for an appropriate output socket to connect from */
|
|
bNodeSocket *sock_fr = best_socket_output(ntree, node_fr, sock_to, allow_multiple);
|
|
if (!sock_fr) {
|
|
continue;
|
|
}
|
|
|
|
if (snode_autoconnect_input(snode, node_fr, sock_fr, node_to, sock_to, replace)) {
|
|
numlinks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!has_selected_inputs) {
|
|
/* no selected inputs, connect by finding suitable match */
|
|
int num_inputs = BLI_listbase_count(&node_to->inputs);
|
|
|
|
for (int i = 0; i < num_inputs; i++) {
|
|
|
|
/* find the best guess input socket */
|
|
bNodeSocket *sock_to = best_socket_input(ntree, node_to, i, replace);
|
|
if (!sock_to) {
|
|
continue;
|
|
}
|
|
|
|
/* check for an appropriate output socket to connect from */
|
|
bNodeSocket *sock_fr = best_socket_output(ntree, node_fr, sock_to, allow_multiple);
|
|
if (!sock_fr) {
|
|
continue;
|
|
}
|
|
|
|
if (snode_autoconnect_input(snode, node_fr, sock_fr, node_to, sock_to, replace)) {
|
|
numlinks++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (numlinks > 0) {
|
|
ntreeUpdateTree(bmain, ntree);
|
|
}
|
|
|
|
BLI_freelistN(nodelist);
|
|
MEM_freeN(nodelist);
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Link Viewer Operator
|
|
* \{ */
|
|
|
|
static int node_link_viewer(const bContext *C, bNode *tonode)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
|
|
/* context check */
|
|
if (tonode == nullptr || BLI_listbase_is_empty(&tonode->outputs)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
if (ELEM(tonode->type, CMP_NODE_VIEWER, CMP_NODE_SPLITVIEWER, GEO_NODE_VIEWER)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
/* get viewer */
|
|
bNode *viewer_node = nullptr;
|
|
LISTBASE_FOREACH (bNode *, node, &snode->edittree->nodes) {
|
|
if (ELEM(node->type, CMP_NODE_VIEWER, CMP_NODE_SPLITVIEWER, GEO_NODE_VIEWER)) {
|
|
if (node->flag & NODE_DO_OUTPUT) {
|
|
viewer_node = node;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/* no viewer, we make one active */
|
|
if (viewer_node == nullptr) {
|
|
LISTBASE_FOREACH (bNode *, node, &snode->edittree->nodes) {
|
|
if (ELEM(node->type, CMP_NODE_VIEWER, CMP_NODE_SPLITVIEWER, GEO_NODE_VIEWER)) {
|
|
node->flag |= NODE_DO_OUTPUT;
|
|
viewer_node = node;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bNodeSocket *sock = nullptr;
|
|
bNodeLink *link = nullptr;
|
|
|
|
/* try to find an already connected socket to cycle to the next */
|
|
if (viewer_node) {
|
|
link = nullptr;
|
|
|
|
for (link = (bNodeLink *)snode->edittree->links.first; link; link = link->next) {
|
|
if (link->tonode == viewer_node && link->fromnode == tonode) {
|
|
if (link->tosock == viewer_node->inputs.first) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (link) {
|
|
/* unlink existing connection */
|
|
sock = link->fromsock;
|
|
nodeRemLink(snode->edittree, link);
|
|
|
|
/* find a socket after the previously connected socket */
|
|
if (ED_node_is_geometry(snode)) {
|
|
/* Geometry nodes viewer only supports geometry sockets for now. */
|
|
for (sock = sock->next; sock; sock = sock->next) {
|
|
if (sock->type == SOCK_GEOMETRY && !nodeSocketIsHidden(sock)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (sock = sock->next; sock; sock = sock->next) {
|
|
if (!nodeSocketIsHidden(sock)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tonode) {
|
|
/* Find a selected socket that overrides the socket to connect to */
|
|
if (ED_node_is_geometry(snode)) {
|
|
/* Geometry nodes viewer only supports geometry sockets for now. */
|
|
LISTBASE_FOREACH (bNodeSocket *, sock2, &tonode->outputs) {
|
|
if (sock2->type == SOCK_GEOMETRY && !nodeSocketIsHidden(sock2) && sock2->flag & SELECT) {
|
|
sock = sock2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
LISTBASE_FOREACH (bNodeSocket *, sock2, &tonode->outputs) {
|
|
if (!nodeSocketIsHidden(sock2) && sock2->flag & SELECT) {
|
|
sock = sock2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* find a socket starting from the first socket */
|
|
if (!sock) {
|
|
if (ED_node_is_geometry(snode)) {
|
|
/* Geometry nodes viewer only supports geometry sockets for now. */
|
|
for (sock = (bNodeSocket *)tonode->outputs.first; sock; sock = sock->next) {
|
|
if (sock->type == SOCK_GEOMETRY && !nodeSocketIsHidden(sock)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (sock = (bNodeSocket *)tonode->outputs.first; sock; sock = sock->next) {
|
|
if (!nodeSocketIsHidden(sock)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sock) {
|
|
/* add a new viewer if none exists yet */
|
|
if (!viewer_node) {
|
|
/* XXX location is a quick hack, just place it next to the linked socket */
|
|
const int viewer_type = ED_node_is_compositor(snode) ? CMP_NODE_VIEWER : GEO_NODE_VIEWER;
|
|
viewer_node = node_add_node(C, nullptr, viewer_type, sock->locx + 100, sock->locy);
|
|
if (!viewer_node) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
link = nullptr;
|
|
}
|
|
else {
|
|
/* get link to viewer */
|
|
for (link = (bNodeLink *)snode->edittree->links.first; link; link = link->next) {
|
|
if (link->tonode == viewer_node && link->tosock == viewer_node->inputs.first) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (link == nullptr) {
|
|
nodeAddLink(
|
|
snode->edittree, tonode, sock, viewer_node, (bNodeSocket *)viewer_node->inputs.first);
|
|
}
|
|
else {
|
|
link->fromnode = tonode;
|
|
link->fromsock = sock;
|
|
/* make sure the dependency sorting is updated */
|
|
snode->edittree->update |= NTREE_UPDATE_LINKS;
|
|
}
|
|
if (ED_node_is_geometry(snode)) {
|
|
ED_spreadsheet_context_paths_set_geometry_node(CTX_data_main(C), snode, viewer_node);
|
|
}
|
|
|
|
ntreeUpdateTree(CTX_data_main(C), snode->edittree);
|
|
snode_update(snode, viewer_node);
|
|
DEG_id_tag_update(&snode->edittree->id, 0);
|
|
}
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
static int node_active_link_viewer_exec(bContext *C, wmOperator *UNUSED(op))
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNode *node = nodeGetActive(snode->edittree);
|
|
|
|
if (!node) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
|
|
|
|
if (node_link_viewer(C, node) == OPERATOR_CANCELLED) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
snode_notify(C, snode);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
static bool node_active_link_viewer_poll(bContext *C)
|
|
{
|
|
if (!ED_operator_node_editable(C)) {
|
|
return false;
|
|
}
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
return ED_node_is_compositor(snode) || ED_node_is_geometry(snode);
|
|
}
|
|
|
|
void NODE_OT_link_viewer(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Link to Viewer Node";
|
|
ot->description = "Link to viewer node";
|
|
ot->idname = "NODE_OT_link_viewer";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_active_link_viewer_exec;
|
|
ot->poll = node_active_link_viewer_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Add Link Operator
|
|
* \{ */
|
|
|
|
static void node_link_update_header(bContext *C, bNodeLinkDrag *UNUSED(nldrag))
|
|
{
|
|
char header[UI_MAX_DRAW_STR];
|
|
|
|
BLI_strncpy(header, TIP_("LMB: drag node link, RMB: cancel"), sizeof(header));
|
|
ED_workspace_status_text(C, header);
|
|
}
|
|
|
|
static int node_count_links(const bNodeTree *ntree, const bNodeSocket *socket)
|
|
{
|
|
int count = 0;
|
|
LISTBASE_FOREACH (bNodeLink *, link, &ntree->links) {
|
|
if (ELEM(socket, link->fromsock, link->tosock)) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void node_remove_extra_links(SpaceNode *snode, bNodeLink *link)
|
|
{
|
|
bNodeTree *ntree = snode->edittree;
|
|
bNodeSocket *from = link->fromsock, *to = link->tosock;
|
|
int to_count = node_count_links(ntree, to);
|
|
int from_count = node_count_links(ntree, from);
|
|
int to_link_limit = nodeSocketLinkLimit(to);
|
|
int from_link_limit = nodeSocketLinkLimit(from);
|
|
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, tlink, &ntree->links) {
|
|
if (tlink == link) {
|
|
continue;
|
|
}
|
|
|
|
if (tlink && tlink->fromsock == from) {
|
|
if (from_count > from_link_limit) {
|
|
nodeRemLink(ntree, tlink);
|
|
tlink = nullptr;
|
|
from_count--;
|
|
}
|
|
}
|
|
|
|
if (tlink && tlink->tosock == to) {
|
|
if (to_count > to_link_limit) {
|
|
nodeRemLink(ntree, tlink);
|
|
tlink = nullptr;
|
|
to_count--;
|
|
}
|
|
else if (tlink->fromsock == from) {
|
|
/* Also remove link if it comes from the same output. */
|
|
nodeRemLink(ntree, tlink);
|
|
tlink = nullptr;
|
|
to_count--;
|
|
from_count--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void node_link_exit(bContext *C, wmOperator *op, bool apply_links)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
bNodeLinkDrag *nldrag = (bNodeLinkDrag *)op->customdata;
|
|
bool do_tag_update = false;
|
|
/* View will be reset if no links connect. */
|
|
bool reset_view = true;
|
|
|
|
/* avoid updates while applying links */
|
|
ntree->is_updating = true;
|
|
LISTBASE_FOREACH (LinkData *, linkdata, &nldrag->links) {
|
|
bNodeLink *link = (bNodeLink *)linkdata->data;
|
|
|
|
/* See note below, but basically TEST flag means that the link
|
|
* was connected to output (or to a node which affects the
|
|
* output).
|
|
*/
|
|
do_tag_update |= (link->flag & NODE_LINK_TEST) != 0;
|
|
|
|
link->flag &= ~NODE_LINK_DRAGGED;
|
|
|
|
if (apply_links && link->tosock && link->fromsock) {
|
|
/* before actually adding the link,
|
|
* let nodes perform special link insertion handling
|
|
*/
|
|
if (link->fromnode->typeinfo->insert_link) {
|
|
link->fromnode->typeinfo->insert_link(ntree, link->fromnode, link);
|
|
}
|
|
if (link->tonode->typeinfo->insert_link) {
|
|
link->tonode->typeinfo->insert_link(ntree, link->tonode, link);
|
|
}
|
|
|
|
/* add link to the node tree */
|
|
BLI_addtail(&ntree->links, link);
|
|
|
|
ntree->update |= NTREE_UPDATE_LINKS;
|
|
|
|
/* tag tonode for update */
|
|
link->tonode->update |= NODE_UPDATE;
|
|
|
|
/* we might need to remove a link */
|
|
node_remove_extra_links(snode, link);
|
|
|
|
if (link->tonode) {
|
|
do_tag_update |= (do_tag_update || node_connected_to_output(bmain, ntree, link->tonode));
|
|
}
|
|
|
|
reset_view = false;
|
|
}
|
|
else {
|
|
nodeRemLink(ntree, link);
|
|
}
|
|
}
|
|
ntree->is_updating = false;
|
|
|
|
ntreeUpdateTree(bmain, ntree);
|
|
snode_notify(C, snode);
|
|
if (do_tag_update) {
|
|
snode_dag_update(C, snode);
|
|
}
|
|
|
|
if (reset_view) {
|
|
UI_view2d_edge_pan_cancel(C, &nldrag->pan_data);
|
|
}
|
|
|
|
BLI_remlink(&snode->runtime->linkdrag, nldrag);
|
|
/* links->data pointers are either held by the tree or freed already */
|
|
BLI_freelistN(&nldrag->links);
|
|
MEM_freeN(nldrag);
|
|
}
|
|
|
|
static void node_link_find_socket(bContext *C, wmOperator *op, float cursor[2])
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeLinkDrag *nldrag = (bNodeLinkDrag *)op->customdata;
|
|
|
|
if (nldrag->in_out == SOCK_OUT) {
|
|
bNode *tnode;
|
|
bNodeSocket *tsock = nullptr;
|
|
if (node_find_indicated_socket(snode, &tnode, &tsock, cursor, SOCK_IN)) {
|
|
LISTBASE_FOREACH (LinkData *, linkdata, &nldrag->links) {
|
|
bNodeLink *link = (bNodeLink *)linkdata->data;
|
|
|
|
/* skip if socket is on the same node as the fromsock */
|
|
if (tnode && link->fromnode == tnode) {
|
|
continue;
|
|
}
|
|
|
|
/* Skip if tsock is already linked with this output. */
|
|
bNodeLink *existing_link_connected_to_fromsock = nullptr;
|
|
LISTBASE_FOREACH (bNodeLink *, existing_link, &snode->edittree->links) {
|
|
if (existing_link->fromsock == link->fromsock && existing_link->tosock == tsock) {
|
|
existing_link_connected_to_fromsock = existing_link;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* attach links to the socket */
|
|
link->tonode = tnode;
|
|
link->tosock = tsock;
|
|
nldrag->last_node_hovered_while_dragging_a_link = tnode;
|
|
if (existing_link_connected_to_fromsock) {
|
|
link->multi_input_socket_index =
|
|
existing_link_connected_to_fromsock->multi_input_socket_index;
|
|
continue;
|
|
}
|
|
if (link->tosock && link->tosock->flag & SOCK_MULTI_INPUT) {
|
|
sort_multi_input_socket_links(snode, tnode, link, cursor);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
LISTBASE_FOREACH (LinkData *, linkdata, &nldrag->links) {
|
|
bNodeLink *link = (bNodeLink *)linkdata->data;
|
|
if (nldrag->last_node_hovered_while_dragging_a_link) {
|
|
sort_multi_input_socket_links(
|
|
snode, nldrag->last_node_hovered_while_dragging_a_link, nullptr, cursor);
|
|
}
|
|
link->tonode = nullptr;
|
|
link->tosock = nullptr;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
bNode *tnode;
|
|
bNodeSocket *tsock = nullptr;
|
|
if (node_find_indicated_socket(snode, &tnode, &tsock, cursor, SOCK_OUT)) {
|
|
LISTBASE_FOREACH (LinkData *, linkdata, &nldrag->links) {
|
|
bNodeLink *link = (bNodeLink *)linkdata->data;
|
|
|
|
/* skip if this is already the target socket */
|
|
if (link->fromsock == tsock) {
|
|
continue;
|
|
}
|
|
/* skip if socket is on the same node as the fromsock */
|
|
if (tnode && link->tonode == tnode) {
|
|
continue;
|
|
}
|
|
|
|
/* attach links to the socket */
|
|
link->fromnode = tnode;
|
|
link->fromsock = tsock;
|
|
}
|
|
}
|
|
else {
|
|
LISTBASE_FOREACH (LinkData *, linkdata, &nldrag->links) {
|
|
bNodeLink *link = (bNodeLink *)linkdata->data;
|
|
|
|
link->fromnode = nullptr;
|
|
link->fromsock = nullptr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Loop that adds a node-link, called by function below. */
|
|
/* in_out = starting socket */
|
|
static int node_link_modal(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
bNodeLinkDrag *nldrag = (bNodeLinkDrag *)op->customdata;
|
|
ARegion *region = CTX_wm_region(C);
|
|
float cursor[2];
|
|
|
|
UI_view2d_edge_pan_apply_event(C, &nldrag->pan_data, event);
|
|
|
|
UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &cursor[0], &cursor[1]);
|
|
|
|
switch (event->type) {
|
|
case MOUSEMOVE:
|
|
if (nldrag->from_multi_input_socket && !RNA_boolean_get(op->ptr, "has_link_picked")) {
|
|
pick_input_link_by_link_intersect(C, op, nldrag, cursor);
|
|
}
|
|
else {
|
|
node_link_find_socket(C, op, cursor);
|
|
|
|
node_link_update_header(C, nldrag);
|
|
ED_region_tag_redraw(region);
|
|
}
|
|
break;
|
|
|
|
case LEFTMOUSE:
|
|
case RIGHTMOUSE:
|
|
case MIDDLEMOUSE: {
|
|
if (event->val == KM_RELEASE) {
|
|
node_link_exit(C, op, true);
|
|
|
|
ED_workspace_status_text(C, nullptr);
|
|
ED_region_tag_redraw(region);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
clear_picking_highlight(&snode->edittree->links);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
/* return 1 when socket clicked */
|
|
static bNodeLinkDrag *node_link_init(Main *bmain, SpaceNode *snode, float cursor[2], bool detach)
|
|
{
|
|
bNodeLinkDrag *nldrag = nullptr;
|
|
|
|
/* output indicated? */
|
|
bNode *node;
|
|
bNodeSocket *sock;
|
|
if (node_find_indicated_socket(snode, &node, &sock, cursor, SOCK_OUT)) {
|
|
nldrag = (bNodeLinkDrag *)MEM_callocN(sizeof(bNodeLinkDrag), "drag link op customdata");
|
|
|
|
const int num_links = nodeCountSocketLinks(snode->edittree, sock);
|
|
int link_limit = nodeSocketLinkLimit(sock);
|
|
if (num_links > 0 && (num_links >= link_limit || detach)) {
|
|
/* dragged links are fixed on input side */
|
|
nldrag->in_out = SOCK_IN;
|
|
/* detach current links and store them in the operator data */
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &snode->edittree->links) {
|
|
if (link->fromsock == sock) {
|
|
LinkData *linkdata = (LinkData *)MEM_callocN(sizeof(LinkData), "drag link op link data");
|
|
bNodeLink *oplink = (bNodeLink *)MEM_callocN(sizeof(bNodeLink), "drag link op link");
|
|
linkdata->data = oplink;
|
|
*oplink = *link;
|
|
oplink->next = oplink->prev = nullptr;
|
|
oplink->flag |= NODE_LINK_VALID;
|
|
oplink->flag |= NODE_LINK_DRAGGED;
|
|
|
|
/* The link could be disconnected and in that case we
|
|
* wouldn't be able to check whether tag update is
|
|
* needed or not when releasing mouse button. So we
|
|
* cache whether the link affects output or not here
|
|
* using TEST flag.
|
|
*/
|
|
oplink->flag &= ~NODE_LINK_TEST;
|
|
if (node_connected_to_output(bmain, snode->edittree, link->tonode)) {
|
|
oplink->flag |= NODE_LINK_TEST;
|
|
}
|
|
|
|
BLI_addtail(&nldrag->links, linkdata);
|
|
nodeRemLink(snode->edittree, link);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
/* dragged links are fixed on output side */
|
|
nldrag->in_out = SOCK_OUT;
|
|
/* create a new link */
|
|
LinkData *linkdata = create_drag_link(bmain, snode, node, sock);
|
|
|
|
BLI_addtail(&nldrag->links, linkdata);
|
|
}
|
|
}
|
|
/* or an input? */
|
|
else if (node_find_indicated_socket(snode, &node, &sock, cursor, SOCK_IN)) {
|
|
nldrag = (bNodeLinkDrag *)MEM_callocN(sizeof(bNodeLinkDrag), "drag link op customdata");
|
|
nldrag->last_node_hovered_while_dragging_a_link = node;
|
|
|
|
const int num_links = nodeCountSocketLinks(snode->edittree, sock);
|
|
if (num_links > 0) {
|
|
/* dragged links are fixed on output side */
|
|
nldrag->in_out = SOCK_OUT;
|
|
/* detach current links and store them in the operator data */
|
|
bNodeLink *link_to_pick;
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &snode->edittree->links) {
|
|
if (link->tosock == sock) {
|
|
if (sock->flag & SOCK_MULTI_INPUT) {
|
|
nldrag->from_multi_input_socket = true;
|
|
}
|
|
link_to_pick = link;
|
|
}
|
|
}
|
|
|
|
if (link_to_pick != nullptr && !nldrag->from_multi_input_socket) {
|
|
LinkData *linkdata = (LinkData *)MEM_callocN(sizeof(LinkData), "drag link op link data");
|
|
bNodeLink *oplink = (bNodeLink *)MEM_callocN(sizeof(bNodeLink), "drag link op link");
|
|
linkdata->data = oplink;
|
|
*oplink = *link_to_pick;
|
|
oplink->next = oplink->prev = nullptr;
|
|
oplink->flag |= NODE_LINK_VALID;
|
|
oplink->flag |= NODE_LINK_DRAGGED;
|
|
oplink->flag &= ~NODE_LINK_TEST;
|
|
if (node_connected_to_output(bmain, snode->edittree, link_to_pick->tonode)) {
|
|
oplink->flag |= NODE_LINK_TEST;
|
|
}
|
|
|
|
BLI_addtail(&nldrag->links, linkdata);
|
|
nodeRemLink(snode->edittree, link_to_pick);
|
|
|
|
/* send changed event to original link->tonode */
|
|
if (node) {
|
|
snode_update(snode, node);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
/* dragged links are fixed on input side */
|
|
nldrag->in_out = SOCK_IN;
|
|
/* create a new link */
|
|
LinkData *linkdata = create_drag_link(bmain, snode, node, sock);
|
|
|
|
BLI_addtail(&nldrag->links, linkdata);
|
|
}
|
|
}
|
|
|
|
return nldrag;
|
|
}
|
|
|
|
static int node_link_invoke(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
ARegion *region = CTX_wm_region(C);
|
|
|
|
bool detach = RNA_boolean_get(op->ptr, "detach");
|
|
|
|
float cursor[2];
|
|
UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &cursor[0], &cursor[1]);
|
|
RNA_float_set_array(op->ptr, "drag_start", cursor);
|
|
RNA_boolean_set(op->ptr, "has_link_picked", false);
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
|
|
bNodeLinkDrag *nldrag = node_link_init(bmain, snode, cursor, detach);
|
|
|
|
if (nldrag) {
|
|
UI_view2d_edge_pan_operator_init(C, &nldrag->pan_data, op);
|
|
|
|
op->customdata = nldrag;
|
|
BLI_addtail(&snode->runtime->linkdrag, nldrag);
|
|
|
|
/* add modal handler */
|
|
WM_event_add_modal_handler(C, op);
|
|
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
|
|
}
|
|
|
|
static void node_link_cancel(bContext *C, wmOperator *op)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeLinkDrag *nldrag = (bNodeLinkDrag *)op->customdata;
|
|
|
|
BLI_remlink(&snode->runtime->linkdrag, nldrag);
|
|
|
|
UI_view2d_edge_pan_cancel(C, &nldrag->pan_data);
|
|
|
|
BLI_freelistN(&nldrag->links);
|
|
MEM_freeN(nldrag);
|
|
clear_picking_highlight(&snode->edittree->links);
|
|
}
|
|
|
|
void NODE_OT_link(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Link Nodes";
|
|
ot->idname = "NODE_OT_link";
|
|
ot->description = "Use the mouse to create a link between two nodes";
|
|
|
|
/* api callbacks */
|
|
ot->invoke = node_link_invoke;
|
|
ot->modal = node_link_modal;
|
|
// ot->exec = node_link_exec;
|
|
ot->poll = ED_operator_node_editable;
|
|
ot->cancel = node_link_cancel;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_BLOCKING;
|
|
|
|
PropertyRNA *prop;
|
|
|
|
RNA_def_boolean(ot->srna, "detach", false, "Detach", "Detach and redirect existing links");
|
|
prop = RNA_def_boolean(
|
|
ot->srna,
|
|
"has_link_picked",
|
|
false,
|
|
"Has Link Picked",
|
|
"The operation has placed a link. Only used for multi-input sockets, where the "
|
|
"link is picked later");
|
|
RNA_def_property_flag(prop, PROP_HIDDEN);
|
|
RNA_def_float_array(ot->srna,
|
|
"drag_start",
|
|
2,
|
|
nullptr,
|
|
-UI_PRECISION_FLOAT_MAX,
|
|
UI_PRECISION_FLOAT_MAX,
|
|
"Drag Start",
|
|
"The position of the mouse cursor at the start of the operation",
|
|
-UI_PRECISION_FLOAT_MAX,
|
|
UI_PRECISION_FLOAT_MAX);
|
|
RNA_def_property_flag(prop, PROP_HIDDEN);
|
|
RNA_def_property_flag(prop, PROP_HIDDEN);
|
|
|
|
UI_view2d_edge_pan_operator_properties_ex(ot,
|
|
NODE_EDGE_PAN_INSIDE_PAD,
|
|
NODE_EDGE_PAN_OUTSIDE_PAD,
|
|
NODE_EDGE_PAN_SPEED_RAMP,
|
|
NODE_EDGE_PAN_MAX_SPEED,
|
|
NODE_EDGE_PAN_DELAY,
|
|
NODE_EDGE_PAN_ZOOM_INFLUENCE);
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Make Link Operator
|
|
* \{ */
|
|
|
|
/* makes a link between selected output and input sockets */
|
|
static int node_make_link_exec(bContext *C, wmOperator *op)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
const bool replace = RNA_boolean_get(op->ptr, "replace");
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
|
|
snode_autoconnect(bmain, snode, true, replace);
|
|
|
|
/* deselect sockets after linking */
|
|
node_deselect_all_input_sockets(snode, false);
|
|
node_deselect_all_output_sockets(snode, false);
|
|
|
|
ntreeUpdateTree(CTX_data_main(C), snode->edittree);
|
|
snode_notify(C, snode);
|
|
snode_dag_update(C, snode);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_link_make(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Make Links";
|
|
ot->description = "Makes a link between selected output in input sockets";
|
|
ot->idname = "NODE_OT_link_make";
|
|
|
|
/* callbacks */
|
|
ot->exec = node_make_link_exec;
|
|
/* XXX we need a special poll which checks that there are selected input/output sockets. */
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
RNA_def_boolean(
|
|
ot->srna, "replace", false, "Replace", "Replace socket connections with the new links");
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Node Link Intersect
|
|
* \{ */
|
|
|
|
static bool node_links_intersect(bNodeLink *link, const float mcoords[][2], int tot)
|
|
{
|
|
float coord_array[NODE_LINK_RESOL + 1][2];
|
|
|
|
if (node_link_bezier_points(nullptr, nullptr, link, coord_array, NODE_LINK_RESOL)) {
|
|
for (int i = 0; i < tot - 1; i++) {
|
|
for (int b = 0; b < NODE_LINK_RESOL; b++) {
|
|
if (isect_seg_seg_v2(mcoords[i], mcoords[i + 1], coord_array[b], coord_array[b + 1]) > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Cut Link Operator
|
|
* \{ */
|
|
|
|
static int cut_links_exec(bContext *C, wmOperator *op)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
ARegion *region = CTX_wm_region(C);
|
|
bool do_tag_update = false;
|
|
|
|
int i = 0;
|
|
float mcoords[256][2];
|
|
RNA_BEGIN (op->ptr, itemptr, "path") {
|
|
float loc[2];
|
|
|
|
RNA_float_get_array(&itemptr, "loc", loc);
|
|
UI_view2d_region_to_view(
|
|
®ion->v2d, (int)loc[0], (int)loc[1], &mcoords[i][0], &mcoords[i][1]);
|
|
i++;
|
|
if (i >= 256) {
|
|
break;
|
|
}
|
|
}
|
|
RNA_END;
|
|
|
|
if (i > 1) {
|
|
bool found = false;
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &snode->edittree->links) {
|
|
if (node_link_is_hidden_or_dimmed(®ion->v2d, link)) {
|
|
continue;
|
|
}
|
|
|
|
if (node_links_intersect(link, mcoords, i)) {
|
|
|
|
if (found == false) {
|
|
/* TODO(sergey): Why did we kill jobs twice? */
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
found = true;
|
|
}
|
|
|
|
do_tag_update |= (do_tag_update ||
|
|
node_connected_to_output(bmain, snode->edittree, link->tonode));
|
|
|
|
snode_update(snode, link->tonode);
|
|
bNode *to_node = link->tonode;
|
|
nodeRemLink(snode->edittree, link);
|
|
sort_multi_input_socket_links(snode, to_node, nullptr, nullptr);
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
ntreeUpdateTree(CTX_data_main(C), snode->edittree);
|
|
snode_notify(C, snode);
|
|
if (do_tag_update) {
|
|
snode_dag_update(C, snode);
|
|
}
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
|
|
}
|
|
|
|
void NODE_OT_links_cut(wmOperatorType *ot)
|
|
{
|
|
ot->name = "Cut Links";
|
|
ot->idname = "NODE_OT_links_cut";
|
|
ot->description = "Use the mouse to cut (remove) some links";
|
|
|
|
ot->invoke = WM_gesture_lines_invoke;
|
|
ot->modal = WM_gesture_lines_modal;
|
|
ot->exec = cut_links_exec;
|
|
ot->cancel = WM_gesture_lines_cancel;
|
|
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_DEPENDS_ON_CURSOR;
|
|
|
|
/* properties */
|
|
PropertyRNA *prop;
|
|
prop = RNA_def_collection_runtime(ot->srna, "path", &RNA_OperatorMousePath, "Path", "");
|
|
RNA_def_property_flag(prop, (PropertyFlag)(PROP_HIDDEN | PROP_SKIP_SAVE));
|
|
|
|
/* internal */
|
|
RNA_def_int(ot->srna, "cursor", WM_CURSOR_KNIFE, 0, INT_MAX, "Cursor", "", 0, INT_MAX);
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Mute Links Operator
|
|
* \{ */
|
|
|
|
static int mute_links_exec(bContext *C, wmOperator *op)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
ARegion *region = CTX_wm_region(C);
|
|
bool do_tag_update = false;
|
|
|
|
int i = 0;
|
|
float mcoords[256][2];
|
|
RNA_BEGIN (op->ptr, itemptr, "path") {
|
|
float loc[2];
|
|
|
|
RNA_float_get_array(&itemptr, "loc", loc);
|
|
UI_view2d_region_to_view(
|
|
®ion->v2d, (int)loc[0], (int)loc[1], &mcoords[i][0], &mcoords[i][1]);
|
|
i++;
|
|
if (i >= 256) {
|
|
break;
|
|
}
|
|
}
|
|
RNA_END;
|
|
|
|
if (i > 1) {
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
|
|
/* Count intersected links and clear test flag. */
|
|
int tot = 0;
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
if (node_link_is_hidden_or_dimmed(®ion->v2d, link)) {
|
|
continue;
|
|
}
|
|
link->flag &= ~NODE_LINK_TEST;
|
|
if (node_links_intersect(link, mcoords, i)) {
|
|
tot++;
|
|
}
|
|
}
|
|
if (tot == 0) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
/* Mute links. */
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
if (node_link_is_hidden_or_dimmed(®ion->v2d, link) || (link->flag & NODE_LINK_TEST)) {
|
|
continue;
|
|
}
|
|
|
|
if (node_links_intersect(link, mcoords, i)) {
|
|
do_tag_update |= (do_tag_update ||
|
|
node_connected_to_output(bmain, snode->edittree, link->tonode));
|
|
|
|
snode_update(snode, link->tonode);
|
|
nodeMuteLinkToggle(snode->edittree, link);
|
|
}
|
|
}
|
|
|
|
/* Clear remaining test flags. */
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
if (node_link_is_hidden_or_dimmed(®ion->v2d, link)) {
|
|
continue;
|
|
}
|
|
link->flag &= ~NODE_LINK_TEST;
|
|
}
|
|
|
|
ntreeUpdateTree(CTX_data_main(C), snode->edittree);
|
|
snode_notify(C, snode);
|
|
if (do_tag_update) {
|
|
snode_dag_update(C, snode);
|
|
}
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH;
|
|
}
|
|
|
|
void NODE_OT_links_mute(wmOperatorType *ot)
|
|
{
|
|
ot->name = "Mute Links";
|
|
ot->idname = "NODE_OT_links_mute";
|
|
ot->description = "Use the mouse to mute links";
|
|
|
|
ot->invoke = WM_gesture_lines_invoke;
|
|
ot->modal = WM_gesture_lines_modal;
|
|
ot->exec = mute_links_exec;
|
|
ot->cancel = WM_gesture_lines_cancel;
|
|
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_DEPENDS_ON_CURSOR;
|
|
|
|
/* properties */
|
|
PropertyRNA *prop;
|
|
prop = RNA_def_collection_runtime(ot->srna, "path", &RNA_OperatorMousePath, "Path", "");
|
|
RNA_def_property_flag(prop, (PropertyFlag)(PROP_HIDDEN | PROP_SKIP_SAVE));
|
|
|
|
/* internal */
|
|
RNA_def_int(ot->srna, "cursor", WM_CURSOR_MUTE, 0, INT_MAX, "Cursor", "", 0, INT_MAX);
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Detach Links Operator
|
|
* \{ */
|
|
|
|
static int detach_links_exec(bContext *C, wmOperator *UNUSED(op))
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
|
|
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (node->flag & SELECT) {
|
|
nodeInternalRelink(ntree, node);
|
|
}
|
|
}
|
|
|
|
ntreeUpdateTree(CTX_data_main(C), ntree);
|
|
|
|
snode_notify(C, snode);
|
|
snode_dag_update(C, snode);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_links_detach(wmOperatorType *ot)
|
|
{
|
|
ot->name = "Detach Links";
|
|
ot->idname = "NODE_OT_links_detach";
|
|
ot->description =
|
|
"Remove all links to selected nodes, and try to connect neighbor nodes together";
|
|
|
|
ot->exec = detach_links_exec;
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Set Parent Operator
|
|
* \{ */
|
|
|
|
static int node_parent_set_exec(bContext *C, wmOperator *UNUSED(op))
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
bNode *frame = nodeGetActive(ntree);
|
|
if (!frame || frame->type != NODE_FRAME) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (node == frame) {
|
|
continue;
|
|
}
|
|
if (node->flag & NODE_SELECT) {
|
|
nodeDetachNode(node);
|
|
nodeAttachNode(node, frame);
|
|
}
|
|
}
|
|
|
|
ED_node_sort(ntree);
|
|
WM_event_add_notifier(C, NC_NODE | ND_DISPLAY, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_parent_set(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Make Parent";
|
|
ot->description = "Attach selected nodes";
|
|
ot->idname = "NODE_OT_parent_set";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_parent_set_exec;
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Join Nodes Operator
|
|
* \{ */
|
|
|
|
/* tags for depth-first search */
|
|
#define NODE_JOIN_DONE 1
|
|
#define NODE_JOIN_IS_DESCENDANT 2
|
|
|
|
static void node_join_attach_recursive(bNode *node, bNode *frame)
|
|
{
|
|
node->done |= NODE_JOIN_DONE;
|
|
|
|
if (node == frame) {
|
|
node->done |= NODE_JOIN_IS_DESCENDANT;
|
|
}
|
|
else if (node->parent) {
|
|
/* call recursively */
|
|
if (!(node->parent->done & NODE_JOIN_DONE)) {
|
|
node_join_attach_recursive(node->parent, frame);
|
|
}
|
|
|
|
/* in any case: if the parent is a descendant, so is the child */
|
|
if (node->parent->done & NODE_JOIN_IS_DESCENDANT) {
|
|
node->done |= NODE_JOIN_IS_DESCENDANT;
|
|
}
|
|
else if (node->flag & NODE_TEST) {
|
|
/* if parent is not an descendant of the frame, reattach the node */
|
|
nodeDetachNode(node);
|
|
nodeAttachNode(node, frame);
|
|
node->done |= NODE_JOIN_IS_DESCENDANT;
|
|
}
|
|
}
|
|
else if (node->flag & NODE_TEST) {
|
|
nodeAttachNode(node, frame);
|
|
node->done |= NODE_JOIN_IS_DESCENDANT;
|
|
}
|
|
}
|
|
|
|
static int node_join_exec(bContext *C, wmOperator *UNUSED(op))
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
|
|
/* XXX save selection: node_add_node call below sets the new frame as single
|
|
* active+selected node */
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (node->flag & NODE_SELECT) {
|
|
node->flag |= NODE_TEST;
|
|
}
|
|
else {
|
|
node->flag &= ~NODE_TEST;
|
|
}
|
|
}
|
|
|
|
bNode *frame = node_add_node(C, nullptr, NODE_FRAME, 0.0f, 0.0f);
|
|
|
|
/* reset tags */
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
node->done = 0;
|
|
}
|
|
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (!(node->done & NODE_JOIN_DONE)) {
|
|
node_join_attach_recursive(node, frame);
|
|
}
|
|
}
|
|
|
|
/* restore selection */
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (node->flag & NODE_TEST) {
|
|
node->flag |= NODE_SELECT;
|
|
}
|
|
}
|
|
|
|
ED_node_sort(ntree);
|
|
WM_event_add_notifier(C, NC_NODE | ND_DISPLAY, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_join(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Join Nodes";
|
|
ot->description = "Attach selected nodes to a new common frame";
|
|
ot->idname = "NODE_OT_join";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_join_exec;
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Attach Operator
|
|
* \{ */
|
|
|
|
static bNode *node_find_frame_to_attach(ARegion *region,
|
|
const bNodeTree *ntree,
|
|
const int mouse_xy[2])
|
|
{
|
|
/* convert mouse coordinates to v2d space */
|
|
float cursor[2];
|
|
UI_view2d_region_to_view(®ion->v2d, UNPACK2(mouse_xy), &cursor[0], &cursor[1]);
|
|
|
|
LISTBASE_FOREACH_BACKWARD (bNode *, frame, &ntree->nodes) {
|
|
/* skip selected, those are the nodes we want to attach */
|
|
if ((frame->type != NODE_FRAME) || (frame->flag & NODE_SELECT)) {
|
|
continue;
|
|
}
|
|
if (BLI_rctf_isect_pt_v(&frame->totr, cursor)) {
|
|
return frame;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static int node_attach_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event)
|
|
{
|
|
ARegion *region = CTX_wm_region(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
bNode *frame = node_find_frame_to_attach(region, ntree, event->mval);
|
|
|
|
if (frame) {
|
|
LISTBASE_FOREACH_BACKWARD (bNode *, node, &ntree->nodes) {
|
|
if (node->flag & NODE_SELECT) {
|
|
if (node->parent == nullptr) {
|
|
/* disallow moving a parent into its child */
|
|
if (nodeAttachNodeCheck(frame, node) == false) {
|
|
/* attach all unparented nodes */
|
|
nodeAttachNode(node, frame);
|
|
}
|
|
}
|
|
else {
|
|
/* attach nodes which share parent with the frame */
|
|
bNode *parent;
|
|
for (parent = frame->parent; parent; parent = parent->parent) {
|
|
if (parent == node->parent) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (parent) {
|
|
/* disallow moving a parent into its child */
|
|
if (nodeAttachNodeCheck(frame, node) == false) {
|
|
nodeDetachNode(node);
|
|
nodeAttachNode(node, frame);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ED_node_sort(ntree);
|
|
WM_event_add_notifier(C, NC_NODE | ND_DISPLAY, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_attach(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Attach Nodes";
|
|
ot->description = "Attach active node to a frame";
|
|
ot->idname = "NODE_OT_attach";
|
|
|
|
/* api callbacks */
|
|
|
|
ot->invoke = node_attach_invoke;
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Detach Operator
|
|
* \{ */
|
|
|
|
/* tags for depth-first search */
|
|
#define NODE_DETACH_DONE 1
|
|
#define NODE_DETACH_IS_DESCENDANT 2
|
|
|
|
static void node_detach_recursive(bNode *node)
|
|
{
|
|
node->done |= NODE_DETACH_DONE;
|
|
|
|
if (node->parent) {
|
|
/* call recursively */
|
|
if (!(node->parent->done & NODE_DETACH_DONE)) {
|
|
node_detach_recursive(node->parent);
|
|
}
|
|
|
|
/* in any case: if the parent is a descendant, so is the child */
|
|
if (node->parent->done & NODE_DETACH_IS_DESCENDANT) {
|
|
node->done |= NODE_DETACH_IS_DESCENDANT;
|
|
}
|
|
else if (node->flag & NODE_SELECT) {
|
|
/* if parent is not a descendant of a selected node, detach */
|
|
nodeDetachNode(node);
|
|
node->done |= NODE_DETACH_IS_DESCENDANT;
|
|
}
|
|
}
|
|
else if (node->flag & NODE_SELECT) {
|
|
node->done |= NODE_DETACH_IS_DESCENDANT;
|
|
}
|
|
}
|
|
|
|
/* detach the root nodes in the current selection */
|
|
static int node_detach_exec(bContext *C, wmOperator *UNUSED(op))
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
|
|
/* reset tags */
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
node->done = 0;
|
|
}
|
|
/* detach nodes recursively
|
|
* relative order is preserved here!
|
|
*/
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (!(node->done & NODE_DETACH_DONE)) {
|
|
node_detach_recursive(node);
|
|
}
|
|
}
|
|
|
|
ED_node_sort(ntree);
|
|
WM_event_add_notifier(C, NC_NODE | ND_DISPLAY, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_detach(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Detach Nodes";
|
|
ot->description = "Detach selected nodes from parents";
|
|
ot->idname = "NODE_OT_detach";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_detach_exec;
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Automatic Node Insert on Dragging
|
|
* \{ */
|
|
|
|
/* prevent duplicate testing code below */
|
|
static bool ed_node_link_conditions(ScrArea *area,
|
|
bool test,
|
|
SpaceNode **r_snode,
|
|
bNode **r_select)
|
|
{
|
|
SpaceNode *snode = area ? (SpaceNode *)area->spacedata.first : nullptr;
|
|
|
|
*r_snode = snode;
|
|
*r_select = nullptr;
|
|
|
|
/* no unlucky accidents */
|
|
if (area == nullptr || area->spacetype != SPACE_NODE) {
|
|
return false;
|
|
}
|
|
|
|
if (!test) {
|
|
/* no need to look for a node */
|
|
return true;
|
|
}
|
|
|
|
bNode *node;
|
|
bNode *select = nullptr;
|
|
for (node = (bNode *)snode->edittree->nodes.first; node; node = node->next) {
|
|
if (node->flag & SELECT) {
|
|
if (select) {
|
|
break;
|
|
}
|
|
select = node;
|
|
}
|
|
}
|
|
/* only one selected */
|
|
if (node || select == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
/* correct node */
|
|
if (BLI_listbase_is_empty(&select->inputs) || BLI_listbase_is_empty(&select->outputs)) {
|
|
return false;
|
|
}
|
|
|
|
ARegion *region = BKE_area_find_region_type(area, RGN_TYPE_WINDOW);
|
|
|
|
/* test node for links */
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
if (node_link_is_hidden_or_dimmed(®ion->v2d, link)) {
|
|
continue;
|
|
}
|
|
|
|
if (link->tonode == select || link->fromnode == select) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
*r_select = select;
|
|
return true;
|
|
}
|
|
|
|
/* test == 0, clear all intersect flags */
|
|
void ED_node_link_intersect_test(ScrArea *area, int test)
|
|
{
|
|
bNode *select;
|
|
SpaceNode *snode;
|
|
if (!ed_node_link_conditions(area, test, &snode, &select)) {
|
|
return;
|
|
}
|
|
|
|
/* clear flags */
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
link->flag &= ~NODE_LINKFLAG_HILITE;
|
|
}
|
|
|
|
if (test == 0) {
|
|
return;
|
|
}
|
|
|
|
ARegion *region = BKE_area_find_region_type(area, RGN_TYPE_WINDOW);
|
|
|
|
/* find link to select/highlight */
|
|
bNodeLink *selink = nullptr;
|
|
float dist_best = FLT_MAX;
|
|
LISTBASE_FOREACH (bNodeLink *, link, &snode->edittree->links) {
|
|
float coord_array[NODE_LINK_RESOL + 1][2];
|
|
|
|
if (node_link_is_hidden_or_dimmed(®ion->v2d, link)) {
|
|
continue;
|
|
}
|
|
|
|
if (node_link_bezier_points(nullptr, nullptr, link, coord_array, NODE_LINK_RESOL)) {
|
|
float dist = FLT_MAX;
|
|
|
|
/* loop over link coords to find shortest dist to
|
|
* upper left node edge of a intersected line segment */
|
|
for (int i = 0; i < NODE_LINK_RESOL; i++) {
|
|
/* Check if the node rectangle intersects the line from this point to next one. */
|
|
if (BLI_rctf_isect_segment(&select->totr, coord_array[i], coord_array[i + 1])) {
|
|
/* store the shortest distance to the upper left edge
|
|
* of all intersections found so far */
|
|
const float node_xy[] = {select->totr.xmin, select->totr.ymax};
|
|
|
|
/* to be precise coord_array should be clipped by select->totr,
|
|
* but not done since there's no real noticeable difference */
|
|
dist = min_ff(
|
|
dist_squared_to_line_segment_v2(node_xy, coord_array[i], coord_array[i + 1]), dist);
|
|
}
|
|
}
|
|
|
|
/* we want the link with the shortest distance to node center */
|
|
if (dist < dist_best) {
|
|
dist_best = dist;
|
|
selink = link;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selink) {
|
|
selink->flag |= NODE_LINKFLAG_HILITE;
|
|
}
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Node Insert Offset Operator
|
|
* \{ */
|
|
|
|
static int get_main_socket_priority(const bNodeSocket *socket)
|
|
{
|
|
switch ((eNodeSocketDatatype)socket->type) {
|
|
case __SOCK_MESH:
|
|
case SOCK_CUSTOM:
|
|
return -1;
|
|
case SOCK_BOOLEAN:
|
|
return 0;
|
|
case SOCK_INT:
|
|
return 1;
|
|
case SOCK_FLOAT:
|
|
return 2;
|
|
case SOCK_VECTOR:
|
|
return 3;
|
|
case SOCK_RGBA:
|
|
return 4;
|
|
case SOCK_STRING:
|
|
case SOCK_SHADER:
|
|
case SOCK_OBJECT:
|
|
case SOCK_IMAGE:
|
|
case SOCK_GEOMETRY:
|
|
case SOCK_COLLECTION:
|
|
case SOCK_TEXTURE:
|
|
case SOCK_MATERIAL:
|
|
return 5;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/** Get the "main" socket of a socket list using a heuristic based on socket types. */
|
|
static bNodeSocket *get_main_socket(ListBase *sockets)
|
|
{
|
|
/* find priority range */
|
|
int maxpriority = -1;
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, sockets) {
|
|
if (sock->flag & SOCK_UNAVAIL) {
|
|
continue;
|
|
}
|
|
maxpriority = max_ii(get_main_socket_priority(sock), maxpriority);
|
|
}
|
|
|
|
/* try all priorities, starting from 'highest' */
|
|
for (int priority = maxpriority; priority >= 0; priority--) {
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, sockets) {
|
|
if (!nodeSocketIsHidden(sock) && priority == get_main_socket_priority(sock)) {
|
|
return sock;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* no visible sockets, unhide first of highest priority */
|
|
for (int priority = maxpriority; priority >= 0; priority--) {
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, sockets) {
|
|
if (sock->flag & SOCK_UNAVAIL) {
|
|
continue;
|
|
}
|
|
if (priority == get_main_socket_priority(sock)) {
|
|
sock->flag &= ~SOCK_HIDDEN;
|
|
return sock;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static bool node_parents_offset_flag_enable_cb(bNode *parent, void *UNUSED(userdata))
|
|
{
|
|
/* NODE_TEST is used to flag nodes that shouldn't be offset (again) */
|
|
parent->flag |= NODE_TEST;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void node_offset_apply(bNode *node, const float offset_x)
|
|
{
|
|
/* NODE_TEST is used to flag nodes that shouldn't be offset (again) */
|
|
if ((node->flag & NODE_TEST) == 0) {
|
|
node->anim_init_locx = node->locx;
|
|
node->anim_ofsx = (offset_x / UI_DPI_FAC);
|
|
node->flag |= NODE_TEST;
|
|
}
|
|
}
|
|
|
|
static void node_parent_offset_apply(NodeInsertOfsData *data, bNode *parent, const float offset_x)
|
|
{
|
|
node_offset_apply(parent, offset_x);
|
|
|
|
/* Flag all children as offset to prevent them from being offset
|
|
* separately (they've already moved with the parent). */
|
|
LISTBASE_FOREACH (bNode *, node, &data->ntree->nodes) {
|
|
if (nodeIsChildOf(parent, node)) {
|
|
/* NODE_TEST is used to flag nodes that shouldn't be offset (again) */
|
|
node->flag |= NODE_TEST;
|
|
}
|
|
}
|
|
}
|
|
|
|
#define NODE_INSOFS_ANIM_DURATION 0.25f
|
|
|
|
/**
|
|
* Callback that applies #NodeInsertOfsData.offset_x to a node or its parent, similar
|
|
* to node_link_insert_offset_output_chain_cb below, but with slightly different logic
|
|
*/
|
|
static bool node_link_insert_offset_frame_chain_cb(bNode *fromnode,
|
|
bNode *tonode,
|
|
void *userdata,
|
|
const bool reversed)
|
|
{
|
|
NodeInsertOfsData *data = (NodeInsertOfsData *)userdata;
|
|
bNode *ofs_node = reversed ? fromnode : tonode;
|
|
|
|
if (ofs_node->parent && ofs_node->parent != data->insert_parent) {
|
|
node_offset_apply(ofs_node->parent, data->offset_x);
|
|
}
|
|
else {
|
|
node_offset_apply(ofs_node, data->offset_x);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Applies #NodeInsertOfsData.offset_x to all children of \a parent.
|
|
*/
|
|
static void node_link_insert_offset_frame_chains(const bNodeTree *ntree,
|
|
const bNode *parent,
|
|
NodeInsertOfsData *data,
|
|
const bool reversed)
|
|
{
|
|
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
|
|
if (nodeIsChildOf(parent, node)) {
|
|
nodeChainIter(ntree, node, node_link_insert_offset_frame_chain_cb, data, reversed);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback that applies NodeInsertOfsData.offset_x to a node or its parent,
|
|
* considering the logic needed for offsetting nodes after link insert
|
|
*/
|
|
static bool node_link_insert_offset_chain_cb(bNode *fromnode,
|
|
bNode *tonode,
|
|
void *userdata,
|
|
const bool reversed)
|
|
{
|
|
NodeInsertOfsData *data = (NodeInsertOfsData *)userdata;
|
|
bNode *ofs_node = reversed ? fromnode : tonode;
|
|
|
|
if (data->insert_parent) {
|
|
if (ofs_node->parent && (ofs_node->parent->flag & NODE_TEST) == 0) {
|
|
node_parent_offset_apply(data, ofs_node->parent, data->offset_x);
|
|
node_link_insert_offset_frame_chains(data->ntree, ofs_node->parent, data, reversed);
|
|
}
|
|
else {
|
|
node_offset_apply(ofs_node, data->offset_x);
|
|
}
|
|
|
|
if (nodeIsChildOf(data->insert_parent, ofs_node) == false) {
|
|
data->insert_parent = nullptr;
|
|
}
|
|
}
|
|
else if (ofs_node->parent) {
|
|
bNode *node = nodeFindRootParent(ofs_node);
|
|
node_offset_apply(node, data->offset_x);
|
|
}
|
|
else {
|
|
node_offset_apply(ofs_node, data->offset_x);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void node_link_insert_offset_ntree(NodeInsertOfsData *iofsd,
|
|
ARegion *region,
|
|
const int mouse_xy[2],
|
|
const bool right_alignment)
|
|
{
|
|
bNodeTree *ntree = iofsd->ntree;
|
|
bNode *insert = iofsd->insert;
|
|
bNode *prev = iofsd->prev, *next = iofsd->next;
|
|
bNode *init_parent = insert->parent; /* store old insert->parent for restoring later */
|
|
|
|
const float min_margin = U.node_margin * UI_DPI_FAC;
|
|
const float width = NODE_WIDTH(insert);
|
|
const bool needs_alignment = (next->totr.xmin - prev->totr.xmax) < (width + (min_margin * 2.0f));
|
|
|
|
float margin = width;
|
|
|
|
/* NODE_TEST will be used later, so disable for all nodes */
|
|
ntreeNodeFlagSet(ntree, NODE_TEST, false);
|
|
|
|
/* insert->totr isn't updated yet,
|
|
* so totr_insert is used to get the correct worldspace coords */
|
|
rctf totr_insert;
|
|
node_to_updated_rect(insert, &totr_insert);
|
|
|
|
/* frame attachment wasn't handled yet
|
|
* so we search the frame that the node will be attached to later */
|
|
insert->parent = node_find_frame_to_attach(region, ntree, mouse_xy);
|
|
|
|
/* this makes sure nodes are also correctly offset when inserting a node on top of a frame
|
|
* without actually making it a part of the frame (because mouse isn't intersecting it)
|
|
* - logic here is similar to node_find_frame_to_attach */
|
|
if (!insert->parent ||
|
|
(prev->parent && (prev->parent == next->parent) && (prev->parent != insert->parent))) {
|
|
bNode *frame;
|
|
rctf totr_frame;
|
|
|
|
/* check nodes front to back */
|
|
for (frame = (bNode *)ntree->nodes.last; frame; frame = frame->prev) {
|
|
/* skip selected, those are the nodes we want to attach */
|
|
if ((frame->type != NODE_FRAME) || (frame->flag & NODE_SELECT)) {
|
|
continue;
|
|
}
|
|
|
|
/* for some reason frame y coords aren't correct yet */
|
|
node_to_updated_rect(frame, &totr_frame);
|
|
|
|
if (BLI_rctf_isect_x(&totr_frame, totr_insert.xmin) &&
|
|
BLI_rctf_isect_x(&totr_frame, totr_insert.xmax)) {
|
|
if (BLI_rctf_isect_y(&totr_frame, totr_insert.ymin) ||
|
|
BLI_rctf_isect_y(&totr_frame, totr_insert.ymax)) {
|
|
/* frame isn't insert->parent actually, but this is needed to make offsetting
|
|
* nodes work correctly for above checked cases (it is restored later) */
|
|
insert->parent = frame;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* *** ensure offset at the left (or right for right_alignment case) of insert_node *** */
|
|
|
|
float dist = right_alignment ? totr_insert.xmin - prev->totr.xmax :
|
|
next->totr.xmin - totr_insert.xmax;
|
|
/* distance between insert_node and prev is smaller than min margin */
|
|
if (dist < min_margin) {
|
|
const float addval = (min_margin - dist) * (right_alignment ? 1.0f : -1.0f);
|
|
|
|
node_offset_apply(insert, addval);
|
|
|
|
totr_insert.xmin += addval;
|
|
totr_insert.xmax += addval;
|
|
margin += min_margin;
|
|
}
|
|
|
|
/* *** ensure offset at the right (or left for right_alignment case) of insert_node *** */
|
|
|
|
dist = right_alignment ? next->totr.xmin - totr_insert.xmax : totr_insert.xmin - prev->totr.xmax;
|
|
/* distance between insert_node and next is smaller than min margin */
|
|
if (dist < min_margin) {
|
|
const float addval = (min_margin - dist) * (right_alignment ? 1.0f : -1.0f);
|
|
if (needs_alignment) {
|
|
bNode *offs_node = right_alignment ? next : prev;
|
|
if (!offs_node->parent || offs_node->parent == insert->parent ||
|
|
nodeIsChildOf(offs_node->parent, insert)) {
|
|
node_offset_apply(offs_node, addval);
|
|
}
|
|
else if (!insert->parent && offs_node->parent) {
|
|
node_offset_apply(nodeFindRootParent(offs_node), addval);
|
|
}
|
|
margin = addval;
|
|
}
|
|
/* enough room is available, but we want to ensure the min margin at the right */
|
|
else {
|
|
/* offset inserted node so that min margin is kept at the right */
|
|
node_offset_apply(insert, -addval);
|
|
}
|
|
}
|
|
|
|
if (needs_alignment) {
|
|
iofsd->insert_parent = insert->parent;
|
|
iofsd->offset_x = margin;
|
|
|
|
/* flag all parents of insert as offset to prevent them from being offset */
|
|
nodeParentsIter(insert, node_parents_offset_flag_enable_cb, nullptr);
|
|
/* iterate over entire chain and apply offsets */
|
|
nodeChainIter(ntree,
|
|
right_alignment ? next : prev,
|
|
node_link_insert_offset_chain_cb,
|
|
iofsd,
|
|
!right_alignment);
|
|
}
|
|
|
|
insert->parent = init_parent;
|
|
}
|
|
|
|
/**
|
|
* Modal handler for insert offset animation
|
|
*/
|
|
static int node_insert_offset_modal(bContext *C, wmOperator *UNUSED(op), const wmEvent *event)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
NodeInsertOfsData *iofsd = snode->runtime->iofsd;
|
|
bool redraw = false;
|
|
|
|
if (!snode || event->type != TIMER || iofsd == nullptr ||
|
|
iofsd->anim_timer != event->customdata) {
|
|
return OPERATOR_PASS_THROUGH;
|
|
}
|
|
|
|
const float duration = (float)iofsd->anim_timer->duration;
|
|
|
|
/* handle animation - do this before possibly aborting due to duration, since
|
|
* main thread might be so busy that node hasn't reached final position yet */
|
|
LISTBASE_FOREACH (bNode *, node, &snode->edittree->nodes) {
|
|
if (UNLIKELY(node->anim_ofsx)) {
|
|
const float endval = node->anim_init_locx + node->anim_ofsx;
|
|
if (IS_EQF(node->locx, endval) == false) {
|
|
node->locx = BLI_easing_cubic_ease_in_out(
|
|
duration, node->anim_init_locx, node->anim_ofsx, NODE_INSOFS_ANIM_DURATION);
|
|
if (node->anim_ofsx < 0) {
|
|
CLAMP_MIN(node->locx, endval);
|
|
}
|
|
else {
|
|
CLAMP_MAX(node->locx, endval);
|
|
}
|
|
redraw = true;
|
|
}
|
|
}
|
|
}
|
|
if (redraw) {
|
|
ED_region_tag_redraw(CTX_wm_region(C));
|
|
}
|
|
|
|
/* end timer + free insert offset data */
|
|
if (duration > NODE_INSOFS_ANIM_DURATION) {
|
|
WM_event_remove_timer(CTX_wm_manager(C), nullptr, iofsd->anim_timer);
|
|
|
|
LISTBASE_FOREACH (bNode *, node, &snode->edittree->nodes) {
|
|
node->anim_init_locx = node->anim_ofsx = 0.0f;
|
|
}
|
|
|
|
snode->runtime->iofsd = nullptr;
|
|
MEM_freeN(iofsd);
|
|
|
|
return (OPERATOR_FINISHED | OPERATOR_PASS_THROUGH);
|
|
}
|
|
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
#undef NODE_INSOFS_ANIM_DURATION
|
|
|
|
static int node_insert_offset_invoke(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
const SpaceNode *snode = CTX_wm_space_node(C);
|
|
NodeInsertOfsData *iofsd = snode->runtime->iofsd;
|
|
|
|
if (!iofsd || !iofsd->insert) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
BLI_assert((snode->flag & SNODE_SKIP_INSOFFSET) == 0);
|
|
|
|
iofsd->ntree = snode->edittree;
|
|
iofsd->anim_timer = WM_event_add_timer(CTX_wm_manager(C), CTX_wm_window(C), TIMER, 0.02);
|
|
|
|
node_link_insert_offset_ntree(
|
|
iofsd, CTX_wm_region(C), event->mval, (snode->insert_ofs_dir == SNODE_INSERTOFS_DIR_RIGHT));
|
|
|
|
/* add temp handler */
|
|
WM_event_add_modal_handler(C, op);
|
|
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
void NODE_OT_insert_offset(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Insert Offset";
|
|
ot->description = "Automatically offset nodes on insertion";
|
|
ot->idname = "NODE_OT_insert_offset";
|
|
|
|
/* callbacks */
|
|
ot->invoke = node_insert_offset_invoke;
|
|
ot->modal = node_insert_offset_modal;
|
|
ot->poll = ED_operator_node_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_BLOCKING;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Note Link Insert
|
|
* \{ */
|
|
|
|
/* assumes link with NODE_LINKFLAG_HILITE set */
|
|
void ED_node_link_insert(Main *bmain, ScrArea *area)
|
|
{
|
|
bNode *select;
|
|
SpaceNode *snode;
|
|
if (!ed_node_link_conditions(area, true, &snode, &select)) {
|
|
return;
|
|
}
|
|
|
|
/* get the link */
|
|
bNodeLink *link;
|
|
for (link = (bNodeLink *)snode->edittree->links.first; link; link = link->next) {
|
|
if (link->flag & NODE_LINKFLAG_HILITE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (link) {
|
|
bNodeSocket *best_input = get_main_socket(&select->inputs);
|
|
bNodeSocket *best_output = get_main_socket(&select->outputs);
|
|
|
|
if (best_input && best_output) {
|
|
bNode *node = link->tonode;
|
|
bNodeSocket *sockto = link->tosock;
|
|
|
|
link->tonode = select;
|
|
link->tosock = best_input;
|
|
node_remove_extra_links(snode, link);
|
|
link->flag &= ~NODE_LINKFLAG_HILITE;
|
|
|
|
bNodeLink *new_link = nodeAddLink(snode->edittree, select, best_output, node, sockto);
|
|
|
|
/* Copy the socket index for the new link, and reset it for the old link. This way the
|
|
* relative order of links is preserved, and the links get drawn in the right place. */
|
|
new_link->multi_input_socket_index = link->multi_input_socket_index;
|
|
link->multi_input_socket_index = 0;
|
|
|
|
/* set up insert offset data, it needs stuff from here */
|
|
if ((snode->flag & SNODE_SKIP_INSOFFSET) == 0) {
|
|
NodeInsertOfsData *iofsd = (NodeInsertOfsData *)MEM_callocN(sizeof(NodeInsertOfsData),
|
|
__func__);
|
|
|
|
iofsd->insert = select;
|
|
iofsd->prev = link->fromnode;
|
|
iofsd->next = node;
|
|
|
|
snode->runtime->iofsd = iofsd;
|
|
}
|
|
|
|
ntreeUpdateTree(bmain, snode->edittree); /* needed for pointers */
|
|
snode_update(snode, select);
|
|
ED_node_tag_update_id((ID *)snode->edittree);
|
|
ED_node_tag_update_id(snode->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** \} */
|