* This patch just moves runtime data to the runtime struct to cleanup the dna struct. Arguably, some of this data should not even be there because it's very use case specific. This can be cleaned up separately. * `miniwidth` was removed completely, because it was not used anywhere. The corresponding rna property `width_hidden` is kept to avoid script breakage, but does not do anything (e.g. node wrangler sets it). * Since rna is in C, some helper functions where added to access the C++ runtime data from rna. * This size of `bNode` decreases from 432 to 368 bytes.
1181 lines
36 KiB
C++
1181 lines
36 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later
|
|
* Copyright 2005 Blender Foundation. All rights reserved. */
|
|
|
|
/** \file
|
|
* \ingroup spnode
|
|
*/
|
|
|
|
#include <cstdlib>
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "DNA_anim_types.h"
|
|
#include "DNA_node_types.h"
|
|
|
|
#include "BLI_linklist.h"
|
|
#include "BLI_listbase.h"
|
|
#include "BLI_map.hh"
|
|
#include "BLI_math_vec_types.hh"
|
|
#include "BLI_string.h"
|
|
#include "BLI_vector.hh"
|
|
|
|
#include "BLT_translation.h"
|
|
|
|
#include "BKE_action.h"
|
|
#include "BKE_animsys.h"
|
|
#include "BKE_context.h"
|
|
#include "BKE_lib_id.h"
|
|
#include "BKE_main.h"
|
|
#include "BKE_node_runtime.hh"
|
|
#include "BKE_node_tree_update.h"
|
|
#include "BKE_report.h"
|
|
|
|
#include "DEG_depsgraph_build.h"
|
|
|
|
#include "ED_node.h" /* own include */
|
|
#include "ED_render.h"
|
|
#include "ED_screen.h"
|
|
|
|
#include "RNA_access.h"
|
|
#include "RNA_define.h"
|
|
#include "RNA_path.h"
|
|
#include "RNA_prototypes.h"
|
|
|
|
#include "WM_api.h"
|
|
#include "WM_types.h"
|
|
|
|
#include "UI_resources.h"
|
|
|
|
#include "NOD_common.h"
|
|
#include "NOD_composite.h"
|
|
#include "NOD_geometry.h"
|
|
#include "NOD_shader.h"
|
|
#include "NOD_socket.h"
|
|
#include "NOD_texture.h"
|
|
|
|
#include "node_intern.hh" /* own include */
|
|
|
|
namespace blender::ed::space_node {
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Local Utilities
|
|
* \{ */
|
|
|
|
static bool node_group_operator_active_poll(bContext *C)
|
|
{
|
|
if (ED_operator_node_active(C)) {
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
|
|
/* Group operators only defined for standard node tree types.
|
|
* Disabled otherwise to allow python-nodes define their own operators
|
|
* with same key-map. */
|
|
if (STR_ELEM(snode->tree_idname,
|
|
"ShaderNodeTree",
|
|
"CompositorNodeTree",
|
|
"TextureNodeTree",
|
|
"GeometryNodeTree")) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool node_group_operator_editable(bContext *C)
|
|
{
|
|
if (ED_operator_node_editable(C)) {
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
|
|
/* Group operators only defined for standard node tree types.
|
|
* Disabled otherwise to allow python-nodes define their own operators
|
|
* with same key-map. */
|
|
if (ED_node_is_shader(snode) || ED_node_is_compositor(snode) || ED_node_is_texture(snode) ||
|
|
ED_node_is_geometry(snode)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static const char *group_ntree_idname(bContext *C)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
return snode->tree_idname;
|
|
}
|
|
|
|
const char *node_group_idname(bContext *C)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
|
|
if (ED_node_is_shader(snode)) {
|
|
return ntreeType_Shader->group_idname;
|
|
}
|
|
if (ED_node_is_compositor(snode)) {
|
|
return ntreeType_Composite->group_idname;
|
|
}
|
|
if (ED_node_is_texture(snode)) {
|
|
return ntreeType_Texture->group_idname;
|
|
}
|
|
if (ED_node_is_geometry(snode)) {
|
|
return ntreeType_Geometry->group_idname;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
static bNode *node_group_get_active(bContext *C, const char *node_idname)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNode *node = nodeGetActive(snode->edittree);
|
|
|
|
if (node && STREQ(node->idname, node_idname)) {
|
|
return node;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Edit Group Operator
|
|
* \{ */
|
|
|
|
static int node_group_edit_exec(bContext *C, wmOperator *op)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
const char *node_idname = node_group_idname(C);
|
|
const bool exit = RNA_boolean_get(op->ptr, "exit");
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
|
|
|
|
bNode *gnode = node_group_get_active(C, node_idname);
|
|
|
|
if (gnode && !exit) {
|
|
bNodeTree *ngroup = (bNodeTree *)gnode->id;
|
|
|
|
if (ngroup) {
|
|
ED_node_tree_push(snode, ngroup, gnode);
|
|
}
|
|
}
|
|
else {
|
|
ED_node_tree_pop(snode);
|
|
}
|
|
|
|
WM_event_add_notifier(C, NC_SCENE | ND_NODES, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_group_edit(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Edit Group";
|
|
ot->description = "Edit node group";
|
|
ot->idname = "NODE_OT_group_edit";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_group_edit_exec;
|
|
ot->poll = node_group_operator_active_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
RNA_def_boolean(ot->srna, "exit", false, "Exit", "");
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Ungroup Operator
|
|
* \{ */
|
|
|
|
/**
|
|
* The given paths will be owned by the returned instance.
|
|
* Both pointers are allowed to point to the same string.
|
|
*/
|
|
static AnimationBasePathChange *animation_basepath_change_new(const char *src_basepath,
|
|
const char *dst_basepath)
|
|
{
|
|
AnimationBasePathChange *basepath_change = (AnimationBasePathChange *)MEM_callocN(
|
|
sizeof(*basepath_change), AT);
|
|
basepath_change->src_basepath = src_basepath;
|
|
basepath_change->dst_basepath = dst_basepath;
|
|
return basepath_change;
|
|
}
|
|
|
|
static void animation_basepath_change_free(AnimationBasePathChange *basepath_change)
|
|
{
|
|
if (basepath_change->src_basepath != basepath_change->dst_basepath) {
|
|
MEM_freeN((void *)basepath_change->src_basepath);
|
|
}
|
|
MEM_freeN((void *)basepath_change->dst_basepath);
|
|
MEM_freeN(basepath_change);
|
|
}
|
|
|
|
/**
|
|
* \return True if successful.
|
|
*/
|
|
static bool node_group_ungroup(Main *bmain, bNodeTree *ntree, bNode *gnode)
|
|
{
|
|
ListBase anim_basepaths = {nullptr, nullptr};
|
|
Vector<bNode *> nodes_delayed_free;
|
|
const bNodeTree *ngroup = reinterpret_cast<const bNodeTree *>(gnode->id);
|
|
|
|
/* `wgroup` is a temporary copy of the #NodeTree we're merging in
|
|
* - All of wgroup's nodes are copied across to their new home.
|
|
* - `ngroup` (i.e. the source NodeTree) is left unscathed.
|
|
* - Temp copy. do change ID user-count for the copies.
|
|
*/
|
|
bNodeTree *wgroup = ntreeCopyTree(bmain, ngroup);
|
|
|
|
/* Add the nodes into the `ntree`. */
|
|
LISTBASE_FOREACH_MUTABLE (bNode *, node, &wgroup->nodes) {
|
|
/* Remove interface nodes.
|
|
* This also removes remaining links to and from interface nodes.
|
|
*/
|
|
if (ELEM(node->type, NODE_GROUP_INPUT, NODE_GROUP_OUTPUT)) {
|
|
/* We must delay removal since sockets will reference this node. see: T52092 */
|
|
nodes_delayed_free.append(node);
|
|
}
|
|
|
|
/* Keep track of this node's RNA "base" path (the part of the path identifying the node)
|
|
* if the old node-tree has animation data which potentially covers this node. */
|
|
const char *old_animation_basepath = nullptr;
|
|
if (wgroup->adt) {
|
|
PointerRNA ptr;
|
|
RNA_pointer_create(&wgroup->id, &RNA_Node, node, &ptr);
|
|
old_animation_basepath = RNA_path_from_ID_to_struct(&ptr);
|
|
}
|
|
|
|
/* migrate node */
|
|
BLI_remlink(&wgroup->nodes, node);
|
|
BLI_addtail(&ntree->nodes, node);
|
|
BKE_ntree_update_tag_node_new(ntree, node);
|
|
|
|
/* ensure unique node name in the node tree */
|
|
nodeUniqueName(ntree, node);
|
|
|
|
if (wgroup->adt) {
|
|
PointerRNA ptr;
|
|
RNA_pointer_create(&ntree->id, &RNA_Node, node, &ptr);
|
|
const char *new_animation_basepath = RNA_path_from_ID_to_struct(&ptr);
|
|
BLI_addtail(&anim_basepaths,
|
|
animation_basepath_change_new(old_animation_basepath, new_animation_basepath));
|
|
}
|
|
|
|
if (!node->parent) {
|
|
node->locx += gnode->locx;
|
|
node->locy += gnode->locy;
|
|
}
|
|
|
|
node->flag |= NODE_SELECT;
|
|
}
|
|
|
|
bNodeLink *glinks_first = (bNodeLink *)ntree->links.last;
|
|
|
|
/* Add internal links to the ntree */
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &wgroup->links) {
|
|
BLI_remlink(&wgroup->links, link);
|
|
BLI_addtail(&ntree->links, link);
|
|
BKE_ntree_update_tag_link_added(ntree, link);
|
|
}
|
|
|
|
bNodeLink *glinks_last = (bNodeLink *)ntree->links.last;
|
|
|
|
/* and copy across the animation,
|
|
* note that the animation data's action can be nullptr here */
|
|
if (wgroup->adt) {
|
|
bAction *waction;
|
|
|
|
/* firstly, wgroup needs to temporary dummy action
|
|
* that can be destroyed, as it shares copies */
|
|
waction = wgroup->adt->action = (bAction *)BKE_id_copy(bmain, &wgroup->adt->action->id);
|
|
|
|
/* now perform the moving */
|
|
BKE_animdata_transfer_by_basepath(bmain, &wgroup->id, &ntree->id, &anim_basepaths);
|
|
|
|
/* paths + their wrappers need to be freed */
|
|
LISTBASE_FOREACH_MUTABLE (AnimationBasePathChange *, basepath_change, &anim_basepaths) {
|
|
animation_basepath_change_free(basepath_change);
|
|
}
|
|
|
|
/* free temp action too */
|
|
if (waction) {
|
|
BKE_id_free(bmain, waction);
|
|
wgroup->adt->action = nullptr;
|
|
}
|
|
}
|
|
|
|
/* free the group tree (takes care of user count) */
|
|
BKE_id_free(bmain, wgroup);
|
|
|
|
/* restore external links to and from the gnode */
|
|
|
|
/* input links */
|
|
if (glinks_first != nullptr) {
|
|
for (bNodeLink *link = glinks_first->next; link != glinks_last->next; link = link->next) {
|
|
if (link->fromnode->type == NODE_GROUP_INPUT) {
|
|
const char *identifier = link->fromsock->identifier;
|
|
int num_external_links = 0;
|
|
|
|
/* find external links to this input */
|
|
for (bNodeLink *tlink = (bNodeLink *)ntree->links.first; tlink != glinks_first->next;
|
|
tlink = tlink->next) {
|
|
if (tlink->tonode == gnode && STREQ(tlink->tosock->identifier, identifier)) {
|
|
nodeAddLink(ntree, tlink->fromnode, tlink->fromsock, link->tonode, link->tosock);
|
|
num_external_links++;
|
|
}
|
|
}
|
|
|
|
/* if group output is not externally linked,
|
|
* convert the constant input value to ensure somewhat consistent behavior */
|
|
if (num_external_links == 0) {
|
|
/* TODO */
|
|
#if 0
|
|
bNodeSocket *sock = node_group_find_input_socket(gnode, identifier);
|
|
BLI_assert(sock);
|
|
|
|
nodeSocketCopy(
|
|
ntree, link->tosock->new_sock, link->tonode->new_node, ntree, sock, gnode);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Also iterate over new links to cover passthrough links. */
|
|
glinks_last = (bNodeLink *)ntree->links.last;
|
|
|
|
/* output links */
|
|
for (bNodeLink *link = (bNodeLink *)ntree->links.first; link != glinks_first->next;
|
|
link = link->next) {
|
|
if (link->fromnode == gnode) {
|
|
const char *identifier = link->fromsock->identifier;
|
|
int num_internal_links = 0;
|
|
|
|
/* find internal links to this output */
|
|
for (bNodeLink *tlink = glinks_first->next; tlink != glinks_last->next;
|
|
tlink = tlink->next) {
|
|
/* only use active output node */
|
|
if (tlink->tonode->type == NODE_GROUP_OUTPUT && (tlink->tonode->flag & NODE_DO_OUTPUT)) {
|
|
if (STREQ(tlink->tosock->identifier, identifier)) {
|
|
nodeAddLink(ntree, tlink->fromnode, tlink->fromsock, link->tonode, link->tosock);
|
|
num_internal_links++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* if group output is not internally linked,
|
|
* convert the constant output value to ensure somewhat consistent behavior */
|
|
if (num_internal_links == 0) {
|
|
/* TODO */
|
|
#if 0
|
|
bNodeSocket *sock = node_group_find_output_socket(gnode, identifier);
|
|
BLI_assert(sock);
|
|
|
|
nodeSocketCopy(ntree, link->tosock, link->tonode, ntree, sock, gnode);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (bNode *node : nodes_delayed_free) {
|
|
nodeRemoveNode(bmain, ntree, node, false);
|
|
}
|
|
|
|
/* delete the group instance and dereference group tree */
|
|
nodeRemoveNode(bmain, ntree, gnode, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
static int node_group_ungroup_exec(bContext *C, wmOperator *op)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
const char *node_idname = node_group_idname(C);
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
|
|
bNode *gnode = node_group_get_active(C, node_idname);
|
|
if (!gnode) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
if (gnode->id && node_group_ungroup(bmain, snode->edittree, gnode)) {
|
|
ED_node_tree_propagate_change(C, CTX_data_main(C), nullptr);
|
|
}
|
|
else {
|
|
BKE_report(op->reports, RPT_WARNING, "Cannot ungroup");
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_group_ungroup(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Ungroup";
|
|
ot->description = "Ungroup selected nodes";
|
|
ot->idname = "NODE_OT_group_ungroup";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_group_ungroup_exec;
|
|
ot->poll = node_group_operator_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Separate Operator
|
|
* \{ */
|
|
|
|
/**
|
|
* \return True if successful.
|
|
*/
|
|
static bool node_group_separate_selected(
|
|
Main &bmain, bNodeTree &ntree, bNodeTree &ngroup, const float2 &offset, const bool make_copy)
|
|
{
|
|
/* deselect all nodes in the target tree */
|
|
LISTBASE_FOREACH (bNode *, node, &ntree.nodes) {
|
|
nodeSetSelected(node, false);
|
|
}
|
|
|
|
ListBase anim_basepaths = {nullptr, nullptr};
|
|
|
|
Map<const bNode *, bNode *> node_map;
|
|
Map<const bNodeSocket *, bNodeSocket *> socket_map;
|
|
|
|
/* add selected nodes into the ntree */
|
|
LISTBASE_FOREACH_MUTABLE (bNode *, node, &ngroup.nodes) {
|
|
if (!(node->flag & NODE_SELECT)) {
|
|
continue;
|
|
}
|
|
|
|
/* ignore interface nodes */
|
|
if (ELEM(node->type, NODE_GROUP_INPUT, NODE_GROUP_OUTPUT)) {
|
|
nodeSetSelected(node, false);
|
|
continue;
|
|
}
|
|
|
|
bNode *newnode;
|
|
if (make_copy) {
|
|
/* make a copy */
|
|
newnode = bke::node_copy_with_mapping(&ngroup, *node, LIB_ID_COPY_DEFAULT, true, socket_map);
|
|
node_map.add_new(node, newnode);
|
|
}
|
|
else {
|
|
/* use the existing node */
|
|
newnode = node;
|
|
}
|
|
|
|
/* Keep track of this node's RNA "base" path (the part of the path identifying the node)
|
|
* if the old node-tree has animation data which potentially covers this node. */
|
|
if (ngroup.adt) {
|
|
PointerRNA ptr;
|
|
char *path;
|
|
|
|
RNA_pointer_create(&ngroup.id, &RNA_Node, newnode, &ptr);
|
|
path = RNA_path_from_ID_to_struct(&ptr);
|
|
|
|
if (path) {
|
|
BLI_addtail(&anim_basepaths, animation_basepath_change_new(path, path));
|
|
}
|
|
}
|
|
|
|
/* ensure valid parent pointers, detach if parent stays inside the group */
|
|
if (newnode->parent && !(newnode->parent->flag & NODE_SELECT)) {
|
|
nodeDetachNode(&ngroup, newnode);
|
|
}
|
|
|
|
/* migrate node */
|
|
BLI_remlink(&ngroup.nodes, newnode);
|
|
BLI_addtail(&ntree.nodes, newnode);
|
|
|
|
/* ensure unique node name in the node tree */
|
|
nodeUniqueName(&ntree, newnode);
|
|
|
|
if (!newnode->parent) {
|
|
newnode->locx += offset.x;
|
|
newnode->locy += offset.y;
|
|
}
|
|
}
|
|
|
|
/* add internal links to the ntree */
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &ngroup.links) {
|
|
const bool fromselect = (link->fromnode && (link->fromnode->flag & NODE_SELECT));
|
|
const bool toselect = (link->tonode && (link->tonode->flag & NODE_SELECT));
|
|
|
|
if (make_copy) {
|
|
/* make a copy of internal links */
|
|
if (fromselect && toselect) {
|
|
nodeAddLink(&ntree,
|
|
node_map.lookup(link->fromnode),
|
|
socket_map.lookup(link->fromsock),
|
|
node_map.lookup(link->tonode),
|
|
socket_map.lookup(link->tosock));
|
|
}
|
|
}
|
|
else {
|
|
/* move valid links over, delete broken links */
|
|
if (fromselect && toselect) {
|
|
BLI_remlink(&ngroup.links, link);
|
|
BLI_addtail(&ntree.links, link);
|
|
}
|
|
else if (fromselect || toselect) {
|
|
nodeRemLink(&ngroup, link);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* and copy across the animation,
|
|
* note that the animation data's action can be nullptr here */
|
|
if (ngroup.adt) {
|
|
/* now perform the moving */
|
|
BKE_animdata_transfer_by_basepath(&bmain, &ngroup.id, &ntree.id, &anim_basepaths);
|
|
|
|
/* paths + their wrappers need to be freed */
|
|
LISTBASE_FOREACH_MUTABLE (AnimationBasePathChange *, basepath_change, &anim_basepaths) {
|
|
animation_basepath_change_free(basepath_change);
|
|
}
|
|
}
|
|
|
|
BKE_ntree_update_tag_all(&ntree);
|
|
if (!make_copy) {
|
|
BKE_ntree_update_tag_all(&ngroup);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
enum eNodeGroupSeparateType {
|
|
NODE_GS_COPY,
|
|
NODE_GS_MOVE,
|
|
};
|
|
|
|
/* Operator Property */
|
|
static const EnumPropertyItem node_group_separate_types[] = {
|
|
{NODE_GS_COPY, "COPY", 0, "Copy", "Copy to parent node tree, keep group intact"},
|
|
{NODE_GS_MOVE, "MOVE", 0, "Move", "Move to parent node tree, remove from group"},
|
|
{0, nullptr, 0, nullptr, nullptr},
|
|
};
|
|
|
|
static int node_group_separate_exec(bContext *C, wmOperator *op)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
int type = RNA_enum_get(op->ptr, "type");
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
|
|
/* are we inside of a group? */
|
|
bNodeTree *ngroup = snode->edittree;
|
|
bNodeTree *nparent = ED_node_tree_get(snode, 1);
|
|
if (!nparent) {
|
|
BKE_report(op->reports, RPT_WARNING, "Not inside node group");
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
/* get node tree offset */
|
|
const float2 offset = space_node_group_offset(*snode);
|
|
|
|
switch (type) {
|
|
case NODE_GS_COPY:
|
|
if (!node_group_separate_selected(*bmain, *nparent, *ngroup, offset, true)) {
|
|
BKE_report(op->reports, RPT_WARNING, "Cannot separate nodes");
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
break;
|
|
case NODE_GS_MOVE:
|
|
if (!node_group_separate_selected(*bmain, *nparent, *ngroup, offset, false)) {
|
|
BKE_report(op->reports, RPT_WARNING, "Cannot separate nodes");
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* switch to parent tree */
|
|
ED_node_tree_pop(snode);
|
|
|
|
ED_node_tree_propagate_change(C, CTX_data_main(C), nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
static int node_group_separate_invoke(bContext *C, wmOperator * /*op*/, const wmEvent * /*event*/)
|
|
{
|
|
uiPopupMenu *pup = UI_popup_menu_begin(
|
|
C, CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Separate"), ICON_NONE);
|
|
uiLayout *layout = UI_popup_menu_layout(pup);
|
|
|
|
uiLayoutSetOperatorContext(layout, WM_OP_EXEC_DEFAULT);
|
|
uiItemEnumO(layout, "NODE_OT_group_separate", nullptr, 0, "type", NODE_GS_COPY);
|
|
uiItemEnumO(layout, "NODE_OT_group_separate", nullptr, 0, "type", NODE_GS_MOVE);
|
|
|
|
UI_popup_menu_end(C, pup);
|
|
|
|
return OPERATOR_INTERFACE;
|
|
}
|
|
|
|
void NODE_OT_group_separate(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Separate";
|
|
ot->description = "Separate selected nodes from the node group";
|
|
ot->idname = "NODE_OT_group_separate";
|
|
|
|
/* api callbacks */
|
|
ot->invoke = node_group_separate_invoke;
|
|
ot->exec = node_group_separate_exec;
|
|
ot->poll = node_group_operator_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
RNA_def_enum(ot->srna, "type", node_group_separate_types, NODE_GS_COPY, "Type", "");
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Make Group Operator
|
|
* \{ */
|
|
|
|
static bool node_group_make_use_node(bNode &node, bNode *gnode)
|
|
{
|
|
return (&node != gnode && !ELEM(node.type, NODE_GROUP_INPUT, NODE_GROUP_OUTPUT) &&
|
|
(node.flag & NODE_SELECT));
|
|
}
|
|
|
|
static bool node_group_make_test_selected(bNodeTree &ntree,
|
|
bNode *gnode,
|
|
const char *ntree_idname,
|
|
ReportList &reports)
|
|
{
|
|
int ok = true;
|
|
|
|
/* make a local pseudo node tree to pass to the node poll functions */
|
|
bNodeTree *ngroup = ntreeAddTree(nullptr, "Pseudo Node Group", ntree_idname);
|
|
|
|
/* check poll functions for selected nodes */
|
|
LISTBASE_FOREACH (bNode *, node, &ntree.nodes) {
|
|
if (node_group_make_use_node(*node, gnode)) {
|
|
const char *disabled_hint = nullptr;
|
|
if (node->typeinfo->poll_instance &&
|
|
!node->typeinfo->poll_instance(node, ngroup, &disabled_hint)) {
|
|
if (disabled_hint) {
|
|
BKE_reportf(&reports,
|
|
RPT_WARNING,
|
|
"Can not add node '%s' in a group:\n %s",
|
|
node->name,
|
|
disabled_hint);
|
|
}
|
|
else {
|
|
BKE_reportf(&reports, RPT_WARNING, "Can not add node '%s' in a group", node->name);
|
|
}
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
node->runtime->done = 0;
|
|
}
|
|
|
|
/* free local pseudo node tree again */
|
|
ntreeFreeTree(ngroup);
|
|
MEM_freeN(ngroup);
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
|
|
/* check if all connections are OK, no unselected node has both
|
|
* inputs and outputs to a selection */
|
|
LISTBASE_FOREACH (bNodeLink *, link, &ntree.links) {
|
|
if (node_group_make_use_node(*link->fromnode, gnode)) {
|
|
link->tonode->runtime->done |= 1;
|
|
}
|
|
if (node_group_make_use_node(*link->tonode, gnode)) {
|
|
link->fromnode->runtime->done |= 2;
|
|
}
|
|
}
|
|
LISTBASE_FOREACH (bNode *, node, &ntree.nodes) {
|
|
if (!(node->flag & NODE_SELECT) && node != gnode && node->runtime->done == 3) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int node_get_selected_minmax(
|
|
bNodeTree &ntree, bNode *gnode, float2 &min, float2 &max, bool use_size)
|
|
{
|
|
int totselect = 0;
|
|
|
|
INIT_MINMAX2(min, max);
|
|
LISTBASE_FOREACH (bNode *, node, &ntree.nodes) {
|
|
if (node_group_make_use_node(*node, gnode)) {
|
|
float2 loc;
|
|
nodeToView(node, node->offsetx, node->offsety, &loc.x, &loc.y);
|
|
math::min_max(loc, min, max);
|
|
if (use_size) {
|
|
loc.x += node->width;
|
|
loc.y -= node->height;
|
|
math::min_max(loc, min, max);
|
|
}
|
|
totselect++;
|
|
}
|
|
}
|
|
|
|
/* sane min/max if no selected nodes */
|
|
if (totselect == 0) {
|
|
min[0] = min[1] = max[0] = max[1] = 0.0f;
|
|
}
|
|
|
|
return totselect;
|
|
}
|
|
|
|
/**
|
|
* Redirect a link that are connecting a non-selected node to selected one.
|
|
* Create new socket or reuse an existing one that was connected from the same input.
|
|
* The output sockets of group nodes usually have consciously given names so they have
|
|
* precedence over socket names the link points to.
|
|
*
|
|
* \param ntree: The node tree that the node group is being created from.
|
|
* \param ngroup: The node tree of the new node group.
|
|
* \param gnode: The new group node in the original tree.
|
|
* \param input_node: The input node of the new node group.
|
|
* \param link: The incoming link that needs to be altered.
|
|
* \param reusable_sockets: Map for input socket interface lookup.
|
|
*/
|
|
static void node_group_make_redirect_incoming_link(
|
|
bNodeTree &ntree,
|
|
bNodeTree *ngroup,
|
|
bNode *gnode,
|
|
bNode *input_node,
|
|
bNodeLink *link,
|
|
Map<bNodeSocket *, bNodeSocket *> &reusable_sockets)
|
|
{
|
|
bNodeSocket *input_socket = reusable_sockets.lookup_default(link->fromsock, nullptr);
|
|
if (input_socket) {
|
|
/* The incoming link is from a socket that has already been linked to
|
|
* a socket interface of the input node.
|
|
* Change the source of the link to the previously created socket interface.
|
|
* Move the link into the node tree of the new group. */
|
|
link->fromnode = input_node;
|
|
link->fromsock = input_socket;
|
|
BLI_remlink(&ntree.links, link);
|
|
BLI_addtail(&ngroup->links, link);
|
|
}
|
|
else {
|
|
bNode *node_for_typeinfo = nullptr;
|
|
bNodeSocket *socket_for_typeinfo = nullptr;
|
|
/* Find a socket where typeinfo and name may come from. */
|
|
node_socket_skip_reroutes(
|
|
&ntree.links, link->tonode, link->tosock, &node_for_typeinfo, &socket_for_typeinfo);
|
|
bNodeSocket *socket_for_naming = socket_for_typeinfo;
|
|
|
|
/* Use the name of group node output sockets. */
|
|
if (ELEM(link->fromnode->type, NODE_GROUP_INPUT, NODE_GROUP, NODE_CUSTOM_GROUP)) {
|
|
socket_for_naming = link->fromsock;
|
|
}
|
|
|
|
bNodeSocket *iosock = ntreeAddSocketInterfaceFromSocketWithName(ngroup,
|
|
node_for_typeinfo,
|
|
socket_for_typeinfo,
|
|
socket_for_naming->idname,
|
|
socket_for_naming->name);
|
|
|
|
/* Update the group node and interface sockets so the new interface socket can be linked. */
|
|
node_group_update(&ntree, gnode);
|
|
node_group_input_update(ngroup, input_node);
|
|
|
|
/* Create new internal link. */
|
|
bNodeSocket *input_sock = node_group_input_find_socket(input_node, iosock->identifier);
|
|
nodeAddLink(ngroup, input_node, input_sock, link->tonode, link->tosock);
|
|
|
|
/* Redirect external link. */
|
|
link->tonode = gnode;
|
|
link->tosock = node_group_find_input_socket(gnode, iosock->identifier);
|
|
|
|
/* Remember which interface socket the link has been redirected to. */
|
|
reusable_sockets.add_new(link->fromsock, input_sock);
|
|
}
|
|
}
|
|
|
|
static void node_group_make_insert_selected(const bContext &C, bNodeTree &ntree, bNode *gnode)
|
|
{
|
|
Main *bmain = CTX_data_main(&C);
|
|
bNodeTree *ngroup = (bNodeTree *)gnode->id;
|
|
bool expose_visible = false;
|
|
|
|
/* XXX rough guess, not nice but we don't have access to UI constants here ... */
|
|
static const float offsetx = 200;
|
|
static const float offsety = 0.0f;
|
|
|
|
/* deselect all nodes in the target tree */
|
|
LISTBASE_FOREACH (bNode *, node, &ngroup->nodes) {
|
|
nodeSetSelected(node, false);
|
|
}
|
|
|
|
float2 center, min, max;
|
|
const int totselect = node_get_selected_minmax(ntree, gnode, min, max, false);
|
|
add_v2_v2v2(center, min, max);
|
|
mul_v2_fl(center, 0.5f);
|
|
|
|
float2 real_min, real_max;
|
|
node_get_selected_minmax(ntree, gnode, real_min, real_max, true);
|
|
|
|
/* auto-add interface for "solo" nodes */
|
|
if (totselect == 1) {
|
|
expose_visible = true;
|
|
}
|
|
|
|
ListBase anim_basepaths = {nullptr, nullptr};
|
|
|
|
/* Detach unselected nodes inside frames when the frame is put into the group. Otherwise the
|
|
* `parent` pointer becomes dangling. */
|
|
LISTBASE_FOREACH (bNode *, node, &ntree.nodes) {
|
|
if (node->parent == nullptr) {
|
|
continue;
|
|
}
|
|
if (node_group_make_use_node(*node->parent, gnode) &&
|
|
!node_group_make_use_node(*node, gnode)) {
|
|
nodeDetachNode(&ntree, node);
|
|
}
|
|
}
|
|
|
|
/* move nodes over */
|
|
LISTBASE_FOREACH_MUTABLE (bNode *, node, &ntree.nodes) {
|
|
if (node_group_make_use_node(*node, gnode)) {
|
|
/* Keep track of this node's RNA "base" path (the part of the pat identifying the node)
|
|
* if the old node-tree has animation data which potentially covers this node. */
|
|
if (ntree.adt) {
|
|
PointerRNA ptr;
|
|
char *path;
|
|
|
|
RNA_pointer_create(&ntree.id, &RNA_Node, node, &ptr);
|
|
path = RNA_path_from_ID_to_struct(&ptr);
|
|
|
|
if (path) {
|
|
BLI_addtail(&anim_basepaths, animation_basepath_change_new(path, path));
|
|
}
|
|
}
|
|
|
|
/* ensure valid parent pointers, detach if parent stays outside the group */
|
|
if (node->parent && !(node->parent->flag & NODE_SELECT)) {
|
|
nodeDetachNode(&ntree, node);
|
|
}
|
|
|
|
/* change node-collection membership */
|
|
BLI_remlink(&ntree.nodes, node);
|
|
BLI_addtail(&ngroup->nodes, node);
|
|
BKE_ntree_update_tag_node_removed(&ntree);
|
|
BKE_ntree_update_tag_node_new(ngroup, node);
|
|
|
|
/* ensure unique node name in the ngroup */
|
|
nodeUniqueName(ngroup, node);
|
|
}
|
|
}
|
|
|
|
/* move animation data over */
|
|
if (ntree.adt) {
|
|
BKE_animdata_transfer_by_basepath(bmain, &ntree.id, &ngroup->id, &anim_basepaths);
|
|
|
|
/* paths + their wrappers need to be freed */
|
|
LISTBASE_FOREACH_MUTABLE (AnimationBasePathChange *, basepath_change, &anim_basepaths) {
|
|
animation_basepath_change_free(basepath_change);
|
|
}
|
|
}
|
|
|
|
/* node groups don't use internal cached data */
|
|
ntreeFreeCache(ngroup);
|
|
|
|
/* create input node */
|
|
bNode *input_node = nodeAddStaticNode(&C, ngroup, NODE_GROUP_INPUT);
|
|
input_node->locx = real_min[0] - center[0] - offsetx;
|
|
input_node->locy = -offsety;
|
|
|
|
/* create output node */
|
|
bNode *output_node = nodeAddStaticNode(&C, ngroup, NODE_GROUP_OUTPUT);
|
|
output_node->locx = real_max[0] - center[0] + offsetx * 0.25f;
|
|
output_node->locy = -offsety;
|
|
|
|
/* relink external sockets */
|
|
|
|
/* A map from link sources to input sockets already connected. */
|
|
Map<bNodeSocket *, bNodeSocket *> reusable_sockets;
|
|
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &ntree.links) {
|
|
const bool fromselect = node_group_make_use_node(*link->fromnode, gnode);
|
|
const bool toselect = node_group_make_use_node(*link->tonode, gnode);
|
|
|
|
if ((fromselect && link->tonode == gnode) || (toselect && link->fromnode == gnode)) {
|
|
/* remove all links to/from the gnode.
|
|
* this can remove link information, but there's no general way to preserve it.
|
|
*/
|
|
nodeRemLink(&ntree, link);
|
|
}
|
|
else if (toselect && !fromselect) {
|
|
/* Remove hidden links to not create unconnected sockets in the interface. */
|
|
if (nodeLinkIsHidden(link)) {
|
|
nodeRemLink(&ntree, link);
|
|
continue;
|
|
}
|
|
|
|
node_group_make_redirect_incoming_link(
|
|
ntree, ngroup, gnode, input_node, link, reusable_sockets);
|
|
}
|
|
else if (fromselect && !toselect) {
|
|
/* Remove hidden links to not create unconnected sockets in the interface. */
|
|
if (nodeLinkIsHidden(link)) {
|
|
nodeRemLink(&ntree, link);
|
|
continue;
|
|
}
|
|
|
|
/* First check whether the source of this link is already connected to an output.
|
|
* If yes, reuse that output instead of duplicating it. */
|
|
bool connected = false;
|
|
LISTBASE_FOREACH (bNodeLink *, olink, &ngroup->links) {
|
|
if (olink->fromsock == link->fromsock && olink->tonode == output_node) {
|
|
bNodeSocket *output_sock = node_group_find_output_socket(gnode,
|
|
olink->tosock->identifier);
|
|
link->fromnode = gnode;
|
|
link->fromsock = output_sock;
|
|
connected = true;
|
|
}
|
|
}
|
|
|
|
if (!connected) {
|
|
bNodeSocket *link_sock;
|
|
bNode *link_node;
|
|
node_socket_skip_reroutes(
|
|
&ntree.links, link->fromnode, link->fromsock, &link_node, &link_sock);
|
|
bNodeSocket *iosock = ntreeAddSocketInterfaceFromSocket(ngroup, link_node, link_sock);
|
|
|
|
/* update the group node and interface node sockets,
|
|
* so the new interface socket can be linked.
|
|
*/
|
|
node_group_update(&ntree, gnode);
|
|
node_group_output_update(ngroup, output_node);
|
|
|
|
/* create new internal link */
|
|
bNodeSocket *output_sock = node_group_output_find_socket(output_node, iosock->identifier);
|
|
nodeAddLink(ngroup, link->fromnode, link->fromsock, output_node, output_sock);
|
|
|
|
/* redirect external link */
|
|
link->fromnode = gnode;
|
|
link->fromsock = node_group_find_output_socket(gnode, iosock->identifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* move internal links */
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &ntree.links) {
|
|
const bool fromselect = node_group_make_use_node(*link->fromnode, gnode);
|
|
const bool toselect = node_group_make_use_node(*link->tonode, gnode);
|
|
|
|
if (fromselect && toselect) {
|
|
BLI_remlink(&ntree.links, link);
|
|
BLI_addtail(&ngroup->links, link);
|
|
}
|
|
}
|
|
|
|
/* move nodes in the group to the center */
|
|
LISTBASE_FOREACH (bNode *, node, &ngroup->nodes) {
|
|
if (node_group_make_use_node(*node, gnode) && !node->parent) {
|
|
node->locx -= center[0];
|
|
node->locy -= center[1];
|
|
}
|
|
}
|
|
|
|
/* Expose all unlinked sockets too but only the visible ones. */
|
|
if (expose_visible) {
|
|
LISTBASE_FOREACH (bNode *, node, &ngroup->nodes) {
|
|
if (node_group_make_use_node(*node, gnode)) {
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) {
|
|
bool skip = false;
|
|
LISTBASE_FOREACH (bNodeLink *, link, &ngroup->links) {
|
|
if (link->tosock == sock) {
|
|
skip = true;
|
|
break;
|
|
}
|
|
}
|
|
if (sock->flag & (SOCK_HIDDEN | SOCK_UNAVAIL)) {
|
|
skip = true;
|
|
}
|
|
if (skip) {
|
|
continue;
|
|
}
|
|
|
|
bNodeSocket *iosock = ntreeAddSocketInterfaceFromSocket(ngroup, node, sock);
|
|
|
|
node_group_input_update(ngroup, input_node);
|
|
|
|
/* create new internal link */
|
|
bNodeSocket *input_sock = node_group_input_find_socket(input_node, iosock->identifier);
|
|
nodeAddLink(ngroup, input_node, input_sock, node, sock);
|
|
}
|
|
|
|
LISTBASE_FOREACH (bNodeSocket *, sock, &node->outputs) {
|
|
bool skip = false;
|
|
LISTBASE_FOREACH (bNodeLink *, link, &ngroup->links) {
|
|
if (link->fromsock == sock) {
|
|
skip = true;
|
|
}
|
|
}
|
|
if (sock->flag & (SOCK_HIDDEN | SOCK_UNAVAIL)) {
|
|
skip = true;
|
|
}
|
|
if (skip) {
|
|
continue;
|
|
}
|
|
|
|
bNodeSocket *iosock = ntreeAddSocketInterfaceFromSocket(ngroup, node, sock);
|
|
|
|
node_group_output_update(ngroup, output_node);
|
|
|
|
/* create new internal link */
|
|
bNodeSocket *output_sock = node_group_output_find_socket(output_node,
|
|
iosock->identifier);
|
|
nodeAddLink(ngroup, node, sock, output_node, output_sock);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static bNode *node_group_make_from_selected(const bContext &C,
|
|
bNodeTree &ntree,
|
|
const char *ntype,
|
|
const char *ntreetype)
|
|
{
|
|
Main *bmain = CTX_data_main(&C);
|
|
|
|
float2 min, max;
|
|
const int totselect = node_get_selected_minmax(ntree, nullptr, min, max, false);
|
|
/* don't make empty group */
|
|
if (totselect == 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
/* New node-tree. */
|
|
bNodeTree *ngroup = ntreeAddTree(bmain, "NodeGroup", ntreetype);
|
|
|
|
/* make group node */
|
|
bNode *gnode = nodeAddNode(&C, &ntree, ntype);
|
|
gnode->id = (ID *)ngroup;
|
|
|
|
gnode->locx = 0.5f * (min[0] + max[0]);
|
|
gnode->locy = 0.5f * (min[1] + max[1]);
|
|
|
|
node_group_make_insert_selected(C, ntree, gnode);
|
|
|
|
return gnode;
|
|
}
|
|
|
|
static int node_group_make_exec(bContext *C, wmOperator *op)
|
|
{
|
|
SpaceNode &snode = *CTX_wm_space_node(C);
|
|
bNodeTree &ntree = *snode.edittree;
|
|
const char *ntree_idname = group_ntree_idname(C);
|
|
const char *node_idname = node_group_idname(C);
|
|
Main *bmain = CTX_data_main(C);
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
|
|
|
|
if (!node_group_make_test_selected(ntree, nullptr, ntree_idname, *op->reports)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
bNode *gnode = node_group_make_from_selected(*C, ntree, node_idname, ntree_idname);
|
|
|
|
if (gnode) {
|
|
bNodeTree *ngroup = (bNodeTree *)gnode->id;
|
|
|
|
nodeSetActive(&ntree, gnode);
|
|
if (ngroup) {
|
|
ED_node_tree_push(&snode, ngroup, gnode);
|
|
}
|
|
}
|
|
|
|
ED_node_tree_propagate_change(C, bmain, nullptr);
|
|
|
|
WM_event_add_notifier(C, NC_NODE | NA_ADDED, nullptr);
|
|
|
|
/* We broke relations in node tree, need to rebuild them in the graphs. */
|
|
DEG_relations_tag_update(bmain);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_group_make(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Make Group";
|
|
ot->description = "Make group from selected nodes";
|
|
ot->idname = "NODE_OT_group_make";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_group_make_exec;
|
|
ot->poll = node_group_operator_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Group Insert Operator
|
|
* \{ */
|
|
|
|
static int node_group_insert_exec(bContext *C, wmOperator *op)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
const char *node_idname = node_group_idname(C);
|
|
Main *bmain = CTX_data_main(C);
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
|
|
|
|
bNode *gnode = node_group_get_active(C, node_idname);
|
|
|
|
if (!gnode || !gnode->id) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
bNodeTree *ngroup = (bNodeTree *)gnode->id;
|
|
if (!node_group_make_test_selected(*ntree, gnode, ngroup->idname, *op->reports)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
node_group_make_insert_selected(*C, *ntree, gnode);
|
|
|
|
nodeSetActive(ntree, gnode);
|
|
ED_node_tree_push(snode, ngroup, gnode);
|
|
ED_node_tree_propagate_change(C, bmain, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void NODE_OT_group_insert(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Group Insert";
|
|
ot->description = "Insert selected nodes into a node group";
|
|
ot->idname = "NODE_OT_group_insert";
|
|
|
|
/* api callbacks */
|
|
ot->exec = node_group_insert_exec;
|
|
ot->poll = node_group_operator_editable;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
} // namespace blender::ed::space_node
|