WIP: Brush assets project #106303

Draft
Julian Eisel wants to merge 358 commits from brush-assets-project into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
15 changed files with 408 additions and 76 deletions
Showing only changes of commit f164e6ceed - Show all commits

View File

@ -6173,6 +6173,7 @@ def km_edit_curves(params):
{"properties": [("mode", 'CURVE_SHRINKFATTEN')]}),
("curves.cyclic_toggle", {"type": 'C', "value": 'PRESS', "alt": True}, None),
("curves.handle_type_set", {"type": 'V', "value": 'PRESS'}, None),
op_menu("VIEW3D_MT_edit_curves_add", {"type": 'A', "value": 'PRESS', "shift": True}),
])
return keymap

View File

@ -1204,6 +1204,8 @@ class VIEW3D_MT_editor_menus(Menu):
layout.menu("VIEW3D_MT_mesh_add", text="Add", text_ctxt=i18n_contexts.operator_default)
elif mode_string == 'EDIT_CURVE':
layout.menu("VIEW3D_MT_curve_add", text="Add", text_ctxt=i18n_contexts.operator_default)
elif mode_string == "EDIT_CURVES":
layout.menu("VIEW3D_MT_edit_curves_add", text="Add", text_ctxt=i18n_contexts.operator_default)
elif mode_string == 'EDIT_SURFACE':
layout.menu("VIEW3D_MT_surface_add", text="Add", text_ctxt=i18n_contexts.operator_default)
elif mode_string == 'EDIT_METABALL':
@ -5945,6 +5947,16 @@ class VIEW3D_MT_edit_greasepencil_point(Menu):
layout.operator("grease_pencil.stroke_smooth", text="Smooth")
class VIEW3D_MT_edit_curves_add(Menu):
bl_label = "Add"
def draw(self, _context):
layout = self.layout
layout.operator("curves.add_bezier", text="Bezier", icon='CURVE_BEZCURVE')
layout.operator("curves.add_circle", text="Circle", icon='CURVE_BEZCIRCLE')
class VIEW3D_MT_edit_curves(Menu):
bl_label = "Curves"
@ -9182,6 +9194,7 @@ classes = (
VIEW3D_MT_edit_armature_delete,
VIEW3D_MT_edit_gpencil_transform,
VIEW3D_MT_edit_curves,
VIEW3D_MT_edit_curves_add,
VIEW3D_MT_edit_curves_segments,
VIEW3D_MT_edit_curves_control_points,
VIEW3D_MT_edit_pointcloud,

View File

@ -603,7 +603,10 @@ void ANIM_armature_bonecoll_name_set(bArmature *armature, BoneCollection *bcoll,
bonecoll_ensure_name_unique(armature, bcoll);
/* Bone collections can be reached via .collections (4.0+) and .collections_all (4.1+).
* Animation data from 4.0 should have been versioned to only use `.collections_all`. */
BKE_animdata_fix_paths_rename_all(&armature->id, "collections", old_name, bcoll->name);
BKE_animdata_fix_paths_rename_all(&armature->id, "collections_all", old_name, bcoll->name);
}
void ANIM_armature_bonecoll_remove_from_index(bArmature *armature, int index)

View File

@ -892,8 +892,12 @@ static bool nlastrips_path_rename_fix(ID *owner_id,
LISTBASE_FOREACH (NlaStrip *, strip, strips) {
/* fix strip's action */
if (strip->act != nullptr) {
is_changed |= fcurves_path_rename_fix(
const bool is_changed_action = fcurves_path_rename_fix(
owner_id, prefix, oldName, newName, oldKey, newKey, &strip->act->curves, verify_paths);
if (is_changed_action) {
DEG_id_tag_update(&strip->act->id, ID_RECALC_ANIMATION);
}
is_changed |= is_changed_action;
}
/* Ignore own F-Curves, since those are local. */
/* Check sub-strips (if meta-strips). */

View File

@ -248,6 +248,23 @@ static void version_bonegroups_to_bonecollections(Main *bmain)
}
}
/**
* Change animation/drivers from "collections[..." to "collections_all[..." so
* they remain stable when the bone collection hierarchy structure changes.
*/
static void version_bonecollection_anim(FCurve *fcurve)
{
const blender::StringRef rna_path(fcurve->rna_path);
constexpr char const *rna_path_prefix = "collections[";
if (!rna_path.startswith(rna_path_prefix)) {
return;
}
const std::string path_remainder(rna_path.drop_known_prefix(rna_path_prefix));
MEM_freeN(fcurve->rna_path);
fcurve->rna_path = BLI_sprintfN("collections_all[%s", path_remainder.c_str());
}
static void version_principled_bsdf_update_animdata(ID *owner_id, bNodeTree *ntree)
{
ID *id = &ntree->id;
@ -498,6 +515,27 @@ void do_versions_after_linking_400(FileData *fd, Main *bmain)
version_nla_tweakmode_incomplete(bmain);
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 402, 15)) {
/* Change drivers and animation on "armature.collections" to
* ".collections_all", so that they are drawn correctly in the tree view,
* and keep working when the collection is moved around in the hierarchy. */
LISTBASE_FOREACH (bArmature *, arm, &bmain->armatures) {
AnimData *adt = BKE_animdata_from_id(&arm->id);
if (!adt) {
continue;
}
LISTBASE_FOREACH (FCurve *, fcurve, &adt->drivers) {
version_bonecollection_anim(fcurve);
}
if (adt->action) {
LISTBASE_FOREACH (FCurve *, fcurve, &adt->action->curves) {
version_bonecollection_anim(fcurve);
}
}
}
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

View File

@ -968,7 +968,13 @@ void DepsgraphNodeBuilder::build_object_modifiers(Object *object)
BuilderWalkUserData data;
data.builder = this;
/* Temporarily set the collection visibility to false, relying on the visibility flushing code
* to flush the visibility from a modifier into collections it depends on. */
const bool is_current_parent_collection_visible = is_parent_collection_visible_;
is_parent_collection_visible_ = false;
BKE_modifiers_foreach_ID_link(object, modifier_walk, &data);
is_parent_collection_visible_ = is_current_parent_collection_visible;
}
void DepsgraphNodeBuilder::build_object_data(Object *object)

View File

@ -63,9 +63,11 @@
#include "UI_interface.hh"
#include "UI_resources.hh"
#include "GEO_join_geometries.hh"
#include "GEO_reverse_uv_sampler.hh"
#include "GEO_set_curve_type.hh"
#include "GEO_subdivide_curves.hh"
#include "GEO_transform.hh"
/**
* The code below uses a suffix naming convention to indicate the coordinate space:
@ -1527,6 +1529,169 @@ static void CURVES_OT_subdivide(wmOperatorType *ot)
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** Add new curves primitive to an existing curves object in edit mode. */
static void append_primitive_curve(bContext *C,
Curves &curves_id,
CurvesGeometry new_curves,
wmOperator &op)
{
const int new_points_num = new_curves.points_num();
const int new_curves_num = new_curves.curves_num();
/* Create geometry sets so that generic join code can be used. */
bke::GeometrySet old_geometry = bke::GeometrySet::from_curves(
&curves_id, bke::GeometryOwnershipType::ReadOnly);
bke::GeometrySet new_geometry = bke::GeometrySet::from_curves(
bke::curves_new_nomain(std::move(new_curves)));
/* Transform primitive according to settings. */
float3 location;
float3 rotation;
float3 scale;
object::add_generic_get_opts(C, &op, 'Z', location, rotation, scale, nullptr, nullptr, nullptr);
const float4x4 transform = math::from_loc_rot_scale<float4x4>(
location, math::EulerXYZ(rotation), scale);
geometry::transform_geometry(new_geometry, transform);
bke::GeometrySet joined_geometry = geometry::join_geometries({old_geometry, new_geometry}, {});
Curves *joined_curves_id = joined_geometry.get_curves_for_write();
CurvesGeometry &dst_curves = curves_id.geometry.wrap();
dst_curves = std::move(joined_curves_id->geometry.wrap());
/* Only select the new curves. */
const bke::AttrDomain selection_domain = bke::AttrDomain(curves_id.selection_domain);
const int new_element_num = selection_domain == bke::AttrDomain::Point ? new_points_num :
new_curves_num;
foreach_selection_attribute_writer(
dst_curves, selection_domain, [&](bke::GSpanAttributeWriter &selection) {
fill_selection_false(selection.span.drop_back(new_element_num));
fill_selection_true(selection.span.take_back(new_element_num));
});
dst_curves.tag_topology_changed();
}
namespace add_circle {
static CurvesGeometry generate_circle_primitive(const float radius)
{
CurvesGeometry curves{4, 1};
MutableSpan<int> offsets = curves.offsets_for_write();
offsets[0] = 0;
offsets[1] = 4;
curves.fill_curve_types(CURVE_TYPE_BEZIER);
curves.cyclic_for_write().fill(true);
curves.handle_types_left_for_write().fill(BEZIER_HANDLE_AUTO);
curves.handle_types_right_for_write().fill(BEZIER_HANDLE_AUTO);
curves.resolution_for_write().fill(12);
MutableSpan<float3> positions = curves.positions_for_write();
positions[0] = float3(-radius, 0, 0);
positions[1] = float3(0, radius, 0);
positions[2] = float3(radius, 0, 0);
positions[3] = float3(0, -radius, 0);
/* Ensure these attributes exist. */
curves.handle_positions_left_for_write();
curves.handle_positions_right_for_write();
curves.calculate_bezier_auto_handles();
return curves;
}
static int exec(bContext *C, wmOperator *op)
{
Object *object = CTX_data_edit_object(C);
Curves *active_curves_id = static_cast<Curves *>(object->data);
const float radius = RNA_float_get(op->ptr, "radius");
append_primitive_curve(C, *active_curves_id, generate_circle_primitive(radius), *op);
DEG_id_tag_update(&active_curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, active_curves_id);
return OPERATOR_FINISHED;
}
} // namespace add_circle
static void CURVES_OT_add_circle(wmOperatorType *ot)
{
ot->name = "Add Circle";
ot->idname = __func__;
ot->description = "Add new circle curve";
ot->exec = add_circle::exec;
ot->poll = editable_curves_in_edit_mode_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
object::add_unit_props_radius(ot);
object::add_generic_props(ot, true);
}
namespace add_bezier {
static CurvesGeometry generate_bezier_primitive(const float radius)
{
CurvesGeometry curves{2, 1};
MutableSpan<int> offsets = curves.offsets_for_write();
offsets[0] = 0;
offsets[1] = 2;
curves.fill_curve_types(CURVE_TYPE_BEZIER);
curves.handle_types_left_for_write().fill(BEZIER_HANDLE_ALIGN);
curves.handle_types_right_for_write().fill(BEZIER_HANDLE_ALIGN);
curves.resolution_for_write().fill(12);
MutableSpan<float3> positions = curves.positions_for_write();
MutableSpan<float3> left_handles = curves.handle_positions_left_for_write();
MutableSpan<float3> right_handles = curves.handle_positions_right_for_write();
left_handles[0] = float3(-1.5f, -0.5, 0) * radius;
positions[0] = float3(-1.0f, 0, 0) * radius;
right_handles[0] = float3(-0.5f, 0.5f, 0) * radius;
left_handles[1] = float3(0, 0, 0) * radius;
positions[1] = float3(1.0f, 0, 0) * radius;
right_handles[1] = float3(2.0f, 0, 0) * radius;
return curves;
}
static int exec(bContext *C, wmOperator *op)
{
Object *object = CTX_data_edit_object(C);
Curves *active_curves_id = static_cast<Curves *>(object->data);
const float radius = RNA_float_get(op->ptr, "radius");
append_primitive_curve(C, *active_curves_id, generate_bezier_primitive(radius), *op);
DEG_id_tag_update(&active_curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, active_curves_id);
return OPERATOR_FINISHED;
}
} // namespace add_bezier
static void CURVES_OT_add_bezier(wmOperatorType *ot)
{
ot->name = "Add Bezier";
ot->idname = __func__;
ot->description = "Add new bezier curve";
ot->exec = add_bezier::exec;
ot->poll = editable_curves_in_edit_mode_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
object::add_unit_props_radius(ot);
object::add_generic_props(ot, true);
}
namespace set_handle_type {
static int exec(bContext *C, wmOperator *op)
@ -1608,6 +1773,8 @@ void operatortypes_curves()
WM_operatortype_append(CURVES_OT_curve_type_set);
WM_operatortype_append(CURVES_OT_switch_direction);
WM_operatortype_append(CURVES_OT_subdivide);
WM_operatortype_append(CURVES_OT_add_circle);
WM_operatortype_append(CURVES_OT_add_bezier);
WM_operatortype_append(CURVES_OT_handle_type_set);
}

View File

@ -1378,8 +1378,8 @@ static void create_inspection_string_for_field_info(const bNodeSocket &socket,
ss << "\n";
for (const int i : input_tooltips.index_range()) {
const blender::StringRef tooltip = input_tooltips[i];
ss << fmt::format(TIP_("\u2022 {}"), TIP_(tooltip.data()));
const blender::StringRefNull tooltip = input_tooltips[i];
ss << fmt::format(TIP_("\u2022 {}"), TIP_(tooltip.c_str()));
if (i < input_tooltips.size() - 1) {
ss << ".\n";
}
@ -1461,15 +1461,16 @@ static void create_inspection_string_for_geometry_info(const geo_log::GeometryIn
break;
}
case bke::GeometryComponent::Type::GreasePencil: {
const geo_log::GeometryInfoLog::GreasePencilInfo &grease_pencil_info =
*value_log.grease_pencil_info;
char line[256];
SNPRINTF(line,
TIP_("\u2022 Grease Pencil: %s layers"),
to_string(grease_pencil_info.layers_num).c_str());
ss << line;
break;
break;
if (U.experimental.use_grease_pencil_version3) {
const geo_log::GeometryInfoLog::GreasePencilInfo &grease_pencil_info =
*value_log.grease_pencil_info;
char line[256];
SNPRINTF(line,
TIP_("\u2022 Grease Pencil: %s layers"),
to_string(grease_pencil_info.layers_num).c_str());
ss << line;
break;
}
}
}
if (type != component_types.last()) {
@ -1592,9 +1593,9 @@ static std::string node_socket_get_tooltip(const SpaceNode *snode,
std::stringstream output;
if (socket.runtime->declaration != nullptr) {
const blender::nodes::SocketDeclaration &socket_decl = *socket.runtime->declaration;
blender::StringRef description = socket_decl.description;
blender::StringRefNull description = socket_decl.description;
if (!description.is_empty()) {
output << TIP_(description.data());
output << TIP_(description.c_str());
}
}

View File

@ -74,6 +74,62 @@ enum class Type {
SHORT4
};
BLI_INLINE int to_component_count(const Type &type)
{
switch (type) {
case Type::FLOAT:
case Type::UINT:
case Type::INT:
case Type::BOOL:
return 1;
case Type::VEC2:
case Type::UVEC2:
case Type::IVEC2:
return 2;
case Type::VEC3:
case Type::UVEC3:
case Type::IVEC3:
return 3;
case Type::VEC4:
case Type::UVEC4:
case Type::IVEC4:
return 4;
case Type::MAT3:
return 9;
case Type::MAT4:
return 16;
/* Alias special types. */
case Type::UCHAR:
case Type::USHORT:
return 1;
case Type::UCHAR2:
case Type::USHORT2:
return 2;
case Type::UCHAR3:
case Type::USHORT3:
return 3;
case Type::UCHAR4:
case Type::USHORT4:
return 4;
case Type::CHAR:
case Type::SHORT:
return 1;
case Type::CHAR2:
case Type::SHORT2:
return 2;
case Type::CHAR3:
case Type::SHORT3:
return 3;
case Type::CHAR4:
case Type::SHORT4:
return 4;
case Type::VEC3_101010I2:
return 3;
}
BLI_assert_unreachable();
return -1;
}
/* All of these functions is a bit out of place */
static inline Type to_type(const eGPUType type)
{

View File

@ -155,7 +155,7 @@ class MTLFrameBuffer : public FrameBuffer {
protected:
void subpass_transition_impl(const GPUAttachmentState /*depth_attachment_state*/,
Span<GPUAttachmentState> /*color_attachment_states*/) override{};
Span<GPUAttachmentState> color_attachment_states) override;
public:
void apply_state();

View File

@ -472,6 +472,27 @@ void MTLFrameBuffer::clear_attachment(GPUAttachmentType type,
this->force_clear();
}
}
void MTLFrameBuffer::subpass_transition_impl(const GPUAttachmentState /*depth_attachment_state*/,
Span<GPUAttachmentState> color_attachment_states)
{
const bool is_tile_based_arch = (GPU_platform_architecture() == GPU_ARCHITECTURE_TBDR);
if (!is_tile_based_arch) {
/* Break renderpass if tile memory is unsupported to ensure current framebuffer results are
* stored. */
context_->main_command_buffer.end_active_command_encoder();
/* Bind framebuffer attachments as textures.
* NOTE: Follows behaviour of gl_framebuffer. However, shaders utilising subpass_in will
* need to avoid bindpoint collisions for image/texture resources. */
for (int i : color_attachment_states.index_range()) {
GPUAttachmentType type = GPU_FB_COLOR_ATTACHMENT0 + i;
GPUTexture *attach_tex = this->attachments_[type].tex;
if (color_attachment_states[i] == GPU_ATTACHEMENT_READ) {
GPU_texture_image_bind(attach_tex, i);
}
}
}
}
void MTLFrameBuffer::read(eGPUFrameBufferBits planes,
eGPUDataFormat format,

View File

@ -414,6 +414,7 @@ class MSLGeneratorInterface {
blender::Vector<MSLConstant> constants;
/* Fragment tile inputs. */
blender::Vector<MSLFragmentTileInputAttribute> fragment_tile_inputs;
bool supports_native_tile_inputs;
/* Should match vertex outputs, but defined separately as
* some shader permutations will not utilize all inputs/outputs.
* Final shader uses the intersection between the two sets. */

View File

@ -2089,6 +2089,16 @@ void MSLGeneratorInterface::prepare_from_createinfo(const shader::ShaderCreateIn
fragment_outputs.append(mtl_frag_out);
}
/** Identify support for tile inputs. */
const bool is_tile_based_arch = (GPU_platform_architecture() == GPU_ARCHITECTURE_TBDR);
if (is_tile_based_arch) {
supports_native_tile_inputs = true;
}
else {
/* NOTE: If emulating tile input reads, we must ensure we also expose position data. */
supports_native_tile_inputs = false;
}
/* Fragment tile inputs. */
for (const shader::ShaderCreateInfo::SubpassIn &frag_tile_in : create_info_->subpass_inputs_) {
@ -2107,6 +2117,51 @@ void MSLGeneratorInterface::prepare_from_createinfo(const shader::ShaderCreateIn
mtl_frag_in.raster_order_group = frag_tile_in.raster_order_group;
fragment_tile_inputs.append(mtl_frag_in);
/* If we do not support native tile inputs, generate an image-binding per input. */
if (!supports_native_tile_inputs) {
/* Determine type: */
bool is_layered_fb = bool(create_info_->builtins_ & BuiltinBits::LAYER);
/* Start with invalid value to detect failure cases. */
ImageType image_type = ImageType::FLOAT_BUFFER;
switch (frag_tile_in.type) {
case Type::FLOAT:
image_type = is_layered_fb ? ImageType::FLOAT_2D_ARRAY : ImageType::FLOAT_2D;
break;
case Type::INT:
image_type = is_layered_fb ? ImageType::INT_2D_ARRAY : ImageType::INT_2D;
break;
case Type::UINT:
image_type = is_layered_fb ? ImageType::UINT_2D_ARRAY : ImageType::UINT_2D;
break;
default:
break;
}
BLI_assert(image_type != ImageType::FLOAT_BUFFER);
/* Generate texture binding resource. */
MSLTextureResource msl_image;
msl_image.stage = ShaderStage::FRAGMENT;
msl_image.type = image_type;
msl_image.name = frag_tile_in.name + "_subpass_img";
msl_image.access = MSLTextureSamplerAccess::TEXTURE_ACCESS_READ;
msl_image.slot = texture_slot_id++;
/* WATCH: We don't have a great place to generate the image bindings.
* So we will use the subpass binding index and check if it collides with an existing
* binding. */
msl_image.location = frag_tile_in.index;
msl_image.is_texture_sampler = false;
BLI_assert(msl_image.slot < MTL_MAX_TEXTURE_SLOTS);
BLI_assert(msl_image.location < MTL_MAX_TEXTURE_SLOTS);
/* Check existing samplers. */
for (const auto &tex : texture_samplers) {
BLI_assert(tex.location != msl_image.location);
}
texture_samplers.append(msl_image);
max_tex_bind_index = max_ii(max_tex_bind_index, msl_image.slot);
}
}
/* Transform feedback. */
@ -3043,10 +3098,31 @@ std::string MSLGeneratorInterface::generate_msl_global_uniform_population(Shader
std::string MSLGeneratorInterface::generate_msl_fragment_tile_input_population()
{
std::stringstream out;
for (const MSLFragmentTileInputAttribute &tile_input : this->fragment_tile_inputs) {
out << "\t" << get_shader_stage_instance_name(ShaderStage::FRAGMENT) << "." << tile_input.name
<< " = "
<< "fragment_tile_in." << tile_input.name << ";" << std::endl;
/* Native tile read is supported on tile-based architectures (Apple Silicon). */
if (supports_native_tile_inputs) {
for (const MSLFragmentTileInputAttribute &tile_input : this->fragment_tile_inputs) {
out << "\t" << get_shader_stage_instance_name(ShaderStage::FRAGMENT) << "."
<< tile_input.name << " = "
<< "fragment_tile_in." << tile_input.name << ";" << std::endl;
}
}
else {
for (const MSLFragmentTileInputAttribute &tile_input : this->fragment_tile_inputs) {
/* Get read swizzle mask. */
char swizzle[] = "xyzw";
swizzle[to_component_count(tile_input.type)] = '\0';
bool is_layered_fb = bool(create_info_->builtins_ & BuiltinBits::LAYER);
std::string texel_co = (is_layered_fb) ?
"ivec3(ivec2(v_in._default_position_.xy), int(v_in.gpu_Layer))" :
"ivec2(v_in._default_position_.xy)";
out << "\t" << get_shader_stage_instance_name(ShaderStage::FRAGMENT) << "."
<< tile_input.name << " = texelFetch("
<< get_shader_stage_instance_name(ShaderStage::FRAGMENT) << "." << tile_input.name
<< "_subpass_img, " << texel_co << ", 0)." << swizzle << ";\n";
}
}
return out.str();
}

View File

@ -144,62 +144,6 @@ static const char *to_string(const Type &type)
return "unknown";
}
static int to_component_count(const Type &type)
{
switch (type) {
case Type::FLOAT:
case Type::UINT:
case Type::INT:
case Type::BOOL:
return 1;
case Type::VEC2:
case Type::UVEC2:
case Type::IVEC2:
return 2;
case Type::VEC3:
case Type::UVEC3:
case Type::IVEC3:
return 3;
case Type::VEC4:
case Type::UVEC4:
case Type::IVEC4:
return 4;
case Type::MAT3:
return 9;
case Type::MAT4:
return 16;
/* Alias special types. */
case Type::UCHAR:
case Type::USHORT:
return 1;
case Type::UCHAR2:
case Type::USHORT2:
return 2;
case Type::UCHAR3:
case Type::USHORT3:
return 3;
case Type::UCHAR4:
case Type::USHORT4:
return 4;
case Type::CHAR:
case Type::SHORT:
return 1;
case Type::CHAR2:
case Type::SHORT2:
return 2;
case Type::CHAR3:
case Type::SHORT3:
return 3;
case Type::CHAR4:
case Type::SHORT4:
return 4;
case Type::VEC3_101010I2:
return 3;
}
BLI_assert_unreachable();
return -1;
}
static Type to_component_type(const Type &type)
{
switch (type) {

View File

@ -470,7 +470,8 @@ static void interpolate_curve_attributes(bke::CurvesGeometry &child_curves,
if (type == CD_PROP_STRING) {
return true;
}
if (guide_curve_attributes.is_builtin(id) && !ELEM(id.name(), "radius", "tilt", "resolution"))
if (guide_curve_attributes.is_builtin(id) &&
!ELEM(id.name(), "radius", "tilt", "resolution", "cyclic"))
{
return true;
}