This adds a new Format String node which simplifies constructing strings from multiple values. The node takes a format string and a dynamic number of additional parameters as input. The format string determines how the other inputs are inserted into the string. Only integer, float and string inputs are supported for now. It supports two different format syntaxes: * Python compatible format syntax which also mostly matches the behavior of the `fmt` C++ library. Most of this is supported, but there are some small limitations. * Syntax of the form `###.##` where each `#` stands for a digit. This is the syntax that was introduced in #134860. This node greatly simplifies common string operations which would have required potentially many nodes before to convert numbers to strings and to concatenate them. It also makes new conversions possible that were not supported before. This node can also be used to insert e.g. frame numbers into a file path which was surprisingly complex before. This node has special behavior for the name of new inputs. For the purpose of the node, the name of the inputs must be valid identifiers and it's usually helpful when they are short. New names are therefore initialized to be single characters. If possible, the first character of the linked input is used. This works well when connecting e.g. a Separate Vector/Color node. Otherwise, inputs are named `a` to `z` by default. If that's not possible, the source socket name is used instead (converted to be a valid identifier). If that still doesn't work, the name is made unique using the normal `.001` mechanism except that `_` instead of `.` is used as separator to make the name a valid identifier. Python Syntax references: * Python: https://docs.python.org/3/library/string.html#formatspec * `fmt`: https://fmt.dev/latest/syntax/ More detailed notes about compatibility with the above syntax specifications: * Conversion using e.g. `!r` like in Python is not supported (maybe the future). * Sub-attribute access like `{vector.x}` is not supported (maybe the future). * Using `%` like in Python is not supported (maybe in future). * Using `#` for an alternate form is not supported. This might help in the future to make the syntax compatible with #134860. * Using `L` like in the `fmt` library is not supported because it depends on the locale which is not good for determinism. * Grouping with e.g. thousands separators using e.g. `,` or `_` like in Python is not supported (maybe in future). Need to think about the locale here too. * Mixing of unnamed (`{}`) and named (`{x} or {0}`) specifiers is allowed. However, all unnamed specifiers must come before any named specifier. The implementation uses the `fmt` library for the actual formatting. However, the inputs are preprocessed to give us more control over the exact supported syntax and error messages. The code is already somewhat written so that many strings could be formatted with the same format but that's not actually used yet because we don't have string fields yet. Error messages are propagated using a new mechanism that allows a limited form of error propagation from multi-functions to the node that evaluates them. Currently, this only works in fairly limited circumstances, e.g. it does not work during field evaluation. Since this node is never part of field evaluation yet, that limitation seems ok, but it's something to work on at some point. Properly supporting that requires some more changes to propagate enough context information everywhere. Also showing errors of field evaluation on the field node itself (instead of on the evaluation node) requires even more work because our current logging system is not setup to support that yet. This node comes with a few new requirements for the socket items system: names must be valid identifiers and they are initialized in a non-trivial way. Overall, this was fairly straight forward to implement but currently it requires to adding a bunch of new members to all the accessors that don't really need it. This is something that we should simplify at some point even if I'm not entirely sure how yet. The same new requirements used in this node would probably also exist in a potential future expression node. Pull Request: https://projects.blender.org/blender/blender/pulls/138860
251 lines
8.6 KiB
C++
251 lines
8.6 KiB
C++
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#pragma once
|
|
|
|
#include "NOD_socket_items.hh"
|
|
|
|
#include "WM_api.hh"
|
|
|
|
#include "BKE_context.hh"
|
|
#include "BKE_library.hh"
|
|
#include "BKE_main_invariants.hh"
|
|
#include "BKE_node_tree_update.hh"
|
|
#include "BKE_node_tree_zones.hh"
|
|
|
|
#include "RNA_access.hh"
|
|
#include "RNA_define.hh"
|
|
#include "RNA_prototypes.hh"
|
|
|
|
#include "ED_node.hh"
|
|
|
|
#include "DNA_space_types.h"
|
|
|
|
namespace blender::nodes::socket_items::ops {
|
|
|
|
inline PointerRNA get_active_node_to_operate_on(bContext *C, const StringRef node_idname)
|
|
{
|
|
SpaceNode *snode = CTX_wm_space_node(C);
|
|
if (!snode) {
|
|
return PointerRNA_NULL;
|
|
}
|
|
if (!snode->edittree) {
|
|
return PointerRNA_NULL;
|
|
}
|
|
if (!ID_IS_EDITABLE(snode->edittree)) {
|
|
return PointerRNA_NULL;
|
|
}
|
|
const bke::bNodeTreeZones *zones = snode->edittree->zones();
|
|
if (!zones) {
|
|
return PointerRNA_NULL;
|
|
}
|
|
bNode *active_node = bke::node_get_active(*snode->edittree);
|
|
if (!active_node) {
|
|
return PointerRNA_NULL;
|
|
}
|
|
if (const bke::bNodeTreeZone *zone = zones->get_zone_by_node(active_node->identifier)) {
|
|
if (zone->input_node() == active_node) {
|
|
/* Assume the data is generally stored on the output and not the input node. */
|
|
active_node = const_cast<bNode *>(zone->output_node());
|
|
}
|
|
}
|
|
if (active_node->idname != node_idname) {
|
|
return PointerRNA_NULL;
|
|
}
|
|
return RNA_pointer_create_discrete(&snode->edittree->id, &RNA_Node, active_node);
|
|
}
|
|
|
|
inline void update_after_node_change(bContext *C, const PointerRNA node_ptr)
|
|
{
|
|
bNode *node = static_cast<bNode *>(node_ptr.data);
|
|
bNodeTree *ntree = reinterpret_cast<bNodeTree *>(node_ptr.owner_id);
|
|
|
|
BKE_ntree_update_tag_node_property(ntree, node);
|
|
BKE_main_ensure_invariants(*CTX_data_main(C), ntree->id);
|
|
WM_main_add_notifier(NC_NODE | NA_EDITED, ntree);
|
|
}
|
|
|
|
template<typename Accessor> inline bool editable_node_active_poll(bContext *C)
|
|
{
|
|
return get_active_node_to_operate_on(C, Accessor::node_idname).data != nullptr;
|
|
}
|
|
|
|
template<typename Accessor>
|
|
inline void remove_active_item(wmOperatorType *ot,
|
|
const char *name,
|
|
const char *idname,
|
|
const char *description)
|
|
{
|
|
ot->name = name;
|
|
ot->idname = idname;
|
|
ot->description = description;
|
|
ot->poll = editable_node_active_poll<Accessor>;
|
|
|
|
ot->exec = [](bContext *C, wmOperator * /*op*/) -> wmOperatorStatus {
|
|
PointerRNA node_ptr = get_active_node_to_operate_on(C, Accessor::node_idname);
|
|
bNode &node = *static_cast<bNode *>(node_ptr.data);
|
|
SocketItemsRef ref = Accessor::get_items_from_node(node);
|
|
if (*ref.items_num > 0) {
|
|
dna::array::remove_index(
|
|
ref.items, ref.items_num, ref.active_index, *ref.active_index, Accessor::destruct_item);
|
|
update_after_node_change(C, node_ptr);
|
|
}
|
|
return OPERATOR_FINISHED;
|
|
};
|
|
}
|
|
|
|
template<typename Accessor>
|
|
inline void remove_item_by_index(wmOperatorType *ot,
|
|
const char *name,
|
|
const char *idname,
|
|
const char *description)
|
|
{
|
|
ot->name = name;
|
|
ot->idname = idname;
|
|
ot->description = description;
|
|
ot->poll = editable_node_active_poll<Accessor>;
|
|
|
|
ot->exec = [](bContext *C, wmOperator *op) -> wmOperatorStatus {
|
|
PointerRNA node_ptr = get_active_node_to_operate_on(C, Accessor::node_idname);
|
|
bNode &node = *static_cast<bNode *>(node_ptr.data);
|
|
const int index_to_remove = RNA_int_get(op->ptr, "index");
|
|
SocketItemsRef ref = Accessor::get_items_from_node(node);
|
|
dna::array::remove_index(
|
|
ref.items, ref.items_num, ref.active_index, index_to_remove, Accessor::destruct_item);
|
|
|
|
update_after_node_change(C, node_ptr);
|
|
return OPERATOR_FINISHED;
|
|
};
|
|
|
|
RNA_def_int(ot->srna, "index", 0, 0, INT32_MAX, "Index", "Index to remove", 0, INT32_MAX);
|
|
}
|
|
|
|
template<typename Accessor>
|
|
inline void add_item(wmOperatorType *ot,
|
|
const char *name,
|
|
const char *idname,
|
|
const char *description)
|
|
{
|
|
ot->name = name;
|
|
ot->idname = idname;
|
|
ot->description = description;
|
|
ot->poll = editable_node_active_poll<Accessor>;
|
|
|
|
ot->exec = [](bContext *C, wmOperator * /*op*/) -> wmOperatorStatus {
|
|
PointerRNA node_ptr = get_active_node_to_operate_on(C, Accessor::node_idname);
|
|
bNode &node = *static_cast<bNode *>(node_ptr.data);
|
|
SocketItemsRef ref = Accessor::get_items_from_node(node);
|
|
const typename Accessor::ItemT *active_item = nullptr;
|
|
int dst_index = *ref.items_num;
|
|
if (ref.active_index) {
|
|
const int old_active_index = *ref.active_index;
|
|
if (old_active_index >= 0 && old_active_index < *ref.items_num) {
|
|
active_item = &(*ref.items)[old_active_index];
|
|
dst_index = active_item ? old_active_index + 1 : *ref.items_num;
|
|
}
|
|
}
|
|
|
|
if constexpr (Accessor::has_type && Accessor::has_name) {
|
|
std::string name = active_item ? active_item->name : "";
|
|
if constexpr (Accessor::has_custom_initial_name) {
|
|
name = Accessor::custom_initial_name(node, name);
|
|
}
|
|
socket_items::add_item_with_socket_type_and_name<Accessor>(
|
|
node,
|
|
active_item ?
|
|
Accessor::get_socket_type(*active_item) :
|
|
(Accessor::supports_socket_type(SOCK_GEOMETRY) ? SOCK_GEOMETRY : SOCK_FLOAT),
|
|
/* Empty name so it is based on the type. */
|
|
name.c_str());
|
|
}
|
|
else if constexpr (!Accessor::has_type && Accessor::has_name) {
|
|
socket_items::add_item_with_name<Accessor>(node, active_item ? active_item->name : "");
|
|
}
|
|
else if constexpr (!Accessor::has_type && !Accessor::has_name) {
|
|
socket_items::add_item<Accessor>(node);
|
|
}
|
|
else {
|
|
BLI_assert_unreachable();
|
|
}
|
|
|
|
dna::array::move_index(*ref.items, *ref.items_num, *ref.items_num - 1, dst_index);
|
|
if (ref.active_index) {
|
|
*ref.active_index = dst_index;
|
|
}
|
|
|
|
update_after_node_change(C, node_ptr);
|
|
return OPERATOR_FINISHED;
|
|
};
|
|
}
|
|
|
|
enum class MoveDirection {
|
|
Up = 0,
|
|
Down = 1,
|
|
};
|
|
|
|
template<typename Accessor>
|
|
inline void move_active_item(wmOperatorType *ot,
|
|
const char *name,
|
|
const char *idname,
|
|
const char *description)
|
|
{
|
|
ot->name = name;
|
|
ot->idname = idname;
|
|
ot->description = description;
|
|
ot->poll = editable_node_active_poll<Accessor>;
|
|
|
|
ot->exec = [](bContext *C, wmOperator *op) -> wmOperatorStatus {
|
|
PointerRNA node_ptr = get_active_node_to_operate_on(C, Accessor::node_idname);
|
|
bNode &node = *static_cast<bNode *>(node_ptr.data);
|
|
const MoveDirection direction = MoveDirection(RNA_enum_get(op->ptr, "direction"));
|
|
|
|
SocketItemsRef ref = Accessor::get_items_from_node(node);
|
|
const int old_active_index = *ref.active_index;
|
|
if (direction == MoveDirection::Up && old_active_index > 0) {
|
|
dna::array::move_index(*ref.items, *ref.items_num, old_active_index, old_active_index - 1);
|
|
*ref.active_index -= 1;
|
|
}
|
|
else if (direction == MoveDirection::Down && old_active_index < *ref.items_num - 1) {
|
|
dna::array::move_index(*ref.items, *ref.items_num, old_active_index, old_active_index + 1);
|
|
*ref.active_index += 1;
|
|
}
|
|
|
|
update_after_node_change(C, node_ptr);
|
|
return OPERATOR_FINISHED;
|
|
};
|
|
|
|
static const EnumPropertyItem direction_items[] = {
|
|
{int(MoveDirection::Up), "UP", 0, "Up", ""},
|
|
{int(MoveDirection::Down), "DOWN", 0, "Down", ""},
|
|
{0, nullptr, 0, nullptr, nullptr},
|
|
};
|
|
|
|
RNA_def_enum(ot->srna, "direction", direction_items, 0, "Direction", "Move direction");
|
|
}
|
|
|
|
/**
|
|
* Creates simple operators for adding, removing and moving items.
|
|
* The idnames are passed in explicitly, so that they are more searchable compared to when they
|
|
* would be computed automatically.
|
|
*/
|
|
template<typename Accessor> inline void make_common_operators()
|
|
{
|
|
WM_operatortype_append([](wmOperatorType *ot) {
|
|
socket_items::ops::add_item<Accessor>(ot,
|
|
"Add Item",
|
|
Accessor::operator_idnames::add_item.c_str(),
|
|
"Add item below active item");
|
|
});
|
|
WM_operatortype_append([](wmOperatorType *ot) {
|
|
socket_items::ops::remove_active_item<Accessor>(
|
|
ot, "Remove Item", Accessor::operator_idnames::remove_item.c_str(), "Remove active item");
|
|
});
|
|
WM_operatortype_append([](wmOperatorType *ot) {
|
|
socket_items::ops::move_active_item<Accessor>(
|
|
ot, "Move Item", Accessor::operator_idnames::move_item.c_str(), "Move active item");
|
|
});
|
|
}
|
|
|
|
} // namespace blender::nodes::socket_items::ops
|