diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 277edcf85d9..c94f119714e 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -2230,6 +2230,7 @@ def km_node_editor(params): {"properties": [("replace", False)]}), ("node.link_make", {"type": 'J', "value": 'PRESS', "shift": True}, {"properties": [("replace", True)]}), + ("node.join_nodes", {"type": 'J', "value": 'PRESS', "ctrl": True}, None), op_menu("NODE_MT_add", {"type": 'A', "value": 'PRESS', "shift": True}), op_menu("NODE_MT_swap", {"type": 'S', "value": 'PRESS', "shift": True}), ("node.duplicate_move", {"type": 'D', "value": 'PRESS', "shift": True}, diff --git a/scripts/startup/bl_ui/space_node.py b/scripts/startup/bl_ui/space_node.py index b028b0fe437..398bb067435 100644 --- a/scripts/startup/bl_ui/space_node.py +++ b/scripts/startup/bl_ui/space_node.py @@ -440,6 +440,7 @@ class NODE_MT_node(Menu): layout.separator() layout.operator("node.join", text="Join in New Frame") layout.operator("node.detach", text="Remove from Frame") + layout.operator("node.join_nodes", text="Join Group Inputs") layout.separator() props = layout.operator("wm.call_panel", text="Rename...") diff --git a/source/blender/editors/space_node/node_intern.hh b/source/blender/editors/space_node/node_intern.hh index a13d2d0c664..6cb295eddda 100644 --- a/source/blender/editors/space_node/node_intern.hh +++ b/source/blender/editors/space_node/node_intern.hh @@ -341,6 +341,7 @@ void NODE_OT_parent_set(wmOperatorType *ot); void NODE_OT_join(wmOperatorType *ot); void NODE_OT_attach(wmOperatorType *ot); void NODE_OT_detach(wmOperatorType *ot); +void NODE_OT_join_nodes(wmOperatorType *ot); void NODE_OT_link_viewer(wmOperatorType *ot); diff --git a/source/blender/editors/space_node/node_ops.cc b/source/blender/editors/space_node/node_ops.cc index 7c27b98fa57..70cdb44fa62 100644 --- a/source/blender/editors/space_node/node_ops.cc +++ b/source/blender/editors/space_node/node_ops.cc @@ -101,6 +101,7 @@ void node_operatortypes() WM_operatortype_append(NODE_OT_join); WM_operatortype_append(NODE_OT_attach); WM_operatortype_append(NODE_OT_detach); + WM_operatortype_append(NODE_OT_join_nodes); WM_operatortype_append(NODE_OT_clipboard_copy); WM_operatortype_append(NODE_OT_clipboard_paste); diff --git a/source/blender/editors/space_node/node_relationships.cc b/source/blender/editors/space_node/node_relationships.cc index c01f3d8e645..9dc702d7297 100644 --- a/source/blender/editors/space_node/node_relationships.cc +++ b/source/blender/editors/space_node/node_relationships.cc @@ -2095,7 +2095,7 @@ void NODE_OT_parent_set(wmOperatorType *ot) /** \} */ /* -------------------------------------------------------------------- */ -/** \name Join Nodes Operator +/** \name Join Nodes in Frame Operator * \{ */ struct NodeJoinState { @@ -2176,7 +2176,7 @@ static const bNode *find_common_parent_node(const Span nodes) return candidates.last(); } -static wmOperatorStatus node_join_exec(bContext *C, wmOperator * /*op*/) +static wmOperatorStatus node_join_in_frame_exec(bContext *C, wmOperator * /*op*/) { Main &bmain = *CTX_data_main(C); SpaceNode &snode = *CTX_wm_space_node(C); @@ -2205,7 +2205,9 @@ static wmOperatorStatus node_join_exec(bContext *C, wmOperator * /*op*/) return OPERATOR_FINISHED; } -static wmOperatorStatus node_join_invoke(bContext *C, wmOperator *op, const wmEvent *event) +static wmOperatorStatus node_join_in_frame_invoke(bContext *C, + wmOperator *op, + const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceNode *snode = CTX_wm_space_node(C); @@ -2220,19 +2222,19 @@ static wmOperatorStatus node_join_invoke(bContext *C, wmOperator *op, const wmEv snode->runtime->cursor[0] /= UI_SCALE_FAC; snode->runtime->cursor[1] /= UI_SCALE_FAC; - return node_join_exec(C, op); + return node_join_in_frame_exec(C, op); } void NODE_OT_join(wmOperatorType *ot) { /* identifiers */ - ot->name = "Join Nodes"; + ot->name = "Join Nodes in Frame"; ot->description = "Attach selected nodes to a new common frame"; ot->idname = "NODE_OT_join"; /* API callbacks. */ - ot->exec = node_join_exec; - ot->invoke = node_join_invoke; + ot->exec = node_join_in_frame_exec; + ot->invoke = node_join_in_frame_invoke; ot->poll = ED_operator_node_editable; /* flags */ @@ -2241,6 +2243,114 @@ void NODE_OT_join(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Join Nodes Operator + * \{ */ + +static void join_group_inputs(bNodeTree &tree, VectorSet group_inputs, bNode *active_node) +{ + bNode *main_node = nullptr; + if (group_inputs.contains(active_node)) { + main_node = active_node; + } + else { + main_node = group_inputs[0]; + /* Move main node to average of all group inputs. */ + float2 location{}; + for (const bNode *node : group_inputs) { + location += node->location; + } + location /= float(group_inputs.size()); + copy_v2_v2(main_node->location, location); + } + tree.ensure_topology_cache(); + MultiValueMap old_link_map; + for (bNode *node : group_inputs) { + for (bNodeSocket *socket : node->output_sockets().drop_back(1)) { + old_link_map.add_multiple(socket, socket->directly_linked_links()); + } + } + MultiValueMap used_link_targets; + for (bNodeSocket *socket : main_node->output_sockets()) { + used_link_targets.add_multiple(socket, socket->directly_linked_sockets()); + } + for (bNode *node : group_inputs) { + if (node == main_node) { + continue; + } + bool keep_node = false; + + /* Using runtime data directly because we know the parts that are used are still valid. */ + for (const int group_input_i : node->runtime->outputs.index_range().drop_back(1)) { + bool keep_socket = false; + bNodeSocket &new_socket = *main_node->runtime->outputs[group_input_i]; + bNodeSocket &old_socket = *node->runtime->outputs[group_input_i]; + for (bNodeLink *link : old_link_map.lookup(&old_socket)) { + bNodeSocket &to_socket = *link->tosock; + if (used_link_targets.lookup(&new_socket).contains(&to_socket)) { + keep_node = true; + keep_socket = true; + continue; + } + used_link_targets.add(&new_socket, &to_socket); + link->fromsock = &new_socket; + link->fromnode = main_node; + new_socket.flag &= ~SOCK_HIDDEN; + BKE_ntree_update_tag_link_changed(&tree); + } + if (!keep_socket) { + old_socket.flag |= SOCK_HIDDEN; + } + } + if (!keep_node) { + bke::node_free_node(&tree, *node); + } + } +} + +static wmOperatorStatus node_join_nodes_exec(bContext *C, wmOperator *op) +{ + Main &bmain = *CTX_data_main(C); + SpaceNode &snode = *CTX_wm_space_node(C); + bNodeTree &ntree = *snode.edittree; + + bNode *active_node = bke::node_get_active(ntree); + VectorSet selected_nodes = get_selected_nodes(ntree); + if (selected_nodes.size() <= 1) { + return OPERATOR_CANCELLED; + } + + if (std::all_of(selected_nodes.begin(), selected_nodes.end(), [](const bNode *node) { + return node->is_group_input(); + })) + { + join_group_inputs(ntree, std::move(selected_nodes), active_node); + } + else { + BKE_report(op->reports, RPT_ERROR, "Selected nodes can't be joined"); + return OPERATOR_CANCELLED; + } + + BKE_main_ensure_invariants(bmain, snode.edittree->id); + WM_event_add_notifier(C, NC_NODE | ND_DISPLAY, nullptr); + + return OPERATOR_FINISHED; +} + +void NODE_OT_join_nodes(wmOperatorType *ot) +{ + ot->name = "Join Nodes"; + ot->description = "Merge selected group input nodes into one if possible"; + ot->idname = "NODE_OT_join_nodes"; + + ot->exec = node_join_nodes_exec; + ot->poll = ED_operator_node_editable; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Attach Operator * \{ */