Similar to other removed UI layout functions, this removes uiItemEnumO* functions and replaces them by calling `uiLayout::op` and writing enum properties to the returned RNA pointer. Part of: #117604 Pull Request: https://projects.blender.org/blender/blender/pulls/141632
1656 lines
55 KiB
C++
1656 lines
55 KiB
C++
/* SPDX-FileCopyrightText: 2005 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup spnode
|
|
*/
|
|
|
|
#include <cstdlib>
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "DNA_anim_types.h"
|
|
#include "DNA_node_types.h"
|
|
|
|
#include "BLI_listbase.h"
|
|
#include "BLI_map.hh"
|
|
#include "BLI_math_vector.h"
|
|
#include "BLI_math_vector.hh"
|
|
#include "BLI_math_vector_types.hh"
|
|
#include "BLI_rand.hh"
|
|
#include "BLI_set.hh"
|
|
#include "BLI_string.h"
|
|
#include "BLI_vector.hh"
|
|
|
|
#include "BLT_translation.hh"
|
|
|
|
#include "BKE_action.hh"
|
|
#include "BKE_animsys.h"
|
|
#include "BKE_context.hh"
|
|
#include "BKE_lib_id.hh"
|
|
#include "BKE_library.hh"
|
|
#include "BKE_main.hh"
|
|
#include "BKE_main_invariants.hh"
|
|
#include "BKE_node_runtime.hh"
|
|
#include "BKE_node_tree_update.hh"
|
|
#include "BKE_report.hh"
|
|
|
|
#include "ANIM_action.hh"
|
|
|
|
#include "DEG_depsgraph_build.hh"
|
|
|
|
#include "ED_node.hh"
|
|
#include "ED_node_preview.hh"
|
|
#include "ED_render.hh"
|
|
#include "ED_screen.hh"
|
|
|
|
#include "RNA_access.hh"
|
|
#include "RNA_define.hh"
|
|
#include "RNA_path.hh"
|
|
#include "RNA_prototypes.hh"
|
|
|
|
#include "WM_api.hh"
|
|
#include "WM_types.hh"
|
|
|
|
#include "UI_resources.hh"
|
|
|
|
#include "NOD_common.hh"
|
|
#include "NOD_composite.hh"
|
|
#include "NOD_geometry.hh"
|
|
#include "NOD_node_declaration.hh"
|
|
#include "NOD_shader.h"
|
|
#include "NOD_socket.hh"
|
|
#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 StringRef group_ntree_idname(bContext *C)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
return snode->tree_idname;
|
|
}
|
|
|
|
StringRef node_group_idname(const 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 StringRef node_idname)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNode *node = bke::node_get_active(*snode->edittree);
|
|
|
|
if (node && node->idname == node_idname) {
|
|
return node;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/* Maps old to new identifiers for simulation input node pairing. */
|
|
static void remap_pairing(bNodeTree &dst_tree,
|
|
Span<bNode *> nodes,
|
|
const Map<int32_t, int32_t> &identifier_map)
|
|
{
|
|
for (bNode *dst_node : nodes) {
|
|
if (bke::all_zone_input_node_types().contains(dst_node->type_legacy)) {
|
|
const bke::bNodeZoneType &zone_type = *bke::zone_type_by_node_type(dst_node->type_legacy);
|
|
int &output_node_id = zone_type.get_corresponding_output_id(*dst_node);
|
|
if (output_node_id == 0) {
|
|
continue;
|
|
}
|
|
output_node_id = identifier_map.lookup_default(output_node_id, 0);
|
|
if (output_node_id == 0) {
|
|
blender::nodes::update_node_declaration_and_sockets(dst_tree, *dst_node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Edit Group Operator
|
|
* \{ */
|
|
|
|
static wmOperatorStatus node_group_edit_exec(bContext *C, wmOperator *op)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
ARegion *region = CTX_wm_region(C);
|
|
const StringRef 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(region, snode, ngroup, gnode);
|
|
}
|
|
}
|
|
else {
|
|
ED_node_tree_pop(region, snode);
|
|
}
|
|
|
|
WM_event_add_notifier(C, NC_SCENE | ND_NODES, nullptr);
|
|
WM_event_add_notifier(C, NC_NODE | ND_NODE_GIZMO, 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;
|
|
|
|
PropertyRNA *prop = RNA_def_boolean(ot->srna, "exit", false, "Exit", "");
|
|
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \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 StringRef src_basepath,
|
|
const StringRef dst_basepath)
|
|
{
|
|
AnimationBasePathChange *basepath_change = (AnimationBasePathChange *)MEM_callocN(
|
|
sizeof(*basepath_change), AT);
|
|
basepath_change->src_basepath = BLI_strdupn(src_basepath.data(), src_basepath.size());
|
|
basepath_change->dst_basepath = BLI_strdupn(dst_basepath.data(), dst_basepath.size());
|
|
return basepath_change;
|
|
}
|
|
|
|
static void animation_basepath_change_free(AnimationBasePathChange *basepath_change)
|
|
{
|
|
if (basepath_change->src_basepath != basepath_change->dst_basepath) {
|
|
MEM_freeN(basepath_change->src_basepath);
|
|
}
|
|
MEM_freeN(basepath_change->dst_basepath);
|
|
MEM_freeN(basepath_change);
|
|
}
|
|
|
|
static void update_nested_node_refs_after_ungroup(bNodeTree &ntree,
|
|
const bNodeTree &ngroup,
|
|
const bNode &gnode,
|
|
const Map<int32_t, int32_t> &node_identifier_map)
|
|
{
|
|
for (bNestedNodeRef &ref : ntree.nested_node_refs_span()) {
|
|
if (ref.path.node_id != gnode.identifier) {
|
|
continue;
|
|
}
|
|
const bNestedNodeRef *child_ref = ngroup.find_nested_node_ref(ref.path.id_in_node);
|
|
if (!child_ref) {
|
|
continue;
|
|
}
|
|
constexpr int32_t missing_id = -1;
|
|
const int32_t new_node_id = node_identifier_map.lookup_default(child_ref->path.node_id,
|
|
missing_id);
|
|
if (new_node_id == missing_id) {
|
|
continue;
|
|
}
|
|
ref.path.node_id = new_node_id;
|
|
ref.path.id_in_node = child_ref->path.id_in_node;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \return True if successful.
|
|
*/
|
|
static void 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 = bke::node_tree_copy_tree(bmain, *ngroup);
|
|
|
|
/* Add the nodes into the `ntree`. */
|
|
Vector<bNode *> new_nodes;
|
|
Map<int32_t, int32_t> node_identifier_map;
|
|
LISTBASE_FOREACH_MUTABLE (bNode *, node, &wgroup->nodes) {
|
|
new_nodes.append(node);
|
|
/* Remove interface nodes.
|
|
* This also removes remaining links to and from interface nodes.
|
|
*/
|
|
if (node->is_group_input() || node->is_group_output()) {
|
|
/* We must delay removal since sockets will reference this node. see: #52092 */
|
|
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. */
|
|
std::optional<std::string> old_animation_basepath;
|
|
if (wgroup->adt) {
|
|
PointerRNA ptr = RNA_pointer_create_discrete(&wgroup->id, &RNA_Node, node);
|
|
old_animation_basepath = RNA_path_from_ID_to_struct(&ptr);
|
|
}
|
|
|
|
/* migrate node */
|
|
BLI_remlink(&wgroup->nodes, node);
|
|
BLI_addtail(&ntree->nodes, node);
|
|
const int32_t old_identifier = node->identifier;
|
|
bke::node_unique_id(*ntree, *node);
|
|
bke::node_unique_name(*ntree, *node);
|
|
node_identifier_map.add(old_identifier, node->identifier);
|
|
|
|
BKE_ntree_update_tag_node_new(ntree, node);
|
|
|
|
if (wgroup->adt) {
|
|
PointerRNA ptr = RNA_pointer_create_discrete(&ntree->id, &RNA_Node, node);
|
|
const std::optional<std::string> new_animation_basepath = RNA_path_from_ID_to_struct(&ptr);
|
|
BLI_addtail(&anim_basepaths,
|
|
animation_basepath_change_new(*old_animation_basepath, *new_animation_basepath));
|
|
}
|
|
|
|
node->location[0] += gnode->location[0];
|
|
node->location[1] += gnode->location[1];
|
|
|
|
node->flag |= NODE_SELECT;
|
|
}
|
|
wgroup->runtime->nodes_by_id.clear();
|
|
|
|
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) {
|
|
/* firstly, wgroup needs to temporary dummy action
|
|
* that can be destroyed, as it shares copies */
|
|
bAction *waction = reinterpret_cast<bAction *>(BKE_id_copy(bmain, &wgroup->adt->action->id));
|
|
const bool assign_ok = animrig::assign_action(waction, {wgroup->id, *wgroup->adt});
|
|
BLI_assert_msg(assign_ok, "assigning a copy of an already-assigned Action should work");
|
|
UNUSED_VARS_NDEBUG(assign_ok);
|
|
|
|
/* 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) {
|
|
const bool unassign_ok = animrig::unassign_action({wgroup->id, *wgroup->adt});
|
|
BLI_assert_msg(unassign_ok, "unassigning an Action that was just assigned should work");
|
|
UNUSED_VARS_NDEBUG(unassign_ok);
|
|
BKE_id_free(bmain, waction);
|
|
}
|
|
}
|
|
|
|
remap_pairing(*ntree, new_nodes, node_identifier_map);
|
|
|
|
/* 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->is_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)) {
|
|
bke::node_add_link(
|
|
*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->is_group_output() && (tlink->tonode->flag & NODE_DO_OUTPUT)) {
|
|
if (STREQ(tlink->tosock->identifier, identifier)) {
|
|
bke::node_add_link(
|
|
*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) {
|
|
bke::node_remove_node(bmain, *ntree, *node, false);
|
|
}
|
|
|
|
update_nested_node_refs_after_ungroup(*ntree, *ngroup, *gnode, node_identifier_map);
|
|
|
|
/* delete the group instance and dereference group tree */
|
|
bke::node_remove_node(bmain, *ntree, *gnode, true);
|
|
}
|
|
|
|
static wmOperatorStatus node_group_ungroup_exec(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
const StringRef node_idname = node_group_idname(C);
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), bmain);
|
|
|
|
Vector<bNode *> nodes_to_ungroup;
|
|
for (bNode *node : snode->edittree->all_nodes()) {
|
|
if (node->flag & NODE_SELECT) {
|
|
if (node->idname == node_idname) {
|
|
if (node->id != nullptr) {
|
|
nodes_to_ungroup.append(node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (nodes_to_ungroup.is_empty()) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
for (bNode *node : nodes_to_ungroup) {
|
|
node_group_ungroup(bmain, snode->edittree, node);
|
|
}
|
|
BKE_main_ensure_invariants(*CTX_data_main(C));
|
|
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)
|
|
{
|
|
node_deselect_all(ntree);
|
|
|
|
ListBase anim_basepaths = {nullptr, nullptr};
|
|
|
|
Map<bNode *, bNode *> node_map;
|
|
Map<const bNodeSocket *, bNodeSocket *> socket_map;
|
|
Map<int32_t, int32_t> node_identifier_map;
|
|
|
|
/* Add selected nodes into the ntree, ignoring interface nodes. */
|
|
VectorSet<bNode *> nodes_to_move = get_selected_nodes(ngroup);
|
|
nodes_to_move.remove_if(
|
|
[](const bNode *node) { return node->is_group_input() || node->is_group_output(); });
|
|
|
|
for (bNode *node : nodes_to_move) {
|
|
bNode *newnode;
|
|
if (make_copy) {
|
|
newnode = bke::node_copy_with_mapping(&ntree, *node, LIB_ID_COPY_DEFAULT, true, socket_map);
|
|
node_identifier_map.add(node->identifier, newnode->identifier);
|
|
}
|
|
else {
|
|
newnode = node;
|
|
BLI_remlink(&ngroup.nodes, newnode);
|
|
BLI_addtail(&ntree.nodes, newnode);
|
|
const int32_t old_identifier = node->identifier;
|
|
bke::node_unique_id(ntree, *newnode);
|
|
bke::node_unique_name(ntree, *newnode);
|
|
node_identifier_map.add(old_identifier, newnode->identifier);
|
|
}
|
|
node_map.add_new(node, newnode);
|
|
|
|
/* 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 = RNA_pointer_create_discrete(&ngroup.id, &RNA_Node, newnode);
|
|
if (const std::optional<std::string> path = RNA_path_from_ID_to_struct(&ptr)) {
|
|
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)) {
|
|
bke::node_detach_node(ngroup, *newnode);
|
|
}
|
|
|
|
newnode->location[0] += offset.x;
|
|
newnode->location[1] += offset.y;
|
|
}
|
|
if (!make_copy) {
|
|
bke::node_rebuild_id_vector(ngroup);
|
|
}
|
|
|
|
/* add internal links to the ntree */
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &ngroup.links) {
|
|
const bool fromselect = (link->fromnode && nodes_to_move.contains(link->fromnode));
|
|
const bool toselect = (link->tonode && nodes_to_move.contains(link->tonode));
|
|
|
|
if (make_copy) {
|
|
/* make a copy of internal links */
|
|
if (fromselect && toselect) {
|
|
bke::node_add_link(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) {
|
|
bke::node_remove_link(&ngroup, *link);
|
|
}
|
|
}
|
|
}
|
|
|
|
remap_pairing(ntree, nodes_to_move, node_identifier_map);
|
|
|
|
for (bNode *node : node_map.values()) {
|
|
bke::node_declaration_ensure(ntree, *node);
|
|
}
|
|
|
|
/* 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 wmOperatorStatus node_group_separate_exec(bContext *C, wmOperator *op)
|
|
{
|
|
Main *bmain = CTX_data_main(C);
|
|
ARegion *region = CTX_wm_region(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(region, snode);
|
|
|
|
BKE_main_ensure_invariants(*CTX_data_main(C));
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
static wmOperatorStatus 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);
|
|
|
|
layout->operator_context_set(WM_OP_EXEC_DEFAULT);
|
|
PointerRNA op_ptr = layout->op("NODE_OT_group_separate", IFACE_("Copy"), ICON_NONE);
|
|
RNA_enum_set(&op_ptr, "type", NODE_GS_COPY);
|
|
op_ptr = layout->op("NODE_OT_group_separate", IFACE_("Move"), ICON_NONE);
|
|
RNA_enum_set(&op_ptr, "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 VectorSet<bNode *> get_nodes_to_group(bNodeTree &node_tree, bNode *group_node)
|
|
{
|
|
VectorSet<bNode *> nodes_to_group = get_selected_nodes(node_tree);
|
|
nodes_to_group.remove_if(
|
|
[](bNode *node) { return node->is_group_input() || node->is_group_output(); });
|
|
nodes_to_group.remove(group_node);
|
|
return nodes_to_group;
|
|
}
|
|
|
|
static bool node_group_make_test_selected(bNodeTree &ntree,
|
|
const VectorSet<bNode *> &nodes_to_group,
|
|
const StringRef ntree_idname,
|
|
ReportList &reports)
|
|
{
|
|
if (nodes_to_group.is_empty()) {
|
|
return false;
|
|
}
|
|
/* make a local pseudo node tree to pass to the node poll functions */
|
|
bNodeTree *ngroup = bke::node_tree_add_tree(nullptr, "Pseudo Node Group", ntree_idname);
|
|
BLI_SCOPED_DEFER([&]() {
|
|
bke::node_tree_free_tree(*ngroup);
|
|
MEM_freeN(ngroup);
|
|
});
|
|
|
|
/* check poll functions for selected nodes */
|
|
for (bNode *node : nodes_to_group) {
|
|
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,
|
|
"Cannot add node '%s' in a group:\n %s",
|
|
node->name,
|
|
disabled_hint);
|
|
}
|
|
else {
|
|
BKE_reportf(&reports, RPT_WARNING, "Cannot add node '%s' in a group", node->name);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* check if all connections are OK, no unselected node has both
|
|
* inputs and outputs to a selection */
|
|
ntree.ensure_topology_cache();
|
|
for (bNode *node : ntree.all_nodes()) {
|
|
if (nodes_to_group.contains(node)) {
|
|
continue;
|
|
}
|
|
auto sockets_connected_to_group = [&](const Span<bNodeSocket *> sockets) {
|
|
for (const bNodeSocket *socket : sockets) {
|
|
for (const bNodeSocket *other_socket : socket->directly_linked_sockets()) {
|
|
if (nodes_to_group.contains(const_cast<bNode *>(&other_socket->owner_node()))) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
if (sockets_connected_to_group(node->input_sockets()) &&
|
|
sockets_connected_to_group(node->output_sockets()))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
/* Check if zone pairs are fully selected.
|
|
* Zone input or output nodes can only be grouped together with the paired node. */
|
|
for (const bke::bNodeZoneType *zone_type : bke::all_zone_types()) {
|
|
for (bNode *input_node : ntree.nodes_by_type(zone_type->input_idname)) {
|
|
if (bNode *output_node = zone_type->get_corresponding_output(ntree, *input_node)) {
|
|
const bool input_selected = nodes_to_group.contains(input_node);
|
|
const bool output_selected = nodes_to_group.contains(output_node);
|
|
if (input_selected && !output_selected) {
|
|
BKE_reportf(&reports,
|
|
RPT_WARNING,
|
|
"Cannot add zone input node '%s' to a group without its paired output '%s'",
|
|
input_node->name,
|
|
output_node->name);
|
|
return false;
|
|
}
|
|
if (output_selected && !input_selected) {
|
|
BKE_reportf(&reports,
|
|
RPT_WARNING,
|
|
"Cannot add zone output node '%s' to a group without its paired input '%s'",
|
|
output_node->name,
|
|
input_node->name);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void get_min_max_of_nodes(const Span<bNode *> nodes,
|
|
const bool use_size,
|
|
float2 &min,
|
|
float2 &max)
|
|
{
|
|
if (nodes.is_empty()) {
|
|
min = float2(0);
|
|
max = float2(0);
|
|
return;
|
|
}
|
|
|
|
INIT_MINMAX2(min, max);
|
|
for (const bNode *node : nodes) {
|
|
float2 loc(node->location);
|
|
math::min_max(loc, min, max);
|
|
if (use_size) {
|
|
loc.x += node->width;
|
|
loc.y -= node->height;
|
|
math::min_max(loc, min, max);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skip reroute nodes when finding the socket to use as an example for a new group interface
|
|
* item. This moves "inward" into nodes selected for grouping to find properties like whether a
|
|
* connected socket has a hidden value. It only works in trivial situations-- a single line of
|
|
* connected reroutes with no branching.
|
|
*/
|
|
static const bNodeSocket &find_socket_to_use_for_interface(const bNodeTree &node_tree,
|
|
const bNodeSocket &socket)
|
|
{
|
|
if (node_tree.has_available_link_cycle()) {
|
|
return socket;
|
|
}
|
|
const bNode &node = socket.owner_node();
|
|
if (!node.is_reroute()) {
|
|
return socket;
|
|
}
|
|
const bNodeSocket &other_socket = socket.in_out == SOCK_IN ? node.output_socket(0) :
|
|
node.input_socket(0);
|
|
if (!other_socket.is_logically_linked()) {
|
|
return socket;
|
|
}
|
|
return *other_socket.logically_linked_sockets().first();
|
|
}
|
|
|
|
/**
|
|
* The output sockets of group nodes usually have consciously given names so they have
|
|
* precedence over socket names the link points to.
|
|
*/
|
|
static bool prefer_node_for_interface_name(const bNode &node)
|
|
{
|
|
return node.is_group() || node.is_group_input() || node.is_group_output();
|
|
}
|
|
|
|
static bNodeTreeInterfaceSocket *add_interface_from_socket(const bNodeTree &original_tree,
|
|
bNodeTree &tree_for_interface,
|
|
const bNodeSocket &socket)
|
|
{
|
|
/* The "example socket" has to have the same `in_out` status as the new interface socket. */
|
|
const bNodeSocket &socket_for_io = find_socket_to_use_for_interface(original_tree, socket);
|
|
const bNode &node_for_io = socket_for_io.owner_node();
|
|
const bNodeSocket &socket_for_name = prefer_node_for_interface_name(socket.owner_node()) ?
|
|
socket :
|
|
socket_for_io;
|
|
return bke::node_interface::add_interface_socket_from_node(
|
|
tree_for_interface, node_for_io, socket_for_io, socket_for_io.idname, socket_for_name.name);
|
|
}
|
|
|
|
static void update_nested_node_refs_after_moving_nodes_into_group(
|
|
bNodeTree &ntree,
|
|
bNodeTree &group,
|
|
bNode &gnode,
|
|
const Map<int32_t, int32_t> &node_identifier_map)
|
|
{
|
|
/* Update nested node references in the parent and child node tree. */
|
|
RandomNumberGenerator rng = RandomNumberGenerator::from_random_seed();
|
|
Vector<bNestedNodeRef> new_nested_node_refs;
|
|
/* Keep all nested node references that were in the group before. */
|
|
for (const bNestedNodeRef &ref : group.nested_node_refs_span()) {
|
|
new_nested_node_refs.append(ref);
|
|
}
|
|
Set<int32_t> used_nested_node_ref_ids;
|
|
for (const bNestedNodeRef &ref : group.nested_node_refs_span()) {
|
|
used_nested_node_ref_ids.add(ref.id);
|
|
}
|
|
Map<bNestedNodePath, int32_t> new_id_by_old_path;
|
|
for (bNestedNodeRef &ref : ntree.nested_node_refs_span()) {
|
|
const int32_t new_node_id = node_identifier_map.lookup_default(ref.path.node_id, -1);
|
|
if (new_node_id == -1) {
|
|
/* The node was not moved between node groups. */
|
|
continue;
|
|
}
|
|
bNestedNodeRef new_ref = ref;
|
|
new_ref.path.node_id = new_node_id;
|
|
/* Find new unique identifier for the nested node ref. */
|
|
while (true) {
|
|
const int32_t new_id = rng.get_int32(INT32_MAX);
|
|
if (used_nested_node_ref_ids.add(new_id)) {
|
|
new_ref.id = new_id;
|
|
break;
|
|
}
|
|
}
|
|
new_id_by_old_path.add_new(ref.path, new_ref.id);
|
|
new_nested_node_refs.append(new_ref);
|
|
/* Updated the nested node ref in the parent so that it points to the same node that is now
|
|
* inside of a nested group. */
|
|
ref.path.node_id = gnode.identifier;
|
|
ref.path.id_in_node = new_ref.id;
|
|
}
|
|
MEM_SAFE_FREE(group.nested_node_refs);
|
|
group.nested_node_refs = MEM_malloc_arrayN<bNestedNodeRef>(new_nested_node_refs.size(),
|
|
__func__);
|
|
uninitialized_copy_n(
|
|
new_nested_node_refs.data(), new_nested_node_refs.size(), group.nested_node_refs);
|
|
group.nested_node_refs_num = new_nested_node_refs.size();
|
|
}
|
|
|
|
static void node_group_make_insert_selected(const bContext &C,
|
|
bNodeTree &ntree,
|
|
bNode *gnode,
|
|
const VectorSet<bNode *> &nodes_to_move)
|
|
{
|
|
Main *bmain = CTX_data_main(&C);
|
|
bNodeTree &group = *reinterpret_cast<bNodeTree *>(gnode->id);
|
|
BLI_assert(!nodes_to_move.contains(gnode));
|
|
|
|
node_deselect_all(group);
|
|
|
|
float2 min, max;
|
|
get_min_max_of_nodes(nodes_to_move, false, min, max);
|
|
const float2 center = math::midpoint(min, max);
|
|
|
|
float2 real_min, real_max;
|
|
get_min_max_of_nodes(nodes_to_move, true, real_min, real_max);
|
|
|
|
/* If only one node is selected expose all its sockets regardless of links. */
|
|
const bool expose_visible = nodes_to_move.size() == 1;
|
|
|
|
/* Reuse an existing output node or create a new one. */
|
|
group.ensure_topology_cache();
|
|
bNode *output_node = [&]() {
|
|
if (bNode *node = group.group_output_node()) {
|
|
return node;
|
|
}
|
|
bNode *output_node = bke::node_add_static_node(&C, group, NODE_GROUP_OUTPUT);
|
|
output_node->location[0] = real_max[0] - center[0] + 50.0f;
|
|
return output_node;
|
|
}();
|
|
|
|
/* Create new group input node for easier organization of the new nodes inside the group. */
|
|
bNode *input_node = bke::node_add_static_node(&C, group, NODE_GROUP_INPUT);
|
|
input_node->location[0] = real_min[0] - center[0] - 200.0f;
|
|
|
|
struct InputSocketInfo {
|
|
/* The unselected node the original link came from. */
|
|
bNode *from_node;
|
|
/* All the links that came from the socket on the unselected node. */
|
|
Vector<bNodeLink *> links;
|
|
const bNodeTreeInterfaceSocket *interface_socket;
|
|
};
|
|
|
|
struct OutputLinkInfo {
|
|
bNodeLink *link;
|
|
const bNodeTreeInterfaceSocket *interface_socket;
|
|
};
|
|
|
|
struct NewInternalLinkInfo {
|
|
bNode *node;
|
|
bNodeSocket *socket;
|
|
const bNodeTreeInterfaceSocket *interface_socket;
|
|
};
|
|
|
|
/* Map from single non-selected output sockets to potentially many selected input sockets. */
|
|
Map<bNodeSocket *, InputSocketInfo> input_links;
|
|
Vector<OutputLinkInfo> output_links;
|
|
Set<bNodeLink *> internal_links_to_move;
|
|
Set<bNodeLink *> links_to_remove;
|
|
/* Map old to new node identifiers. */
|
|
Map<int32_t, int32_t> node_identifier_map;
|
|
Vector<NewInternalLinkInfo> new_internal_links;
|
|
|
|
ntree.ensure_topology_cache();
|
|
/* Add all outputs first. */
|
|
for (bNode *node : nodes_to_move) {
|
|
for (bNodeSocket *output_socket : node->output_sockets()) {
|
|
if (!output_socket->is_visible()) {
|
|
for (bNodeLink *link : output_socket->directly_linked_links()) {
|
|
links_to_remove.add(link);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
for (bNodeLink *link : output_socket->directly_linked_links()) {
|
|
if (bke::node_link_is_hidden(*link)) {
|
|
links_to_remove.add(link);
|
|
continue;
|
|
}
|
|
if (link->tonode == gnode) {
|
|
links_to_remove.add(link);
|
|
continue;
|
|
}
|
|
if (nodes_to_move.contains(link->tonode)) {
|
|
internal_links_to_move.add(link);
|
|
continue;
|
|
}
|
|
bNodeTreeInterfaceSocket *io_socket = add_interface_from_socket(
|
|
ntree, group, *link->fromsock);
|
|
if (io_socket) {
|
|
output_links.append({link, io_socket});
|
|
}
|
|
else {
|
|
links_to_remove.add(link);
|
|
}
|
|
}
|
|
if (expose_visible && !output_socket->is_directly_linked()) {
|
|
bNodeTreeInterfaceSocket *io_socket = bke::node_interface::add_interface_socket_from_node(
|
|
group, *node, *output_socket);
|
|
if (io_socket) {
|
|
new_internal_links.append({node, output_socket, io_socket});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Now add all inputs. */
|
|
for (bNode *node : nodes_to_move) {
|
|
for (bNodeSocket *input_socket : node->input_sockets()) {
|
|
if (!input_socket->is_visible()) {
|
|
for (bNodeLink *link : input_socket->directly_linked_links()) {
|
|
links_to_remove.add(link);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
for (bNodeLink *link : input_socket->directly_linked_links()) {
|
|
if (bke::node_link_is_hidden(*link)) {
|
|
links_to_remove.add(link);
|
|
continue;
|
|
}
|
|
if (link->fromnode == gnode) {
|
|
links_to_remove.add(link);
|
|
continue;
|
|
}
|
|
if (nodes_to_move.contains(link->fromnode)) {
|
|
internal_links_to_move.add(link);
|
|
continue;
|
|
}
|
|
InputSocketInfo &info = input_links.lookup_or_add_default(link->fromsock);
|
|
info.from_node = link->fromnode;
|
|
info.links.append(link);
|
|
if (!info.interface_socket) {
|
|
info.interface_socket = add_interface_from_socket(ntree, group, *link->tosock);
|
|
}
|
|
}
|
|
if (expose_visible && !input_socket->is_directly_linked()) {
|
|
bNodeTreeInterfaceSocket *io_socket = bke::node_interface::add_interface_socket_from_node(
|
|
group, *node, *input_socket);
|
|
if (io_socket) {
|
|
new_internal_links.append({node, input_socket, io_socket});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Un-parent nodes when only the parent or child moves into the group. */
|
|
for (bNode *node : ntree.all_nodes()) {
|
|
if (node->parent && nodes_to_move.contains(node->parent) && !nodes_to_move.contains(node)) {
|
|
bke::node_detach_node(ntree, *node);
|
|
}
|
|
}
|
|
for (bNode *node : nodes_to_move) {
|
|
if (node->parent && !nodes_to_move.contains(node->parent)) {
|
|
bke::node_detach_node(ntree, *node);
|
|
}
|
|
}
|
|
|
|
/* Move animation data from the parent tree to the group. */
|
|
if (ntree.adt) {
|
|
ListBase anim_basepaths = {nullptr, nullptr};
|
|
for (bNode *node : nodes_to_move) {
|
|
PointerRNA ptr = RNA_pointer_create_discrete(&ntree.id, &RNA_Node, node);
|
|
if (const std::optional<std::string> path = RNA_path_from_ID_to_struct(&ptr)) {
|
|
BLI_addtail(&anim_basepaths, animation_basepath_change_new(*path, *path));
|
|
}
|
|
}
|
|
BKE_animdata_transfer_by_basepath(bmain, &ntree.id, &group.id, &anim_basepaths);
|
|
|
|
LISTBASE_FOREACH_MUTABLE (AnimationBasePathChange *, basepath_change, &anim_basepaths) {
|
|
animation_basepath_change_free(basepath_change);
|
|
}
|
|
}
|
|
|
|
/* Move nodes into the group. */
|
|
for (bNode *node : nodes_to_move) {
|
|
const int32_t old_identifier = node->identifier;
|
|
|
|
BLI_remlink(&ntree.nodes, node);
|
|
BLI_addtail(&group.nodes, node);
|
|
bke::node_unique_id(group, *node);
|
|
bke::node_unique_name(group, *node);
|
|
|
|
node_identifier_map.add(old_identifier, node->identifier);
|
|
|
|
BKE_ntree_update_tag_node_removed(&ntree);
|
|
BKE_ntree_update_tag_node_new(&group, node);
|
|
}
|
|
bke::node_rebuild_id_vector(ntree);
|
|
|
|
/* Update input and output node first, since the group node declaration can depend on them. */
|
|
nodes::update_node_declaration_and_sockets(group, *input_node);
|
|
nodes::update_node_declaration_and_sockets(group, *output_node);
|
|
|
|
/* move nodes in the group to the center */
|
|
for (bNode *node : nodes_to_move) {
|
|
node->location[0] -= center[0];
|
|
node->location[1] -= center[1];
|
|
}
|
|
|
|
for (bNodeLink *link : internal_links_to_move) {
|
|
BLI_remlink(&ntree.links, link);
|
|
BLI_addtail(&group.links, link);
|
|
BKE_ntree_update_tag_link_removed(&ntree);
|
|
BKE_ntree_update_tag_link_added(&group, link);
|
|
}
|
|
|
|
for (bNodeLink *link : links_to_remove) {
|
|
bke::node_remove_link(&ntree, *link);
|
|
}
|
|
|
|
/* Handle links to the new group inputs. */
|
|
for (const auto item : input_links.items()) {
|
|
const StringRefNull interface_identifier = item.value.interface_socket->identifier;
|
|
bNodeSocket *input_socket = node_group_input_find_socket(input_node, interface_identifier);
|
|
|
|
for (bNodeLink *link : item.value.links) {
|
|
/* Move the link into the new group, connected from the input node to the original socket. */
|
|
BLI_remlink(&ntree.links, link);
|
|
BLI_addtail(&group.links, link);
|
|
BKE_ntree_update_tag_link_removed(&ntree);
|
|
BKE_ntree_update_tag_link_added(&group, link);
|
|
link->fromnode = input_node;
|
|
link->fromsock = input_socket;
|
|
}
|
|
}
|
|
|
|
/* Handle links to new group outputs. */
|
|
for (const OutputLinkInfo &info : output_links) {
|
|
/* Create a new link inside of the group. */
|
|
const StringRefNull io_identifier = info.interface_socket->identifier;
|
|
bNodeSocket *output_sock = node_group_output_find_socket(output_node, io_identifier);
|
|
bke::node_add_link(
|
|
group, *info.link->fromnode, *info.link->fromsock, *output_node, *output_sock);
|
|
}
|
|
|
|
/* Handle new links inside the group. */
|
|
for (const NewInternalLinkInfo &info : new_internal_links) {
|
|
const StringRefNull io_identifier = info.interface_socket->identifier;
|
|
if (info.socket->in_out == SOCK_IN) {
|
|
bNodeSocket *input_socket = node_group_input_find_socket(input_node, io_identifier);
|
|
bke::node_add_link(group, *input_node, *input_socket, *info.node, *info.socket);
|
|
}
|
|
else {
|
|
bNodeSocket *output_socket = node_group_output_find_socket(output_node, io_identifier);
|
|
bke::node_add_link(group, *info.node, *info.socket, *output_node, *output_socket);
|
|
}
|
|
}
|
|
|
|
remap_pairing(group, nodes_to_move, node_identifier_map);
|
|
|
|
if (group.type == NTREE_GEOMETRY) {
|
|
bke::node_field_inferencing::update_field_inferencing(group);
|
|
}
|
|
|
|
if (ELEM(group.type, NTREE_GEOMETRY, NTREE_COMPOSIT)) {
|
|
bke::node_structure_type_inferencing::update_structure_type_interface(group);
|
|
}
|
|
|
|
nodes::update_node_declaration_and_sockets(ntree, *gnode);
|
|
|
|
/* Add new links to inputs outside of the group. */
|
|
for (const auto item : input_links.items()) {
|
|
const StringRefNull interface_identifier = item.value.interface_socket->identifier;
|
|
bNodeSocket *group_node_socket = node_group_find_input_socket(gnode, interface_identifier);
|
|
bke::node_add_link(ntree, *item.value.from_node, *item.key, *gnode, *group_node_socket);
|
|
}
|
|
|
|
/* Add new links to outputs outside the group. */
|
|
for (const OutputLinkInfo &info : output_links) {
|
|
/* Reconnect the link to the group node instead of the node now inside the group. */
|
|
info.link->fromnode = gnode;
|
|
info.link->fromsock = node_group_find_output_socket(gnode, info.interface_socket->identifier);
|
|
}
|
|
|
|
update_nested_node_refs_after_moving_nodes_into_group(ntree, group, *gnode, node_identifier_map);
|
|
|
|
BKE_main_ensure_invariants(*bmain);
|
|
}
|
|
|
|
static bNode *node_group_make_from_nodes(const bContext &C,
|
|
bNodeTree &ntree,
|
|
const VectorSet<bNode *> &nodes_to_group,
|
|
const StringRef ntype,
|
|
const StringRef ntreetype)
|
|
{
|
|
Main *bmain = CTX_data_main(&C);
|
|
|
|
float2 min, max;
|
|
get_min_max_of_nodes(nodes_to_group, false, min, max);
|
|
|
|
/* New node-tree. */
|
|
bNodeTree *ngroup = bke::node_tree_add_tree(bmain, "NodeGroup", ntreetype);
|
|
|
|
BKE_id_move_to_same_lib(*bmain, ngroup->id, ntree.id);
|
|
|
|
/* make group node */
|
|
bNode *gnode = bke::node_add_node(&C, ntree, ntype);
|
|
gnode->id = (ID *)ngroup;
|
|
|
|
gnode->location[0] = 0.5f * (min[0] + max[0]);
|
|
gnode->location[1] = 0.5f * (min[1] + max[1]);
|
|
|
|
node_group_make_insert_selected(C, ntree, gnode, nodes_to_group);
|
|
|
|
return gnode;
|
|
}
|
|
|
|
struct WrapperNodeGroupMapping {
|
|
int num_inputs = 0;
|
|
int num_outputs = 0;
|
|
Map<const bNodeSocket *, int> new_index_by_src_socket;
|
|
Map<int, int> new_by_old_panel_identifier;
|
|
Vector<int> exposed_input_indices;
|
|
Vector<int> exposed_output_indices;
|
|
|
|
bNodeSocket *get_new_input(const bNodeSocket *old_socket, bNode &new_node) const
|
|
{
|
|
if (const std::optional<int> index = new_index_by_src_socket.lookup_try(old_socket)) {
|
|
return &new_node.input_socket(*index);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bNodeSocket *get_new_output(const bNodeSocket *old_socket, bNode &new_node) const
|
|
{
|
|
if (const std::optional<int> index = new_index_by_src_socket.lookup_try(old_socket)) {
|
|
return &new_node.output_socket(*index);
|
|
}
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
static void add_node_group_interface_from_declaration_recursive(
|
|
bNodeTree &group,
|
|
const bNode &src_node,
|
|
const nodes::ItemDeclaration &item_decl,
|
|
bNodeTreeInterfacePanel *parent,
|
|
WrapperNodeGroupMapping &r_mapping)
|
|
{
|
|
if (const nodes::SocketDeclaration *socket_decl = dynamic_cast<const nodes::SocketDeclaration *>(
|
|
&item_decl))
|
|
{
|
|
const bNodeSocket &socket = src_node.socket_by_decl(*socket_decl);
|
|
if (!socket.is_available()) {
|
|
return;
|
|
}
|
|
bNodeTreeInterfaceSocket *io_socket = bke::node_interface::add_interface_socket_from_node(
|
|
group, src_node, socket);
|
|
if (!io_socket) {
|
|
return;
|
|
}
|
|
group.tree_interface.move_item_to_parent(io_socket->item, parent, INT32_MAX);
|
|
if (socket.is_input()) {
|
|
r_mapping.new_index_by_src_socket.add_new(&socket, r_mapping.num_inputs++);
|
|
r_mapping.exposed_input_indices.append(socket.index());
|
|
}
|
|
else {
|
|
r_mapping.new_index_by_src_socket.add_new(&socket, r_mapping.num_outputs++);
|
|
r_mapping.exposed_output_indices.append(socket.index());
|
|
}
|
|
}
|
|
else if (const nodes::PanelDeclaration *panel_decl =
|
|
dynamic_cast<const nodes::PanelDeclaration *>(&item_decl))
|
|
{
|
|
NodeTreeInterfacePanelFlag flag{};
|
|
if (panel_decl->default_collapsed) {
|
|
flag |= NODE_INTERFACE_PANEL_DEFAULT_CLOSED;
|
|
}
|
|
bNodeTreeInterfacePanel *io_panel = group.tree_interface.add_panel(
|
|
panel_decl->name, panel_decl->description, flag, parent);
|
|
r_mapping.new_by_old_panel_identifier.add_new(panel_decl->identifier, io_panel->identifier);
|
|
for (const nodes::ItemDeclaration *child_item_decl : panel_decl->items) {
|
|
add_node_group_interface_from_declaration_recursive(
|
|
group, src_node, *child_item_decl, io_panel, r_mapping);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bNodeTree *node_group_make_wrapper(const bContext &C,
|
|
const bNodeTree &src_tree,
|
|
const bNode &src_node,
|
|
WrapperNodeGroupMapping &r_mapping)
|
|
{
|
|
Main &bmain = *CTX_data_main(&C);
|
|
|
|
bNodeTree *dst_group = bke::node_tree_add_tree(
|
|
&bmain, bke::node_label(src_tree, src_node), src_tree.idname);
|
|
|
|
const nodes::NodeDeclaration &node_decl = *src_node.declaration();
|
|
for (const nodes::ItemDeclaration *item_decl : node_decl.root_items) {
|
|
add_node_group_interface_from_declaration_recursive(
|
|
*dst_group, src_node, *item_decl, nullptr, r_mapping);
|
|
}
|
|
|
|
/* Add the node that make up the wrapper node group. */
|
|
bNode &input_node = *bke::node_add_static_node(&C, *dst_group, NODE_GROUP_INPUT);
|
|
bNode &output_node = *bke::node_add_static_node(&C, *dst_group, NODE_GROUP_OUTPUT);
|
|
bNode &inner_node = *bke::node_copy(dst_group, src_node, 0, true);
|
|
|
|
/* Position nodes. */
|
|
input_node.location[0] = -300 - input_node.width;
|
|
output_node.location[0] = 300;
|
|
inner_node.location[0] = -src_node.width / 2;
|
|
inner_node.location[1] = 0;
|
|
inner_node.width = src_node.width;
|
|
inner_node.parent = nullptr;
|
|
|
|
/* This makes sure that all nodes have the correct sockets so that we can link. */
|
|
BKE_main_ensure_invariants(bmain, dst_group->id);
|
|
|
|
/* Expand all panels in wrapper node group. */
|
|
for (bNodePanelState &panel_state : inner_node.panel_states()) {
|
|
panel_state.flag &= ~NODE_PANEL_COLLAPSED;
|
|
}
|
|
/* Make all sockets visible in wrapper node group. */
|
|
for (bNodeSocket *socket : inner_node.input_sockets()) {
|
|
socket->flag &= ~SOCK_HIDDEN;
|
|
}
|
|
for (bNodeSocket *socket : inner_node.output_sockets()) {
|
|
socket->flag &= ~SOCK_HIDDEN;
|
|
}
|
|
|
|
const Array<bNodeSocket *> group_inputs = input_node.output_sockets().drop_back(1);
|
|
const Array<bNodeSocket *> group_outputs = output_node.input_sockets().drop_back(1);
|
|
const Array<bNodeSocket *> inner_inputs = inner_node.input_sockets();
|
|
const Array<bNodeSocket *> inner_outputs = inner_node.output_sockets();
|
|
BLI_assert(group_inputs.size() == r_mapping.exposed_input_indices.size());
|
|
BLI_assert(group_outputs.size() == r_mapping.exposed_output_indices.size());
|
|
|
|
/* Add links. */
|
|
for (const int i : group_inputs.index_range()) {
|
|
bke::node_add_link(*dst_group,
|
|
input_node,
|
|
*group_inputs[i],
|
|
inner_node,
|
|
*inner_inputs[r_mapping.exposed_input_indices[i]]);
|
|
}
|
|
for (const int i : group_outputs.index_range()) {
|
|
bke::node_add_link(*dst_group,
|
|
inner_node,
|
|
*inner_outputs[r_mapping.exposed_output_indices[i]],
|
|
output_node,
|
|
*group_outputs[i]);
|
|
}
|
|
|
|
BKE_main_ensure_invariants(bmain, dst_group->id);
|
|
return dst_group;
|
|
}
|
|
|
|
static bNode *node_group_make_from_node_declaration(bContext &C,
|
|
bNodeTree &ntree,
|
|
bNode &src_node,
|
|
const StringRef node_idname)
|
|
{
|
|
Main &bmain = *CTX_data_main(&C);
|
|
|
|
WrapperNodeGroupMapping mapping;
|
|
bNodeTree *wrapper_group = node_group_make_wrapper(C, ntree, src_node, mapping);
|
|
|
|
/* Create a group node. */
|
|
bNode *gnode = bke::node_add_node(&C, ntree, node_idname);
|
|
STRNCPY(gnode->name, BKE_id_name(wrapper_group->id));
|
|
bke::node_unique_name(ntree, *gnode);
|
|
|
|
/* Assign the newly created wrapper group to the new group node. */
|
|
gnode->id = &wrapper_group->id;
|
|
|
|
/* Position node exactly where the old node was. */
|
|
gnode->parent = src_node.parent;
|
|
gnode->width = std::max<float>(src_node.width, GROUP_NODE_MIN_WIDTH);
|
|
copy_v2_v2(gnode->location, src_node.location);
|
|
|
|
BKE_main_ensure_invariants(bmain);
|
|
ntree.ensure_topology_cache();
|
|
|
|
/* Keep old socket visibility. */
|
|
for (const bNodeSocket *src_socket : src_node.input_sockets()) {
|
|
if (bNodeSocket *new_socket = mapping.get_new_input(src_socket, *gnode)) {
|
|
new_socket->flag |= src_socket->flag & (SOCK_HIDDEN | SOCK_COLLAPSED);
|
|
}
|
|
}
|
|
for (const bNodeSocket *src_socket : src_node.output_sockets()) {
|
|
if (bNodeSocket *new_socket = mapping.get_new_output(src_socket, *gnode)) {
|
|
new_socket->flag |= src_socket->flag & (SOCK_HIDDEN | SOCK_COLLAPSED);
|
|
}
|
|
}
|
|
|
|
/* Keep old panel collapse status. */
|
|
const Span<bNodePanelState> src_panel_states = src_node.panel_states();
|
|
MutableSpan<bNodePanelState> new_panel_states = gnode->panel_states();
|
|
for (const bNodePanelState &src_panel_state : src_panel_states) {
|
|
if (const std::optional<int> new_identifier = mapping.new_by_old_panel_identifier.lookup_try(
|
|
src_panel_state.identifier))
|
|
{
|
|
for (bNodePanelState &new_panel_state : new_panel_states) {
|
|
if (new_panel_state.identifier == *new_identifier) {
|
|
SET_FLAG_FROM_TEST(new_panel_state.flag,
|
|
src_panel_state.flag & NODE_PANEL_COLLAPSED,
|
|
NODE_PANEL_COLLAPSED);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Relink links from old to new node. */
|
|
LISTBASE_FOREACH_MUTABLE (bNodeLink *, link, &ntree.links) {
|
|
if (link->tonode == &src_node) {
|
|
if (bNodeSocket *new_to_socket = mapping.get_new_input(link->tosock, *gnode)) {
|
|
link->tonode = gnode;
|
|
link->tosock = new_to_socket;
|
|
continue;
|
|
}
|
|
bke::node_remove_link(&ntree, *link);
|
|
continue;
|
|
}
|
|
if (link->fromnode == &src_node) {
|
|
if (bNodeSocket *new_from_socket = mapping.get_new_output(link->fromsock, *gnode)) {
|
|
link->fromnode = gnode;
|
|
link->fromsock = new_from_socket;
|
|
continue;
|
|
}
|
|
bke::node_remove_link(&ntree, *link);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* Remove the old node because it has been replaced. */
|
|
bke::node_remove_node(&bmain, ntree, src_node, true);
|
|
|
|
BKE_ntree_update_tag_node_property(&ntree, gnode);
|
|
BKE_main_ensure_invariants(bmain);
|
|
return gnode;
|
|
}
|
|
|
|
static wmOperatorStatus node_group_make_exec(bContext *C, wmOperator *op)
|
|
{
|
|
ARegion ®ion = *CTX_wm_region(C);
|
|
SpaceNode &snode = *CTX_wm_space_node(C);
|
|
bNodeTree &ntree = *snode.edittree;
|
|
const StringRef ntree_idname = group_ntree_idname(C);
|
|
const StringRef node_idname = node_group_idname(C);
|
|
Main *bmain = CTX_data_main(C);
|
|
|
|
ED_preview_kill_jobs(CTX_wm_manager(C), CTX_data_main(C));
|
|
|
|
VectorSet<bNode *> nodes_to_group = get_nodes_to_group(ntree, nullptr);
|
|
if (!node_group_make_test_selected(ntree, nodes_to_group, ntree_idname, *op->reports)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
bNode *gnode = nullptr;
|
|
if (nodes_to_group.size() == 1 && nodes_to_group[0]->declaration()) {
|
|
gnode = node_group_make_from_node_declaration(*C, ntree, *nodes_to_group[0], node_idname);
|
|
}
|
|
else {
|
|
gnode = node_group_make_from_nodes(*C, ntree, nodes_to_group, node_idname, ntree_idname);
|
|
}
|
|
|
|
if (gnode) {
|
|
bNodeTree *ngroup = (bNodeTree *)gnode->id;
|
|
|
|
bke::node_set_active(ntree, *gnode);
|
|
if (ngroup) {
|
|
ED_node_tree_push(®ion, &snode, ngroup, gnode);
|
|
}
|
|
}
|
|
|
|
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 wmOperatorStatus node_group_insert_exec(bContext *C, wmOperator *op)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
ARegion *region = CTX_wm_region(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
const StringRef node_idname = node_group_idname(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 = reinterpret_cast<bNodeTree *>(gnode->id);
|
|
VectorSet<bNode *> nodes_to_group = get_nodes_to_group(*ntree, gnode);
|
|
|
|
/* Make sure that there won't be a node group containing itself afterwards. */
|
|
for (const bNode *group : nodes_to_group) {
|
|
if (!group->is_group() || group->id == nullptr) {
|
|
continue;
|
|
}
|
|
if (bke::node_tree_contains_tree(*reinterpret_cast<bNodeTree *>(group->id), *ngroup)) {
|
|
BKE_reportf(
|
|
op->reports, RPT_WARNING, "Cannot insert group '%s' in '%s'", group->name, gnode->name);
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
}
|
|
|
|
if (!node_group_make_test_selected(*ntree, nodes_to_group, ngroup->idname, *op->reports)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
node_group_make_insert_selected(*C, *ntree, gnode, nodes_to_group);
|
|
|
|
bke::node_set_active(*ntree, *gnode);
|
|
ED_node_tree_push(region, snode, ngroup, gnode);
|
|
|
|
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;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/** \name Set Default Group Width Operator
|
|
* \{ */
|
|
|
|
static bool node_default_group_width_set_poll(bContext *C)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
if (!snode) {
|
|
return false;
|
|
}
|
|
bNodeTree *ntree = snode->edittree;
|
|
if (!ntree) {
|
|
return false;
|
|
}
|
|
if (!ID_IS_EDITABLE(ntree)) {
|
|
return false;
|
|
}
|
|
if (snode->nodetree == snode->edittree) {
|
|
/* Top-level node group does not have enough context to set the node width. */
|
|
CTX_wm_operator_poll_msg_set(C, "There is no parent group node in this context");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static wmOperatorStatus node_default_group_width_set_exec(bContext *C, wmOperator * /*op*/)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
bNodeTree *ntree = snode->edittree;
|
|
|
|
bNodeTreePath *last_path_item = static_cast<bNodeTreePath *>(snode->treepath.last);
|
|
bNodeTreePath *parent_path_item = last_path_item->prev;
|
|
if (!parent_path_item) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
bNodeTree *parent_ntree = parent_path_item->nodetree;
|
|
if (!parent_ntree) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
parent_ntree->ensure_topology_cache();
|
|
bNode *parent_node = bke::node_find_node_by_name(*parent_ntree, last_path_item->node_name);
|
|
if (!parent_node) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
ntree->default_group_node_width = parent_node->width;
|
|
WM_event_add_notifier(C, NC_NODE | NA_EDITED, nullptr);
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
void NODE_OT_default_group_width_set(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Set Default Group Node Width";
|
|
ot->description = "Set the width based on the parent group node in the current context";
|
|
ot->idname = "NODE_OT_default_group_width_set";
|
|
|
|
/* API callbacks. */
|
|
ot->exec = node_default_group_width_set_exec;
|
|
ot->poll = node_default_group_width_set_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/** \} */
|
|
|
|
} // namespace blender::ed::space_node
|