Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
../include
../../makesrna
# RNA_prototypes.hh
${CMAKE_BINARY_DIR}/source/blender/makesrna
)
set(INC_SYS
)
set(SRC
intern/curves_add.cc
intern/curves_attribute_set.cc
intern/curves_data.cc
intern/curves_draw.cc
intern/curves_edit.cc
intern/curves_extrude.cc
intern/curves_masks.cc
intern/curves_ops.cc
intern/curves_pen.cc
intern/curves_selection.cc
intern/curves_undo.cc
intern/join.cc
intern/select_linked_pick.cc
intern/separate.cc
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blentranslation
PRIVATE bf::bmesh
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::extern::curve_fit_nd
PRIVATE bf::functions
PRIVATE bf::geometry
PRIVATE bf::gpu
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
PRIVATE bf::nodes
PRIVATE bf::windowmanager
)
blender_add_lib(bf_editor_curves "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
add_dependencies(bf_editor_curves bf_rna)
if(WITH_GTESTS)
set(TEST_SRC
tests/curves_edit_test.cc
)
set(TEST_INC
)
set(TEST_LIB
)
blender_add_test_suite_lib(editor_curves "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@@ -0,0 +1,157 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edcurves
*/
#include "BLI_listbase.h"
#include "BLI_math_base_safe.h"
#include "BLI_rand.hh"
#include "BKE_attribute.hh"
#include "BKE_context.hh"
#include "BKE_curves.hh"
#include "BKE_main_invariants.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_node_runtime.hh"
#include "BLT_translation.hh"
#include "ED_curves.hh"
#include "ED_node.hh"
#include "ED_object.hh"
#include "DNA_modifier_types.h"
#include "DNA_node_types.h"
#include "DNA_object_types.h"
namespace blender::ed::curves {
static bool has_surface_deformation_node(const bNodeTree &ntree)
{
if (!ntree.nodes_by_type("GeometryNodeDeformCurvesOnSurface"_ustr).is_empty()) {
return true;
}
for (const bNode *node : ntree.group_nodes()) {
if (const bNodeTree *sub_tree = reinterpret_cast<const bNodeTree *>(node->id)) {
if (has_surface_deformation_node(*sub_tree)) {
return true;
}
}
}
return false;
}
static bool has_surface_deformation_node(const Object &curves_ob)
{
for (const ModifierData &md : curves_ob.modifiers) {
if (md.type != eModifierType_Nodes) {
continue;
}
const NodesModifierData *nmd = reinterpret_cast<const NodesModifierData *>(&md);
if (nmd->node_group == nullptr || ID_MISSING(nmd->node_group)) {
continue;
}
if (has_surface_deformation_node(*nmd->node_group)) {
return true;
}
}
return false;
}
void ensure_surface_deformation_node_exists(bContext &C, Object &curves_ob)
{
if (has_surface_deformation_node(curves_ob)) {
return;
}
Main *bmain = CTX_data_main(&C);
Scene *scene = CTX_data_scene(&C);
ModifierData *md = object::modifier_add(
nullptr, bmain, scene, &curves_ob, DATA_("Surface Deform"), eModifierType_Nodes);
NodesModifierData &nmd = *reinterpret_cast<NodesModifierData *>(md);
nmd.node_group = bke::node_tree_add_tree(bmain, DATA_("Surface Deform"), "GeometryNodeTree");
if (!nmd.node_group->geometry_node_asset_traits) {
nmd.node_group->geometry_node_asset_traits = MEM_new<GeometryNodeAssetTraits>(__func__);
}
nmd.node_group->geometry_node_asset_traits->flag |= GEO_NODE_ASSET_MODIFIER;
bNodeTree *ntree = nmd.node_group;
ntree->tree_interface.add_socket(
"Geometry", "", "NodeSocketGeometry", NODE_INTERFACE_SOCKET_OUTPUT, nullptr);
ntree->tree_interface.add_socket(
"Geometry", "", "NodeSocketGeometry", NODE_INTERFACE_SOCKET_INPUT, nullptr);
bNode *group_input = bke::node_add_static_node(&C, *ntree, NODE_GROUP_INPUT);
bNode *group_output = bke::node_add_static_node(&C, *ntree, NODE_GROUP_OUTPUT);
bNode *deform_node = bke::node_add_static_node(&C, *ntree, GEO_NODE_DEFORM_CURVES_ON_SURFACE);
BKE_main_ensure_invariants(*bmain, nmd.node_group->id);
bke::node_add_link(*ntree,
*group_input,
*static_cast<bNodeSocket *>(group_input->outputs.first),
*deform_node,
*bke::node_find_socket(*deform_node, SOCK_IN, "Curves"_ustr));
bke::node_add_link(*ntree,
*deform_node,
*bke::node_find_socket(*deform_node, SOCK_OUT, "Curves"_ustr),
*group_output,
*static_cast<bNodeSocket *>(group_output->inputs.first));
group_input->location[0] = -200;
group_output->location[0] = 200;
deform_node->location[0] = 0;
BKE_main_ensure_invariants(*bmain, nmd.node_group->id);
}
bke::CurvesGeometry primitive_random_sphere(const int curves_size, const int points_per_curve)
{
bke::CurvesGeometry curves(points_per_curve * curves_size, curves_size);
MutableSpan<int> offsets = curves.offsets_for_write();
MutableSpan<float3> positions = curves.positions_for_write();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
bke::SpanAttributeWriter<float> radius = attributes.lookup_or_add_for_write_only_span<float>(
"radius", bke::AttrDomain::Point);
for (const int i : offsets.index_range()) {
offsets[i] = points_per_curve * i;
}
RandomNumberGenerator rng;
const OffsetIndices points_by_curve = curves.points_by_curve();
for (const int i : curves.curves_range()) {
const IndexRange points = points_by_curve[i];
MutableSpan<float3> curve_positions = positions.slice(points);
MutableSpan<float> curve_radii = radius.span.slice(points);
const float theta = 2.0f * M_PI * rng.get_float();
const float phi = safe_acosf(2.0f * rng.get_float() - 1.0f);
float3 no = {std::sin(theta) * std::sin(phi), std::cos(theta) * std::sin(phi), std::cos(phi)};
no = math::normalize(no);
float3 co = no;
for (int key = 0; key < points_per_curve; key++) {
float t = key / float(points_per_curve - 1);
curve_positions[key] = co;
curve_radii[key] = 0.02f * (1.0f - t);
float3 offset = float3(rng.get_float(), rng.get_float(), rng.get_float()) * 2.0f - 1.0f;
co += (offset + no) / points_per_curve;
}
}
radius.finish();
return curves;
}
} // namespace blender::ed::curves

View File

@@ -0,0 +1,233 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edmesh
*/
#include "BLI_generic_pointer.hh"
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_attribute_math.hh"
#include "BKE_context.hh"
#include "BKE_type_conversions.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "ED_curves.hh"
#include "ED_geometry.hh"
#include "ED_object.hh"
#include "ED_screen.hh"
#include "ED_transform.hh"
#include "ED_view3d.hh"
#include "RNA_access.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "DNA_object_types.h"
#include "DEG_depsgraph.hh"
/* -------------------------------------------------------------------- */
/** \name Delete Operator
* \{ */
namespace blender::ed::curves {
static bool active_attribute_poll(bContext *C)
{
if (!editable_curves_in_edit_mode_poll(C)) {
return false;
}
const Object *object = CTX_data_active_object(C);
const ID &object_data = *static_cast<const ID *>(object->data);
if (!geometry::attribute_set_poll(*C, object_data)) {
return false;
}
return true;
}
static IndexMask retrieve_selected_elements(const Curves &curves_id,
const bke::AttrDomain domain,
IndexMaskMemory &memory)
{
switch (domain) {
case bke::AttrDomain::Point:
return retrieve_selected_points(curves_id, memory);
case bke::AttrDomain::Curve:
return retrieve_selected_curves(curves_id, memory);
default:
BLI_assert_unreachable();
return {};
}
}
static void validate_value(const bke::AttributeAccessor attributes,
const StringRef name,
const CPPType &type,
void *buffer)
{
const bke::AttributeValidator validator = attributes.lookup_validator(name);
if (!validator) {
return;
}
BUFFER_FOR_CPP_TYPE_VALUE(type, validated_buffer);
BLI_SCOPED_DEFER([&]() { type.destruct(validated_buffer); });
const IndexMask single_mask(1);
mf::ParamsBuilder params(*validator.function, &single_mask);
params.add_readonly_single_input(GPointer(type, buffer));
params.add_uninitialized_single_output({type, validated_buffer, 1});
mf::ContextBuilder context;
validator.function->call(single_mask, params, context);
type.copy_assign(validated_buffer, buffer);
}
static wmOperatorStatus set_attribute_exec(bContext *C, wmOperator *op)
{
Object *active_object = CTX_data_active_object(C);
Curves &active_curves_id = *id_cast<Curves *>(active_object->data);
AttributeOwner active_owner = AttributeOwner::from_id(&active_curves_id.id);
const StringRef name = *BKE_attributes_active_name_get(active_owner);
const bke::AttributeMetaData active_meta_data =
*active_curves_id.geometry.wrap().attributes().lookup_meta_data(name);
const bke::AttrType active_type = active_meta_data.data_type;
const CPPType &type = bke::attribute_type_to_cpp_type(active_type);
BUFFER_FOR_CPP_TYPE_VALUE(type, buffer);
BLI_SCOPED_DEFER([&]() { type.destruct(buffer); });
const GPointer value = geometry::rna_property_for_attribute_type_retrieve_value(
*op->ptr, active_type, buffer);
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
for (Curves *curves_id : get_unique_editable_curves(*C)) {
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
const std::optional<bke::AttributeMetaData> meta_data = attributes.lookup_meta_data(name);
if (!meta_data) {
continue;
}
IndexMaskMemory memory;
const IndexMask selection = retrieve_selected_elements(*curves_id, meta_data->domain, memory);
if (selection.is_empty()) {
continue;
}
/* Use implicit conversions to try to handle the case where the active attribute has a
* different type on multiple objects. */
const CPPType &dst_type = bke::attribute_type_to_cpp_type(meta_data->data_type);
if (&type != &dst_type && !conversions.is_convertible(type, dst_type)) {
continue;
}
BUFFER_FOR_CPP_TYPE_VALUE(dst_type, dst_buffer);
BLI_SCOPED_DEFER([&]() { dst_type.destruct(dst_buffer); });
conversions.convert_to_uninitialized(type, dst_type, value.get(), dst_buffer);
validate_value(attributes, name, dst_type, dst_buffer);
const GPointer dst_value(dst_type, dst_buffer);
if (selection.size() == attributes.domain_size(meta_data->domain)) {
if (attributes.assign_data(name, bke::AttributeInitValue(dst_value))) {
DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id);
continue;
}
}
bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(name);
dst_type.fill_assign_indices(dst_value.get(), attribute.span.data(), selection);
attribute.finish();
DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id);
}
return OPERATOR_FINISHED;
}
static wmOperatorStatus set_attribute_invoke(bContext *C, wmOperator *op, const wmEvent *event)
{
Object *active_object = CTX_data_active_object(C);
Curves &active_curves_id = *id_cast<Curves *>(active_object->data);
AttributeOwner owner = AttributeOwner::from_id(&active_curves_id.id);
const StringRef name = *BKE_attributes_active_name_get(owner);
const bke::CurvesGeometry &curves = active_curves_id.geometry.wrap();
const bke::AttributeAccessor attributes = curves.attributes();
const bke::GAttributeReader attribute = attributes.lookup(name);
const bke::AttrDomain domain = attribute.domain;
IndexMaskMemory memory;
const IndexMask selection = retrieve_selected_elements(active_curves_id, domain, memory);
const CPPType &type = attribute.varray.type();
PropertyRNA *prop = geometry::rna_property_for_type(*op->ptr,
bke::cpp_type_to_attribute_type(type));
if (RNA_property_is_set(op->ptr, prop)) {
return WM_operator_props_popup(C, op, event);
}
BUFFER_FOR_CPP_TYPE_VALUE(type, buffer);
BLI_SCOPED_DEFER([&]() { type.destruct(buffer); });
bke::attribute_math::to_static_type(type, [&]<typename T>() {
if constexpr (!std::is_void_v<bke::attribute_math::DefaultMixer<T>>) {
const VArray<T> values_typed = attribute.varray.typed<T>();
bke::attribute_math::DefaultMixer<T> mixer{MutableSpan(static_cast<T *>(buffer), 1)};
selection.foreach_index([&](const int i) { mixer.mix_in(0, values_typed[i]); });
mixer.finalize();
}
});
geometry::rna_property_for_attribute_type_set_value(*op->ptr, *prop, GPointer(type, buffer));
return WM_operator_props_popup(C, op, event);
}
static void set_attribute_ui(bContext *C, wmOperator *op)
{
ui::Layout &layout = op->layout->column(true);
layout.use_property_split_set(true);
layout.use_property_decorate_set(false);
Object *object = CTX_data_active_object(C);
Curves &curves_id = *id_cast<Curves *>(object->data);
AttributeOwner owner = AttributeOwner::from_id(&curves_id.id);
const StringRef name = *BKE_attributes_active_name_get(owner);
const bke::CurvesGeometry &curves = curves_id.geometry.wrap();
const bke::AttributeMetaData meta_data = *curves.attributes().lookup_meta_data(name);
const StringRefNull prop_name = geometry::rna_property_name_for_type(meta_data.data_type);
layout.prop(op->ptr, prop_name, UI_ITEM_NONE, name, ICON_NONE);
}
void CURVES_OT_attribute_set(wmOperatorType *ot)
{
using namespace blender::ed;
using namespace blender::ed::curves;
ot->name = "Set Attribute";
ot->description = "Set values of the active attribute for selected elements";
ot->idname = "CURVES_OT_attribute_set";
ot->exec = set_attribute_exec;
ot->invoke = set_attribute_invoke;
ot->poll = active_attribute_poll;
ot->ui = set_attribute_ui;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
geometry::register_rna_properties_for_attribute_types(*ot->srna);
}
} // namespace blender::ed::curves
/** \} */

View File

@@ -0,0 +1,136 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_curves.hh"
#include "BKE_curves_utils.hh"
#include "DNA_object_types.h"
#include "ED_curves.hh"
#include "ED_transverts.hh"
namespace blender::ed::curves {
Vector<MutableSpan<float3>> get_curves_positions_for_write(bke::CurvesGeometry &curves)
{
Vector<MutableSpan<float3>> positions_per_attribute;
positions_per_attribute.append(curves.positions_for_write());
if (curves.has_curve_with_type(CURVE_TYPE_BEZIER)) {
positions_per_attribute.append(curves.handle_positions_left_for_write());
positions_per_attribute.append(curves.handle_positions_right_for_write());
}
return positions_per_attribute;
}
Vector<Span<float3>> get_curves_positions(const bke::CurvesGeometry &curves)
{
Vector<Span<float3>> positions_per_attribute;
positions_per_attribute.append(curves.positions());
const std::optional<Span<float3>> handles_left = curves.handle_positions_left();
const std::optional<Span<float3>> handles_right = curves.handle_positions_right();
if (handles_left && handles_right) {
positions_per_attribute.append(*handles_left);
positions_per_attribute.append(*handles_right);
}
return positions_per_attribute;
}
static std::array<IndexMask, 3> transverts_curves_selection(const bke::CurvesGeometry &curves,
const bool skip_handles,
IndexMaskMemory &memory)
{
std::array<IndexMask, 3> selection;
const Span<StringRef> selection_names = ed::curves::get_curves_selection_attribute_names(curves);
if (selection_names.size() == 1) {
selection[0] = ed::curves::retrieve_selected_points(curves, memory);
}
else {
const IndexMask bezier_points = bke::curves::curve_type_point_selection(
curves, CURVE_TYPE_BEZIER, memory);
for (const int i : selection_names.index_range()) {
selection[i] = ed::curves::retrieve_selected_points(
curves, selection_names[i], bezier_points, memory);
}
}
if (skip_handles) {
/* When the control point is selected, both handles are ignored. */
selection[1] = IndexMask::from_difference(selection[1], selection[0], memory);
selection[2] = IndexMask::from_difference(selection[2], selection[0], memory);
}
return selection;
}
void transverts_from_curves_positions_create(bke::CurvesGeometry &curves,
TransVertStore *tvs,
const bool skip_handles)
{
IndexMaskMemory memory;
std::array<IndexMask, 3> selection = transverts_curves_selection(curves, skip_handles, memory);
const int size = selection[0].size() + selection[1].size() + selection[2].size();
if (size == 0) {
return;
}
tvs->transverts = MEM_new_array_zeroed<TransVert>(size, __func__);
tvs->transverts_tot = size;
int offset = 0;
const Vector<MutableSpan<float3>> positions = ed::curves::get_curves_positions_for_write(curves);
for (const int attribute_i : positions.index_range()) {
selection[attribute_i].foreach_index(
[&](const int64_t i, const int64_t pos) {
TransVert &tv = tvs->transverts[pos + offset];
tv.loc = positions[attribute_i][i];
tv.flag = SELECT;
copy_v3_v3(tv.oldloc, tv.loc);
},
exec_mode::grain_size(1024));
offset += selection[attribute_i].size();
}
}
void transverts_update_curves(bke::CurvesGeometry &curves,
const TransVertStore *tvs,
const bool skip_handles)
{
IndexMaskMemory memory;
std::array<IndexMask, 3> selection = transverts_curves_selection(curves, skip_handles, memory);
const int size = selection[0].size() + selection[1].size() + selection[2].size();
if (size == 0 || size != tvs->transverts_tot) {
return;
}
int offset = 0;
const Vector<MutableSpan<float3>> positions = ed::curves::get_curves_positions_for_write(curves);
const Span<TransVert> all_transverts = {tvs->transverts, tvs->transverts_tot};
for (const int attribute_i : positions.index_range()) {
const Span<TransVert> transverts = all_transverts.slice_safe(offset,
selection[attribute_i].size());
selection[attribute_i].foreach_index(
[&](const int64_t i, const int64_t pos) {
const TransVert &tv = transverts[pos];
positions[attribute_i][i] += float3(tv.loc) - float3(tv.oldloc);
},
exec_mode::grain_size(1024));
offset += selection[attribute_i].size();
}
curves.tag_positions_changed();
curves.calculate_bezier_auto_handles();
}
float (*point_normals_array_create(const Curves *curves_id))[3]
{
const bke::CurvesGeometry &curves = curves_id->geometry.wrap();
const int size = curves.points_num();
float3 *data = MEM_new_array_uninitialized<float3>(size, __func__);
bke::curves_normals_point_domain_calc(curves, {data, size});
return reinterpret_cast<float (*)[3]>(data);
}
} // namespace blender::ed::curves

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,733 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edcurves
*/
#include "BLI_array_utils.hh"
#include "BKE_anonymous_attribute_id.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_curves_utils.hh"
#include "BKE_deform.hh"
#include "GEO_reorder.hh"
#include "ED_curves.hh"
namespace blender::ed::curves {
bool remove_selection(bke::CurvesGeometry &curves, const bke::AttrDomain selection_domain)
{
const bke::AttributeAccessor attributes = curves.attributes();
const VArray<bool> selection = *attributes.lookup_or_default<bool>(
".selection", selection_domain, true);
const int domain_size_orig = attributes.domain_size(selection_domain);
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_bools(selection, memory);
switch (selection_domain) {
case bke::AttrDomain::Point:
curves.remove_points(mask, {});
break;
case bke::AttrDomain::Curve:
curves.remove_curves(mask, {});
break;
default:
BLI_assert_unreachable();
}
return attributes.domain_size(selection_domain) != domain_size_orig;
}
static void curve_offsets_from_selection(const Span<IndexRange> selected_points,
const IndexRange points,
const int curve,
const bool cyclic,
Vector<int> &r_new_curve_offsets,
Vector<bool> &r_new_cyclic,
Vector<IndexRange> &r_src_ranges,
Vector<int> &r_dst_offsets,
Vector<int> &r_dst_to_src_curve)
{
if (selected_points.is_empty()) {
return;
}
const bool merge_loop = cyclic && selected_points.first().size() < points.size() &&
selected_points.first().first() == points.first() &&
selected_points.last().last() == points.last();
int last_dst_offset = r_dst_offsets.last();
int last_curve_offset = r_new_curve_offsets.last();
for (const IndexRange range : selected_points.drop_front(merge_loop)) {
r_src_ranges.append(range);
last_dst_offset += range.size();
r_dst_offsets.append(last_dst_offset);
last_curve_offset += range.size();
r_new_curve_offsets.append(last_curve_offset);
};
if (merge_loop) {
const IndexRange merge_to_end = selected_points.first();
r_src_ranges.append(merge_to_end);
r_dst_offsets.append(last_dst_offset + merge_to_end.size());
r_new_curve_offsets.last() += merge_to_end.size();
}
const int curves_added = selected_points.size() - merge_loop;
r_dst_to_src_curve.append_n_times(curve, curves_added);
r_new_cyclic.append_n_times(cyclic && selected_points.first().size() == points.size(),
curves_added);
}
static void append_point_knots(const Span<IndexRange> src_ranges,
const OffsetIndices<int> dst_offsets,
const Span<int> dst_to_src_curve,
const bke::CurvesGeometry &src_curves,
bke::CurvesGeometry &curves)
{
curves.nurbs_custom_knots_update_size();
const Span<int> src_points_by_curve = src_curves.points_by_curve().data();
const Span<int> src_knots_by_curve = src_curves.nurbs_custom_knots_by_curve().data();
const VArray<int8_t> src_orders = src_curves.nurbs_orders();
const VArray<int8_t> knot_modes = curves.nurbs_knots_modes();
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
const OffsetIndices<int> knots_by_curve = curves.nurbs_custom_knots_by_curve();
MutableSpan<float> dst_knots = curves.nurbs_custom_knots_for_write();
/* Source knots must be defined after destination knots, because when `src_curves` == `curves`
* call to `nurbs_custom_knots_for_write()` might invalidate the result of previously called
* `nurbs_custom_knots()`. */
const Span<float> src_knots = src_curves.nurbs_custom_knots();
const int old_curves_num = curves.curves_num() - dst_to_src_curve.size();
int range = 0;
for (const int appended_curve : dst_to_src_curve.index_range()) {
const int dst_curve = appended_curve + old_curves_num;
if (knot_modes[dst_curve] != NURBS_KNOT_MODE_CUSTOM) {
range++;
continue;
}
const int src_curve = dst_to_src_curve[appended_curve];
const int order = src_orders[src_curve];
const int first_curve_point = src_points_by_curve[src_curve];
const int first_curve_knot = src_knots_by_curve[src_curve];
const int point_to_knot = -first_curve_point + first_curve_knot;
const IndexRange src_range = src_ranges[range];
const IndexRange src_knot_range = IndexRange::from_begin_size(
src_range.first() + point_to_knot, src_range.size() + order);
const IndexRange dst_knot_range = knots_by_curve[dst_curve];
dst_knots.slice(dst_knot_range.take_front(src_knot_range.size()))
.copy_from(src_knots.slice(src_knot_range));
if (dst_offsets[range].size() != points_by_curve[dst_curve].size()) {
range++;
const IndexRange merged_tail = src_ranges[range];
const IndexRange src_tail_knots = merged_tail.shift(point_to_knot + order);
const IndexRange dst_tail_knots = dst_knot_range.take_back(src_tail_knots.size());
const float knot_shift = dst_knots[dst_tail_knots.one_before_start()] -
src_knots[src_tail_knots.one_before_start()];
for (const int i : src_tail_knots.index_range()) {
dst_knots[dst_tail_knots[i]] = src_knots[src_tail_knots[i]] + knot_shift;
}
}
range++;
}
}
void duplicate_points(bke::CurvesGeometry &curves, const IndexMask &mask)
{
if (curves.is_empty()) {
return;
}
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
const VArray<bool> src_cyclic = curves.cyclic();
Vector<int> dst_to_src_curve;
Vector<int> new_curve_offsets({points_by_curve.data().last()});
Vector<IndexRange> src_ranges;
Vector<int> dst_offsets({0});
Vector<bool> dst_cyclic;
dst_to_src_curve.reserve(curves.curves_num());
new_curve_offsets.reserve(curves.curves_num() + 1);
src_ranges.reserve(curves.curves_num());
dst_offsets.reserve(curves.curves_num() + 1);
dst_cyclic.reserve(curves.curves_num());
/* Add the duplicated curves and points. */
bke::curves::foreach_selected_point_ranges_per_curve(
mask,
points_by_curve,
[&](const int curve, const IndexRange points, Span<IndexRange> ranges_to_duplicate) {
curve_offsets_from_selection(ranges_to_duplicate,
points,
curve,
src_cyclic[curve],
new_curve_offsets,
dst_cyclic,
src_ranges,
dst_offsets,
dst_to_src_curve);
});
const int old_curves_num = curves.curves_num();
const int old_points_num = curves.points_num();
const int num_curves_to_add = dst_to_src_curve.size();
const int num_points_to_add = mask.size();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
/* Delete selection attribute so that it will not have to be resized. */
remove_selection_attributes(attributes);
curves.resize(old_points_num + num_points_to_add, old_curves_num + num_curves_to_add);
array_utils::copy(new_curve_offsets.as_span(),
curves.offsets_for_write().drop_front(old_curves_num));
/* Transfer curve and point attributes. */
attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
if (iter.storage_type == bke::AttrStorageType::Single) {
return;
}
bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(iter.name);
if (!attribute) {
return;
}
switch (iter.domain) {
case bke::AttrDomain::Curve: {
if (iter.name == "cyclic") {
attribute.finish();
return;
}
bke::attribute_math::gather(
attribute.span,
dst_to_src_curve,
attribute.span.slice(IndexRange(old_curves_num, num_curves_to_add)));
break;
}
case bke::AttrDomain::Point: {
bke::attribute_math::gather_ranges_to_groups(
src_ranges.as_span(),
dst_offsets.as_span(),
attribute.span,
attribute.span.slice(IndexRange(old_points_num, num_points_to_add)));
break;
}
default: {
attribute.finish();
BLI_assert_unreachable();
return;
}
}
attribute.finish();
});
if (!(src_cyclic.is_single() && !src_cyclic.get_internal_single())) {
array_utils::copy(dst_cyclic.as_span(), curves.cyclic_for_write().drop_front(old_curves_num));
}
curves.update_curve_types();
curves.tag_topology_changed();
if (curves.nurbs_has_custom_knots()) {
append_point_knots(src_ranges, dst_offsets.as_span(), dst_to_src_curve, curves, curves);
}
for (const StringRef selection_name : get_curves_selection_attribute_names(curves)) {
bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
selection_name, bke::AttrDomain::Point);
selection.span.take_back(num_points_to_add).fill(true);
selection.finish();
}
}
static void append_curve_knots(const IndexMask &mask, bke::CurvesGeometry &curves)
{
curves.nurbs_custom_knots_update_size();
const int old_curves_num = curves.curves_num() - mask.size();
bke::curves::nurbs::gather_custom_knots(curves, mask, old_curves_num, curves);
}
void duplicate_curves(bke::CurvesGeometry &curves, const IndexMask &mask)
{
const int orig_points_num = curves.points_num();
const int orig_curves_num = curves.curves_num();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
/* Delete selection attribute so that it will not have to be resized. */
remove_selection_attributes(attributes);
/* Resize the curves and copy the offsets of duplicated curves into the new offsets. */
curves.resize(curves.points_num(), orig_curves_num + mask.size());
const IndexRange orig_curves_range = curves.curves_range().take_front(orig_curves_num);
const IndexRange new_curves_range = curves.curves_range().drop_front(orig_curves_num);
MutableSpan<int> offset_data = curves.offsets_for_write();
offset_indices::gather_selected_offsets(
OffsetIndices<int>(offset_data.take_front(orig_curves_num + 1)),
mask,
orig_points_num,
offset_data.drop_front(orig_curves_num));
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
/* Resize the points array to match the new total point count. */
curves.resize(points_by_curve.total_size(), curves.curves_num());
attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
if (iter.storage_type == bke::AttrStorageType::Single) {
return;
}
bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(iter.name);
switch (iter.domain) {
case bke::AttrDomain::Point:
bke::attribute_math::gather_group_to_group(points_by_curve.slice(orig_curves_range),
points_by_curve.slice(new_curves_range),
mask,
attribute.span,
attribute.span);
break;
case bke::AttrDomain::Curve:
array_utils::gather(attribute.span, mask, attribute.span.take_back(mask.size()));
break;
default:
BLI_assert_unreachable();
return;
}
attribute.finish();
});
curves.update_curve_types();
curves.tag_topology_changed();
if (curves.nurbs_has_custom_knots()) {
append_curve_knots(mask, curves);
}
for (const StringRef selection_name : get_curves_selection_attribute_names(curves)) {
bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
selection_name, bke::AttrDomain::Curve);
selection.span.take_back(mask.size()).fill(true);
selection.finish();
}
}
static void invert_ranges(const IndexRange universe,
const Span<IndexRange> ranges,
Array<IndexRange> &inverted)
{
const bool contains_first = ranges.first().first() == universe.first();
const bool contains_last = ranges.last().last() == universe.last();
inverted.reinitialize(ranges.size() - 1 + !contains_first + !contains_last);
int64_t start = contains_first ? ranges.first().one_after_last() : universe.first();
int i = 0;
for (const IndexRange range : ranges.drop_front(contains_first)) {
inverted[i++] = IndexRange::from_begin_end(start, range.first());
start = range.one_after_last();
}
if (!contains_last) {
inverted.last() = IndexRange::from_begin_end(start, universe.one_after_last());
}
}
static IndexRange extend_range(const IndexRange range, const IndexRange universe)
{
return IndexRange::from_begin_end_inclusive(math::max(range.start() - 1, universe.start()),
math::min(range.one_after_last(), universe.last()));
}
/**
* Extends each range by one point at both ends of it. Merges adjacent ranges if intersections
* occur.
*/
static void extend_range_by_1_within_bounds(const IndexRange universe,
const bool cyclic,
const Span<IndexRange> ranges,
Vector<IndexRange> &extended_ranges)
{
extended_ranges.clear();
if (ranges.is_empty()) {
return;
}
const bool first_match = ranges.first().first() == universe.first();
const bool last_match = ranges.last().last() == universe.last();
const bool add_first = cyclic && last_match && !first_match;
const bool add_last = cyclic && first_match && !last_match;
IndexRange current = add_first ? IndexRange::from_single(universe.first()) :
extend_range(ranges.first(), universe);
for (const IndexRange range : ranges.drop_front(!add_first)) {
const IndexRange extended = extend_range(range, universe);
if (extended.first() <= current.last()) {
current = IndexRange::from_begin_end_inclusive(current.start(), extended.last());
}
else {
extended_ranges.append(current);
current = extended;
}
}
extended_ranges.append(current);
if (add_last) {
extended_ranges.append(IndexRange::from_single(universe.last()));
}
}
static bke::CurvesGeometry copy_data_to_geometry(const bke::CurvesGeometry &src_curves,
const Span<int> dst_to_src_curve,
const Span<int> offsets,
const Span<bool> cyclic,
const Span<IndexRange> src_ranges,
const OffsetIndices<int> dst_offsets)
{
bke::CurvesGeometry dst_curves(offsets.last(), dst_to_src_curve.size());
BKE_defgroup_copy_list(&dst_curves.vertex_group_names, &src_curves.vertex_group_names);
if (!dst_curves.is_empty()) {
array_utils::copy(offsets, dst_curves.offsets_for_write());
}
dst_curves.cyclic_for_write().copy_from(cyclic);
const bke::AttributeAccessor src_attributes = src_curves.attributes();
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
bke::gather_attributes(src_attributes,
bke::AttrDomain::Curve,
bke::AttrDomain::Curve,
bke::attribute_filter_from_skip_ref({"cyclic"}),
dst_to_src_curve,
dst_attributes);
for (auto &attribute : bke::retrieve_attributes_for_transfer(
src_attributes,
dst_attributes,
{bke::AttrDomain::Point},
bke::attribute_filter_from_skip_ref(
ed::curves::get_curves_selection_attribute_names(src_curves))))
{
bke::attribute_math::gather_ranges_to_groups(
src_ranges, dst_offsets, attribute.src, attribute.dst.span);
attribute.dst.finish();
};
dst_curves.update_curve_types();
if (src_curves.nurbs_has_custom_knots()) {
append_point_knots(src_ranges, dst_offsets, dst_to_src_curve, src_curves, dst_curves);
}
return dst_curves;
}
bke::CurvesGeometry split_points(const bke::CurvesGeometry &curves,
const IndexMask &points_to_split)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
const VArray<bool> cyclic = curves.cyclic();
Vector<int> curve_map;
Vector<int> new_offsets({0});
Vector<IndexRange> src_ranges;
Vector<int> dst_offsets({0});
Vector<bool> new_cyclic;
Vector<IndexRange> deselect;
Array<IndexRange> unselected_curve_points;
Vector<IndexRange> curve_points_to_preserve;
bke::curves::foreach_selected_point_ranges_per_curve(
points_to_split,
points_by_curve,
[&](const int curve, const IndexRange points, const Span<IndexRange> selected_curve_points) {
curve_offsets_from_selection(selected_curve_points,
points,
curve,
cyclic[curve],
new_offsets,
new_cyclic,
src_ranges,
dst_offsets,
curve_map);
/* Invert ranges to get non selected points. */
invert_ranges(points, selected_curve_points, unselected_curve_points);
/* Extended every range to left and right by one point. Any resulting intersection is
* merged. */
extend_range_by_1_within_bounds(
points, cyclic[curve], unselected_curve_points, curve_points_to_preserve);
const int size_before = curve_map.size();
/* Unselected part can contain all points from original curve, but have cuts. This happens
* when pairs of adjacent points are selected. To prevent loop merge and result curve from
* cyclic additional condition is checked. */
const bool can_merge_loop = !unselected_curve_points.is_empty() &&
(unselected_curve_points.first().first() == points.first() ||
unselected_curve_points.last().last() == points.last());
curve_offsets_from_selection(curve_points_to_preserve,
points,
curve,
cyclic[curve] && can_merge_loop,
new_offsets,
new_cyclic,
src_ranges,
dst_offsets,
curve_map);
deselect.append(IndexRange::from_begin_end(size_before, curve_map.size()));
},
[&](const IndexRange curves, const IndexRange /*unselected_points*/) {
deselect.append(IndexRange::from_begin_size(curve_map.size(), curves.size()));
int last_offset = new_offsets.last();
int last_dst_offset = dst_offsets.last();
for (const int curve : curves) {
/* Point ranges to `src_ranges` and `dst_offsets` have to be appended curve by curve to
* ease custom knots copying. It gives better mapping between `src_ranges` and
* `curve_map`. */
const IndexRange points = points_by_curve[curve];
src_ranges.append(points);
last_dst_offset += points.size();
dst_offsets.append(last_dst_offset);
last_offset += points.size();
new_offsets.append(last_offset);
curve_map.append(curve);
new_cyclic.append(cyclic[curve]);
}
});
bke::CurvesGeometry new_curves = copy_data_to_geometry(
curves, curve_map, new_offsets, new_cyclic, src_ranges, dst_offsets.as_span());
const OffsetIndices<int> new_points_by_curve = new_curves.points_by_curve();
foreach_selection_attribute_writer(
new_curves, bke::AttrDomain::Point, [&](bke::GSpanAttributeWriter &selection) {
for (const IndexRange curves : deselect) {
for (const int curve : curves) {
fill_selection_false(selection.span.slice(new_points_by_curve[curve]));
}
}
});
return new_curves;
}
void separate_points(const bke::CurvesGeometry &curves,
const IndexMask &points_to_separate,
bke::CurvesGeometry &separated,
bke::CurvesGeometry &retained)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
const VArray<bool> cyclic = curves.cyclic();
Vector<int> separated_curve_map;
Vector<int> separated_offsets({0});
Vector<IndexRange> separated_src_ranges;
Vector<int> separated_dst_offsets({0});
Vector<bool> separated_cyclic;
Vector<int> retained_curve_map;
Vector<int> retained_offsets({0});
Vector<IndexRange> retained_src_ranges;
Vector<int> retained_dst_offsets({0});
Vector<bool> retained_cyclic;
Array<IndexRange> unselected_curve_points;
Vector<IndexRange> curve_points_to_retain;
bke::curves::foreach_selected_point_ranges_per_curve(
points_to_separate,
points_by_curve,
[&](const int curve, const IndexRange points, const Span<IndexRange> selected_curve_points) {
curve_offsets_from_selection(selected_curve_points,
points,
curve,
cyclic[curve],
separated_offsets,
separated_cyclic,
separated_src_ranges,
separated_dst_offsets,
separated_curve_map);
/* Invert ranges to get non selected points. */
invert_ranges(points, selected_curve_points, unselected_curve_points);
/* Extended every range to left and right by one point. Any resulting intersection is
* merged. */
extend_range_by_1_within_bounds(
points, cyclic[curve], unselected_curve_points, curve_points_to_retain);
/* Unselected part can contain all points from original curve, but have cuts. This happens
* when pairs of adjacent points are selected. To prevent loop merge and result curve from
* cyclic additional condition is checked. */
const bool can_merge_loop = !unselected_curve_points.is_empty() &&
(unselected_curve_points.first().first() == points.first() ||
unselected_curve_points.last().last() == points.last());
curve_offsets_from_selection(curve_points_to_retain,
points,
curve,
cyclic[curve] && can_merge_loop,
retained_offsets,
retained_cyclic,
retained_src_ranges,
retained_dst_offsets,
retained_curve_map);
},
[&](const IndexRange curves, const IndexRange /*unselected_points*/) {
int last_offset = retained_offsets.last();
int last_dst_offset = retained_dst_offsets.last();
for (const int curve : curves) {
/* Point ranges to `retained_src_ranges` and `retained_dst_offsets` have to be appended
* curve by curve to ease custom knots copying. It gives better mapping between
* `retained_src_ranges` and `retained_curve_map`. */
const IndexRange points = points_by_curve[curve];
retained_src_ranges.append(points);
last_dst_offset += points.size();
retained_dst_offsets.append(last_dst_offset);
last_offset += points.size();
retained_offsets.append(last_offset);
retained_curve_map.append(curve);
retained_cyclic.append(cyclic[curve]);
}
});
{
bke::MutableAttributeAccessor attributes = separated.attributes_for_write();
remove_selection_attributes(attributes);
separated = copy_data_to_geometry(curves,
separated_curve_map,
separated_offsets,
separated_cyclic,
separated_src_ranges,
separated_dst_offsets.as_span());
}
{
bke::MutableAttributeAccessor attributes = retained.attributes_for_write();
remove_selection_attributes(attributes);
retained = copy_data_to_geometry(curves,
retained_curve_map,
retained_offsets,
retained_cyclic,
retained_src_ranges,
retained_dst_offsets.as_span());
}
foreach_selection_attribute_writer(
retained, bke::AttrDomain::Point, [&](bke::GSpanAttributeWriter &selection) {
fill_selection_false(selection.span);
});
}
void add_curves(bke::CurvesGeometry &curves, const Span<int> new_sizes)
{
const int orig_points_num = curves.points_num();
const int orig_curves_num = curves.curves_num();
curves.resize(orig_points_num, orig_curves_num + new_sizes.size());
/* Find the final number of points by accumulating the new */
MutableSpan<int> new_offsets = curves.offsets_for_write().drop_front(orig_curves_num);
new_offsets.drop_back(1).copy_from(new_sizes);
offset_indices::accumulate_counts_to_offsets(new_offsets, orig_points_num);
/* First, resize the curve domain. */
curves.resize(curves.offsets().last(), curves.curves_num());
/* Initialize new attribute values, since #CurvesGeometry::resize() doesn't do that. */
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
bke::fill_attribute_range_default(
attributes, bke::AttrDomain::Point, {}, curves.points_range().drop_front(orig_points_num));
bke::fill_attribute_range_default(
attributes, bke::AttrDomain::Curve, {}, curves.curves_range().drop_front(orig_curves_num));
curves.update_curve_types();
}
void resize_curves(bke::CurvesGeometry &curves,
const IndexMask &curves_to_resize,
const Span<int> new_sizes)
{
if (curves_to_resize.is_empty()) {
return;
}
BLI_assert(curves_to_resize.size() == new_sizes.size());
bke::CurvesGeometry dst_curves = bke::curves::copy_only_curve_domain(curves);
IndexMaskMemory memory;
IndexMask curves_to_copy;
std::optional<IndexRange> range = curves_to_resize.to_range();
/* Check if we need to copy some curves over. Write the new sizes into the offsets. */
if (range && curves.curves_range() == *range) {
curves_to_copy = {};
dst_curves.offsets_for_write().drop_back(1).copy_from(new_sizes);
}
else {
curves_to_copy = curves_to_resize.complement(curves.curves_range(), memory);
offset_indices::copy_group_sizes(
curves.offsets(), curves_to_copy, dst_curves.offsets_for_write());
array_utils::scatter(new_sizes, curves_to_resize, dst_curves.offsets_for_write());
}
/* Accumulate the sizes written from `new_sizes` into offsets. */
offset_indices::accumulate_counts_to_offsets(dst_curves.offsets_for_write());
/* Resize the points domain. */
dst_curves.resize(dst_curves.offsets().last(), dst_curves.curves_num());
/* Copy point attributes and default initialize newly added point ranges. */
const OffsetIndices<int> src_offsets = curves.points_by_curve();
const OffsetIndices<int> dst_offsets = dst_curves.points_by_curve();
const bke::AttributeAccessor src_attributes = curves.attributes();
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
src_attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
if (iter.domain != bke::AttrDomain::Point) {
return;
}
const GVArray src = *iter.get();
const CPPType &type = src.type();
const CommonVArrayInfo info = src.common_info();
if (info.type == CommonVArrayInfo::Type::Single) {
const bke::AttributeInitValue init(GPointer(type, info.data));
if (dst_attributes.add(iter.name, iter.domain, iter.data_type, init)) {
return;
}
}
bke::GSpanAttributeWriter dst = dst_attributes.lookup_or_add_for_write_only_span(
iter.name, iter.domain, iter.data_type);
if (!dst) {
return;
}
const GVArraySpan src_span(src);
curves_to_resize.foreach_index(
[&](const int curve_i) {
const IndexRange src_points = src_offsets[curve_i];
const IndexRange dst_points = dst_offsets[curve_i];
if (dst_points.size() < src_points.size()) {
const int src_excees = src_points.size() - dst_points.size();
dst.span.slice(dst_points).copy_from(src_span.slice(src_points.drop_back(src_excees)));
}
else {
const int dst_excees = dst_points.size() - src_points.size();
dst.span.slice(dst_points.drop_back(dst_excees)).copy_from(src_span.slice(src_points));
GMutableSpan dst_end_slice = dst.span.slice(dst_points.take_back(dst_excees));
type.value_initialize_n(dst_end_slice.data(), dst_end_slice.size());
}
},
exec_mode::grain_size(512));
array_utils::copy_group_to_group(src_offsets, dst_offsets, curves_to_copy, src_span, dst.span);
dst.finish();
});
dst_curves.update_curve_types();
if (dst_curves.nurbs_has_custom_knots()) {
bke::curves::nurbs::update_custom_knot_modes(
dst_curves.curves_range(), NURBS_KNOT_MODE_NORMAL, NURBS_KNOT_MODE_NORMAL, dst_curves);
}
/* Move the result into `curves`. */
curves = std::move(dst_curves);
curves.tag_topology_changed();
}
void reorder_curves(bke::CurvesGeometry &curves, const Span<int> old_by_new_indices_map)
{
curves = geometry::reorder_curves_geometry(curves, old_by_new_indices_map, {});
}
} // namespace blender::ed::curves

View File

@@ -0,0 +1,372 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute.hh"
#include "BKE_curves_utils.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "ED_curves.hh"
#include "DEG_depsgraph.hh"
namespace blender::ed::curves {
/**
* Merges copy intervals at curve endings to minimize number of copy operations.
* For example given in function 'extrude_curves' intervals [0, 3, 4, 4, 4] became [0, 4, 4].
* Leading to only two copy operations.
*/
static Span<int> compress_intervals(const OffsetIndices<int> intervals_by_curve,
MutableSpan<int> intervals)
{
const int *src = intervals.data();
/* Skip the first curve, as all the data stays in the same place.
* -1 to drop index denoting curve's right endpoint.
*/
int *dst = intervals.data() + intervals_by_curve[0].size() - 1;
for (const int curve : intervals_by_curve.index_range().drop_front(1)) {
const IndexRange range = intervals_by_curve[curve];
/* -2 one to drop index denoting curve's beginning, second one for ending. */
const int width = range.size() - 2;
std::copy_n(src + range.first() + 1, width, dst);
dst += width;
}
(*dst) = src[intervals_by_curve[intervals_by_curve.size() - 1].last()];
return {intervals.data(), dst - intervals.data() + 1};
}
static void calc_curves_extrusion(const IndexMask &selection,
const OffsetIndices<int> points_by_curve,
MutableSpan<int> copy_intervals,
MutableSpan<int> curves_intervals_offsets,
MutableSpan<bool> is_first_selected)
{
int current_endpoint_index = 0;
curves_intervals_offsets.first() = 0;
bke::curves::foreach_selected_point_ranges_per_curve(
selection,
points_by_curve,
[&](const int curve,
const IndexRange curve_points,
const Span<IndexRange> selected_point_ranges) {
const IndexRange first_range = selected_point_ranges.first();
is_first_selected[curve] = first_range.first() == curve_points.start() &&
first_range.size() == 1 &&
/* If single point curve is extruded we want the newly created
* point to get selected. */
curve_points.size() != 1;
current_endpoint_index += !is_first_selected[curve];
copy_intervals[curves_intervals_offsets[curve]] = curve_points.start();
for (const IndexRange range : selected_point_ranges) {
copy_intervals[current_endpoint_index++] = range.first();
copy_intervals[current_endpoint_index++] = range.last();
}
const int last_interval_index = current_endpoint_index - 1;
if (copy_intervals[last_interval_index] != curve_points.last() ||
copy_intervals[last_interval_index - 1] != copy_intervals[last_interval_index])
{
/* Append last point of the current curve if it is not extruded or extruded together with
* preceding points. */
copy_intervals[current_endpoint_index++] = curve_points.last();
}
curves_intervals_offsets[curve + 1] = current_endpoint_index;
},
[&](const IndexRange curves, [[maybe_unused]] const IndexRange unselected_points) {
for (const int curve : curves) {
const IndexRange curve_points = points_by_curve[curve];
/* Setup interval to copy full curve. */
is_first_selected[curve] = false;
copy_intervals[current_endpoint_index++] = curve_points.first();
copy_intervals[current_endpoint_index++] = curve_points.last();
curves_intervals_offsets[curve + 1] = current_endpoint_index;
}
});
}
static void calc_new_offsets(const Span<int> old_offsets,
const Span<int> curves_intervals_offsets,
MutableSpan<int> new_offsets)
{
new_offsets.first() = 0;
const IndexRange range = old_offsets.index_range().drop_back(1).shift(1);
threading::parallel_for(range, 256, [&](IndexRange index_range) {
for (const int i : index_range) {
/* -1 subtracts last interval endpoint and gives number of intervals.
* Another -1 from number of intervals gives number of new points created for curve.
* Multiplied by i because -2 are accumulated for each curve.
*/
new_offsets[i] = old_offsets[i] + curves_intervals_offsets[i] - 2 * i;
}
});
}
/**
* Creates a new index range with the same beginning but a shifted end.
*/
static IndexRange shift_end_by(const IndexRange &range, const int n)
{
return IndexRange::from_begin_size(range.start(), range.size() + n);
}
static float clamp_to_zero(const float value)
{
return math::abs(value) < 0.00001 ? 0.0 : value;
}
static void extrude_knots(const bke::CurvesGeometry &curves,
const OffsetIndices<int> intervals_by_curve,
const OffsetIndices<int> copy_intervals,
const Span<bool> is_first_selected,
bke::CurvesGeometry &dst_curves)
{
IndexMaskMemory memory;
const IndexMask custom_knot_curves = curves.nurbs_custom_knot_curves(memory);
const Span<float> src_knots = curves.nurbs_custom_knots();
const VArray<int8_t> orders = curves.nurbs_orders();
const OffsetIndices<int> src_knots_by_curve = curves.nurbs_custom_knots_by_curve();
dst_curves.nurbs_custom_knots_update_size();
MutableSpan<float> dst_knots = dst_curves.nurbs_custom_knots_for_write();
custom_knot_curves.foreach_index(
[&](const int64_t curve) {
const int order = orders[curve];
const bool is_first_interval_selected = is_first_selected[curve];
Span<float> src_curve_knots = src_knots.slice(src_knots_by_curve[curve]);
Array<float> curve_span_data(src_curve_knots.size() - 1);
Array<int> span_multiplicity(curve_span_data.size(), 0);
int span = 0;
curve_span_data[span] = clamp_to_zero(src_curve_knots[1] - src_curve_knots[0]);
span_multiplicity[span] = 1;
for (const int i : src_curve_knots.index_range().drop_back(1).drop_front(1)) {
const float span_value = clamp_to_zero(src_curve_knots[i + 1] - src_curve_knots[i]);
const bool is_new = abs(curve_span_data[span] - span_value) >= 0.00001;
span += is_new;
curve_span_data[span] = span_value;
span_multiplicity[span]++;
}
MutableSpan<float> curve_spans = curve_span_data.as_mutable_span().slice(0, span + 1);
const IndexRange curve_intervals = intervals_by_curve[curve];
const Span<int> duplicated_points =
copy_intervals.data().slice(curve_intervals).drop_front(1).drop_back(1);
const int first_curve_point = copy_intervals.data()[curve_intervals.first()];
Vector<int> increase_span_multiplicity;
increase_span_multiplicity.reserve(duplicated_points.size());
int first_span_knot = 0;
span = 0;
for (const int i : duplicated_points.index_range()) {
const bool is_selected = bool(i % 2) != is_first_interval_selected;
const int point = duplicated_points[i] - first_curve_point;
while (first_span_knot + span_multiplicity[span] <= point) {
first_span_knot += span_multiplicity[span];
span++;
}
int multiplicity = point - first_span_knot;
int point_span = span;
std::array<int, 2> side_spans{point_span, point_span};
int side = 0;
for ([[maybe_unused]] const int i : IndexRange(order)) {
multiplicity++;
if (multiplicity > span_multiplicity[point_span]) {
point_span++;
multiplicity = 1;
}
if (curve_spans[point_span] == 0.0) {
continue;
}
side_spans[side] = point_span;
side = 1;
side_spans[side] = point_span;
}
increase_span_multiplicity.append(side_spans[is_selected]);
}
for (const int span : increase_span_multiplicity) {
span_multiplicity[span]++;
}
const OffsetIndices<int> dst_knots_by_curve = dst_curves.nurbs_custom_knots_by_curve();
MutableSpan<float> dst_curve_knots = dst_knots.slice(dst_knots_by_curve[curve]);
int knot = 0;
float knot_value = src_curve_knots[knot];
dst_curve_knots[knot++] = knot_value;
for (const int span : curve_spans.index_range()) {
for ([[maybe_unused]] const int k : IndexRange(span_multiplicity[span])) {
knot_value += curve_spans[span];
dst_curve_knots[knot++] = knot_value;
}
}
},
exec_mode::grain_size(64));
}
static bke::CurvesGeometry extrude_curves(const bke::CurvesGeometry &curves,
const IndexMask &extruded_points)
{
bke::CurvesGeometry new_curves = bke::curves::copy_only_curve_domain(curves);
const int curves_num = curves.curves_num();
/* Buffer for intervals of all curves. Beginning and end of a curve can be determined only by
* #curve_interval_ranges. For ex. [0, 3, 4, 4, 4] indicates one copy interval for first curve
* [0, 3] and two for second [4, 4][4, 4]. The first curve will be copied as is without changes,
* in the second one (consisting only one point - 4) first point will be duplicated (extruded).
*/
Array<int> copy_interval_offsets(extruded_points.size() * 2 + curves_num * 2);
/* Points to intervals for each curve in the copy_intervals array.
* For example above value would be [0, 3, 5]. Meaning that [0 .. 2] are indices for curve 0 in
* copy_intervals array, [3 .. 4] for curve 1. */
Array<int> curves_intervals_offsets(curves_num + 1);
/* Per curve boolean indicating if first interval in a curve is selected.
* Other can be calculated as in a curve two adjacent intervals can not have same selection
* state. */
Array<bool> is_first_selected(curves_num);
calc_curves_extrusion(extruded_points,
curves.points_by_curve(),
copy_interval_offsets,
curves_intervals_offsets,
is_first_selected);
MutableSpan<int> new_offsets = new_curves.offsets_for_write();
calc_new_offsets(curves.offsets(), curves_intervals_offsets, new_offsets);
new_curves.resize(new_offsets.last(), new_curves.curves_num());
const bke::AttributeAccessor src_attributes = curves.attributes();
std::array<GVArraySpan, 3> src_selection;
std::array<bke::GSpanAttributeWriter, 3> dst_selections;
const Span<StringRef> selection_attr_names = get_curves_selection_attribute_names(curves);
for (const int selection_i : selection_attr_names.index_range()) {
const StringRef selection_name = selection_attr_names[selection_i];
GVArray src_selection_array = *src_attributes.lookup(selection_name, bke::AttrDomain::Point);
if (!src_selection_array) {
src_selection_array = VArray<bool>::from_single(true, curves.points_num());
}
src_selection[selection_i] = src_selection_array;
dst_selections[selection_i] = ensure_selection_attribute(
new_curves,
bke::AttrDomain::Point,
src_selection_array.type().is<bool>() ? bke::AttrType::Bool : bke::AttrType::Float,
selection_name);
}
const OffsetIndices<int> intervals_by_curve = curves_intervals_offsets.as_span();
const OffsetIndices<int> copy_intervals = copy_interval_offsets.as_span().slice(
0, curves_intervals_offsets.last());
threading::parallel_for(curves.curves_range(), 256, [&](IndexRange curves_range) {
for (const int curve : curves_range) {
const int first_index = intervals_by_curve[curve].start();
const int first_value = copy_intervals[first_index].start();
const bool first_selected = is_first_selected[curve];
for (const int i : intervals_by_curve[curve].drop_back(1)) {
const bool is_selected = bool((i - first_index) % 2) != first_selected;
const IndexRange src = shift_end_by(copy_intervals[i], 1);
const IndexRange dst = src.shift(new_offsets[curve] - first_value + i - first_index);
for (const int selection_i : selection_attr_names.index_range()) {
GMutableSpan dst_span = dst_selections[selection_i].span.slice(dst);
if (is_selected) {
GSpan src_span = src_selection[selection_i].slice(src);
src_selection[selection_i].type().copy_assign_n(
src_span.data(), dst_span.data(), src.size());
}
else {
fill_selection(dst_span, false);
}
}
}
}
});
for (const int selection_i : selection_attr_names.index_range()) {
dst_selections[selection_i].finish();
}
if (curves.nurbs_has_custom_knots()) {
extrude_knots(curves, intervals_by_curve, copy_intervals, is_first_selected, new_curves);
}
const OffsetIndices<int> compact_intervals = compress_intervals(intervals_by_curve,
copy_interval_offsets);
bke::MutableAttributeAccessor dst_attributes = new_curves.attributes_for_write();
for (auto &attribute : bke::retrieve_attributes_for_transfer(
src_attributes,
dst_attributes,
{bke::AttrDomain::Point},
bke::attribute_filter_from_skip_ref(selection_attr_names)))
{
const CPPType &type = attribute.src.type();
threading::parallel_for(compact_intervals.index_range(), 512, [&](IndexRange range) {
for (const int i : range) {
const IndexRange src = shift_end_by(compact_intervals[i], 1);
const IndexRange dst = src.shift(i);
type.copy_assign_n(
attribute.src.slice(src).data(), attribute.dst.span.slice(dst).data(), src.size());
}
});
attribute.dst.finish();
}
return new_curves;
}
static wmOperatorStatus curves_extrude_exec(bContext *C, wmOperator * /*op*/)
{
bool extruded = false;
for (Curves *curves_id : get_unique_editable_curves(*C)) {
const bke::AttrDomain selection_domain = bke::AttrDomain(curves_id->selection_domain);
if (selection_domain != bke::AttrDomain::Point) {
continue;
}
const bke::CurvesGeometry &curves = curves_id->geometry.wrap();
IndexMaskMemory memory;
const IndexMask extruded_points = retrieve_selected_points(curves, memory);
if (extruded_points.is_empty()) {
continue;
}
curves_id->geometry.wrap() = extrude_curves(curves, extruded_points);
DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY);
extruded = true;
}
return extruded ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
}
void CURVES_OT_extrude(wmOperatorType *ot)
{
ot->name = "Extrude";
ot->description = "Extrude selected control point(s)";
ot->idname = "CURVES_OT_extrude";
ot->exec = curves_extrude_exec;
ot->poll = editable_curves_in_edit_mode_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
} // namespace blender::ed::curves

View File

@@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edcurves
*/
#include "BLI_offset_indices.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "ED_curves.hh"
namespace blender::ed::curves {
IndexMask end_points(const bke::CurvesGeometry &curves,
const IndexMask &curves_mask,
const int amount_start,
const int amount_end,
const bool inverted,
IndexMaskMemory &memory)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
Array<bool> end_points(curves.points_num(), inverted ? false : true);
curves_mask.foreach_index(
[&](const int64_t curve_i) {
end_points.as_mutable_span()
.slice(points_by_curve[curve_i].drop_front(amount_start).drop_back(amount_end))
.fill(inverted ? true : false);
},
exec_mode::grain_size(512));
return IndexMask::from_bools(end_points, memory);
}
IndexMask end_points(const bke::CurvesGeometry &curves,
const int amount_start,
const int amount_end,
const bool inverted,
IndexMaskMemory &memory)
{
return end_points(curves, curves.curves_range(), amount_start, amount_end, inverted, memory);
}
} // namespace blender::ed::curves

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edcurves
*/
#include "BLI_task.hh"
#include "BKE_context.hh"
#include "BKE_curves.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "BKE_undo_system.hh"
#include "CLG_log.h"
#include "DEG_depsgraph.hh"
#include "ED_curves.hh"
#include "ED_undo.hh"
#include "WM_api.hh"
#include "WM_types.hh"
namespace blender {
static CLG_LogRef LOG = {"undo.curves"};
namespace ed::curves {
namespace undo {
/* -------------------------------------------------------------------- */
/** \name Implements ED Undo System
*
* \note This is similar for all edit-mode types.
* \{ */
struct StepObject {
UndoRefID_Object obedit_ref = {};
bke::CurvesGeometry geometry = {};
};
struct CurvesUndoStep {
UndoStep step;
/** See #ED_undo_object_editmode_validate_scene_from_windows code comment for details. */
UndoRefID_Scene scene_ref = {};
Array<StepObject> objects;
};
static bool step_encode(bContext *C, Main *bmain, UndoStep *us_p)
{
CurvesUndoStep *us = reinterpret_cast<CurvesUndoStep *>(us_p);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
Vector<Object *> objects = ED_undo_editmode_objects_from_view_layer(*bmain, scene, view_layer);
us->scene_ref.ptr = scene;
new (&us->objects) Array<StepObject>(objects.size());
threading::parallel_for(us->objects.index_range(), 8, [&](const IndexRange range) {
for (const int i : range) {
Object *ob = objects[i];
const Curves &curves_id = *id_cast<Curves *>(ob->data);
StepObject &object = us->objects[i];
object.obedit_ref.ptr = ob;
object.geometry = curves_id.geometry.wrap();
}
});
bmain->is_memfile_undo_flush_needed = true;
return true;
}
static void step_decode(
bContext *C, Main *bmain, UndoStep *us_p, const eUndoStepDir /*dir*/, bool /*is_final*/)
{
CurvesUndoStep *us = reinterpret_cast<CurvesUndoStep *>(us_p);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
ED_undo_object_editmode_validate_scene_from_windows(
CTX_wm_manager(C), us->scene_ref.ptr, &scene, &view_layer);
ED_undo_object_editmode_restore_helper(scene,
view_layer,
&us->objects.first().obedit_ref.ptr,
us->objects.size(),
sizeof(decltype(us->objects)::value_type));
BLI_assert(BKE_object_is_in_editmode(us->objects.first().obedit_ref.ptr));
for (const StepObject &object : us->objects) {
Curves &curves_id = *id_cast<Curves *>(object.obedit_ref.ptr->data);
/* Overwrite the curves geometry. */
curves_id.geometry.wrap() = object.geometry;
DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY);
}
ED_undo_object_set_active_or_warn(
*bmain, scene, view_layer, us->objects.first().obedit_ref.ptr, us_p->name, &LOG);
bmain->is_memfile_undo_flush_needed = true;
WM_event_add_notifier(C, NC_GEOM | ND_DATA, nullptr);
}
static void step_free(UndoStep *us_p)
{
CurvesUndoStep *us = reinterpret_cast<CurvesUndoStep *>(us_p);
us->objects.~Array();
}
static void foreach_ID_ref(UndoStep *us_p,
UndoTypeForEachIDRefFn foreach_ID_ref_fn,
void *user_data)
{
CurvesUndoStep *us = reinterpret_cast<CurvesUndoStep *>(us_p);
foreach_ID_ref_fn(user_data, (reinterpret_cast<UndoRefID *>(&us->scene_ref)));
for (const StepObject &object : us->objects) {
foreach_ID_ref_fn(
user_data,
(reinterpret_cast<UndoRefID *>(const_cast<UndoRefID_Object *>(&object.obedit_ref))));
}
}
/** \} */
} // namespace undo
void undosys_type_register(UndoType *ut)
{
ut->name = "Edit Curves";
ut->poll = editable_curves_in_edit_mode_poll;
ut->step_encode = undo::step_encode;
ut->step_decode = undo::step_decode;
ut->step_free = undo::step_free;
ut->step_foreach_ID_ref = undo::foreach_ID_ref;
ut->flags = UNDOTYPE_FLAG_NEED_CONTEXT_FOR_ENCODE;
ut->step_size = sizeof(undo::CurvesUndoStep);
}
} // namespace ed::curves
} // namespace blender

View File

@@ -0,0 +1,94 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_scene_types.h"
#include "BKE_context.hh"
#include "BKE_instances.hh"
#include "BKE_report.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "ED_curves.hh"
#include "ED_object.hh"
#include "GEO_realize_instances.hh"
namespace blender::ed::curves {
wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
Object *active_object = CTX_data_active_object(C);
BLI_assert(active_object);
BLI_assert(active_object->type == OB_CURVES);
Curves &active_curves = *id_cast<Curves *>(active_object->data);
const float4x4 &world_to_active = active_object->world_to_object();
Vector<Object *> objects{active_object};
bool active_object_selected = false;
CTX_DATA_BEGIN (C, Object *, object, selected_editable_objects) {
if (object == active_object) {
active_object_selected = true;
continue;
}
if (object->type != OB_CURVES) {
continue;
}
objects.append(object);
}
CTX_DATA_END;
if (!active_object_selected) {
BKE_report(op->reports, RPT_WARNING, "Active object is not a selected curves object");
return OPERATOR_CANCELLED;
}
bke::Instances instances;
instances.resize(objects.size());
MutableSpan<float4x4> transforms = instances.transforms_for_write();
MutableSpan<int> references = instances.reference_handles_for_write();
Map<const Curves *, int> reference_by_orig_curves;
for (const int i : objects.index_range()) {
transforms[i] = world_to_active * objects[i]->object_to_world();
const Curves *orig_curves = id_cast<const Curves *>(objects[i]->data);
references[i] = reference_by_orig_curves.lookup_or_add_cb(orig_curves, [&]() {
auto geometry = bke::GeometrySet::from_curves(BKE_curves_copy_for_eval(orig_curves));
return instances.add_new_reference(std::move(geometry));
});
}
bke::GeometrySet realized_geometry = geometry::realize_instances(
bke::GeometrySet::from_instances(
&instances, bke::GeometryOwnershipType::ReadOnly),
geometry::RealizeInstancesOptions())
.geometry;
if (!realized_geometry.has_curves()) {
BKE_report(op->reports, RPT_WARNING, "No curves data to join");
return OPERATOR_CANCELLED;
}
Curves *realized_curves = realized_geometry.get_curves_for_write();
active_curves.geometry.wrap() = std::move(realized_curves->geometry.wrap());
for (Object *object : objects.as_span().drop_front(1)) {
object::base_free_and_unlink(bmain, scene, object);
}
DEG_relations_tag_update(bmain);
DEG_id_tag_update(&active_object->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
DEG_id_tag_update(&scene->id, ID_RECALC_SELECT);
WM_event_add_notifier(C, NC_SCENE | ND_OB_ACTIVE, scene);
WM_event_add_notifier(C, NC_SCENE | ND_LAYER_CONTENT, scene);
return OPERATOR_FINISHED;
}
} // namespace blender::ed::curves

View File

@@ -0,0 +1,145 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_context.hh"
#include "BKE_curves.hh"
#include "BKE_layer.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "DEG_depsgraph.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "ED_curves.hh"
#include "ED_view3d.hh"
namespace blender::ed::curves {
struct ClosestCurveDataBlock {
Curves *curves_id = nullptr;
FindClosestData elem;
};
static ClosestCurveDataBlock find_closest_curve(const Depsgraph &depsgraph,
const ViewContext &vc,
const Span<Base *> bases,
const int2 &mval)
{
return threading::parallel_reduce(
bases.index_range(),
1L,
ClosestCurveDataBlock(),
[&](const IndexRange range, const ClosestCurveDataBlock &init) {
ClosestCurveDataBlock new_closest = init;
for (Base *base : bases.slice(range)) {
Object &curves_ob = *base->object;
Curves &curves_id = *id_cast<Curves *>(curves_ob.data);
bke::crazyspace::GeometryDeformation deformation =
bke::crazyspace::get_evaluated_curves_deformation(depsgraph, curves_ob);
const bke::CurvesGeometry &curves = curves_id.geometry.wrap();
const float4x4 projection = ED_view3d_ob_project_mat_get(vc.rv3d, &curves_ob);
foreach_selectable_curve_range(
curves,
deformation,
eHandleDisplay(vc.v3d->overlay.handle_display),
[&](IndexRange range, Span<float3> positions, StringRef /*selection_name*/) {
std::optional<FindClosestData> new_closest_elem = closest_elem_find_screen_space(
vc,
curves.points_by_curve(),
positions,
curves.cyclic(),
projection,
range,
bke::AttrDomain::Curve,
mval,
new_closest.elem);
if (new_closest_elem) {
new_closest.elem = *new_closest_elem;
new_closest.curves_id = &curves_id;
}
});
}
return new_closest;
},
[](const ClosestCurveDataBlock &a, const ClosestCurveDataBlock &b) {
return (a.elem.distance_sq < b.elem.distance_sq) ? a : b;
});
}
static bool select_linked_pick(bContext &C, const int2 &mval, const SelectPick_Params &params)
{
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(&C);
const ViewContext vc = ED_view3d_viewcontext_init(&C, depsgraph);
const Vector<Base *> bases = BKE_view_layer_array_from_bases_in_edit_mode_unique_data(
*vc.bmain, vc.scene, vc.view_layer, vc.v3d);
const ClosestCurveDataBlock closest = find_closest_curve(*depsgraph, vc, bases, mval);
if (!closest.curves_id) {
return false;
}
bke::CurvesGeometry &closest_curves = closest.curves_id->geometry.wrap();
const bke::AttrDomain selection_domain = bke::AttrDomain(closest.curves_id->selection_domain);
if (selection_domain == bke::AttrDomain::Point) {
const OffsetIndices points_by_curve = closest_curves.points_by_curve();
foreach_selection_attribute_writer(
closest_curves, bke::AttrDomain::Point, [&](bke::GSpanAttributeWriter &selection) {
for (const int point : points_by_curve[closest.elem.index]) {
apply_selection_operation_at_index(selection.span, point, params.sel_op);
}
});
}
else if (selection_domain == bke::AttrDomain::Curve) {
bke::GSpanAttributeWriter selection = ensure_selection_attribute(
closest_curves, bke::AttrDomain::Curve, bke::AttrType::Bool);
apply_selection_operation_at_index(selection.span, closest.elem.index, params.sel_op);
selection.finish();
}
/* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a
* generic attribute for now. */
DEG_id_tag_update(&closest.curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GEOM | ND_DATA, closest.curves_id);
return true;
}
static wmOperatorStatus select_linked_pick_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
{
SelectPick_Params params{};
params.sel_op = RNA_boolean_get(op->ptr, "deselect") ? SEL_OP_SUB : SEL_OP_ADD;
params.deselect_all = false;
params.select_passthrough = false;
if (!select_linked_pick(*C, event->mval, params)) {
return OPERATOR_CANCELLED;
}
return OPERATOR_FINISHED;
}
void CURVES_OT_select_linked_pick(wmOperatorType *ot)
{
ot->name = "Select Linked";
ot->idname = "CURVES_OT_select_linked_pick";
ot->description = "Select all points in the curve under the cursor";
ot->invoke = select_linked_pick_invoke;
ot->poll = editable_curves_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
RNA_def_boolean(ot->srna,
"deselect",
false,
"Deselect",
"Deselect linked control points rather than selecting them");
}
} // namespace blender::ed::curves

View File

@@ -0,0 +1,135 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_index_mask.hh"
#include "BLI_task.hh"
#include "BLI_vector_set.hh"
#include "BKE_context.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_curves.hh"
#include "ED_curves.hh"
#include "ED_object.hh"
#include "DNA_layer_types.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "GEO_curves_remove_and_split.hh"
#include "WM_api.hh"
namespace blender::ed::curves {
static wmOperatorStatus separate_exec(bContext *C, wmOperator * /*op*/)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
Vector<Base *> bases = BKE_view_layer_array_from_bases_in_edit_mode(
*bmain, scene, view_layer, CTX_wm_view3d(C));
VectorSet<Curves *> src_curves;
for (Base *base_src : bases) {
src_curves.add(id_cast<Curves *>(base_src->object->data));
}
/* Modify new curves and generate new curves in parallel. */
Array<std::optional<bke::CurvesGeometry>> dst_geometry(src_curves.size());
threading::parallel_for(dst_geometry.index_range(), 1, [&](const IndexRange range) {
for (const int i : range) {
Curves &src = *src_curves[i];
IndexMaskMemory memory;
switch (bke::AttrDomain(src.selection_domain)) {
case bke::AttrDomain::Point: {
const IndexMask selection = retrieve_selected_points(src, memory);
if (selection.is_empty()) {
continue;
}
bke::CurvesGeometry separated;
bke::CurvesGeometry retained;
separate_points(src.geometry.wrap(), selection, separated, retained);
separated.calculate_bezier_auto_handles();
retained.calculate_bezier_auto_handles();
dst_geometry[i] = std::move(separated);
src.geometry.wrap() = std::move(retained);
break;
}
case bke::AttrDomain::Curve: {
const IndexMask selection = retrieve_selected_curves(src, memory);
if (selection.is_empty()) {
continue;
}
dst_geometry[i] = bke::curves_copy_curve_selection(src.geometry.wrap(), selection, {});
src.geometry.wrap().remove_curves(selection, {});
break;
}
default:
BLI_assert_unreachable();
break;
}
}
});
/* Move new curves into main data-base. */
Array<Curves *> dst_curves(src_curves.size(), nullptr);
for (const int i : dst_curves.index_range()) {
if (std::optional<bke::CurvesGeometry> &dst = dst_geometry[i]) {
dst_curves[i] = BKE_curves_add(bmain, BKE_id_name(src_curves[i]->id));
dst_curves[i]->geometry.wrap() = std::move(*dst);
bke::curves_copy_parameters(*src_curves[i], *dst_curves[i]);
}
}
/* Skip processing objects with no selected elements. */
bases.remove_if([&](Base *base) {
Curves *curves = id_cast<Curves *>(base->object->data);
return dst_curves[src_curves.index_of(curves)] == nullptr;
});
if (bases.is_empty()) {
return OPERATOR_CANCELLED;
}
/* Add new objects for the new curves. */
for (Base *base_src : bases) {
Curves *src = id_cast<Curves *>(base_src->object->data);
Curves *dst = dst_curves[src_curves.index_of(src)];
Base *base_dst = object::add_duplicate(
bmain, scene, view_layer, base_src, eDupli_ID_Flags(U.dupflag) & USER_DUP_ACT);
Object *object_dst = base_dst->object;
object_dst->mode = OB_MODE_OBJECT;
object_dst->data = id_cast<ID *>(dst);
DEG_id_tag_update(&src->id, ID_RECALC_GEOMETRY);
DEG_id_tag_update(&dst->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_OBJECT | ND_DRAW, base_src->object);
WM_event_add_notifier(C, NC_OBJECT | ND_DRAW, object_dst);
}
DEG_relations_tag_update(bmain);
return OPERATOR_FINISHED;
}
void CURVES_OT_separate(wmOperatorType *ot)
{
ot->name = "Separate";
ot->idname = "CURVES_OT_separate";
ot->description = "Separate selected geometry into a new object";
ot->exec = separate_exec;
ot->poll = editable_curves_in_edit_mode_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
} // namespace blender::ed::curves

View File

@@ -0,0 +1,355 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*/
#include "ED_curves.hh"
#include "BKE_gtest_base.hh"
#include "testing/testing.h"
namespace blender::ed::curves::tests {
static bke::CurvesGeometry create_curves(const Span<Vector<float3>> all_positions,
const int order,
const Set<int> &is_cyclic)
{
Array<int> offsets(all_positions.size() + 1, 0);
for (const int curve : all_positions.index_range()) {
const Vector<float3> &curve_positions = all_positions[curve];
offsets[curve + 1] = offsets[curve] + curve_positions.size();
}
bke::CurvesGeometry curves(offsets.last(), all_positions.size());
curves.offsets_for_write().copy_from(offsets);
const OffsetIndices points_by_curve = curves.points_by_curve();
MutableSpan<float3> positions = curves.positions_for_write();
MutableSpan<bool> cyclic = curves.cyclic_for_write();
MutableSpan<int8_t> orders = curves.nurbs_orders_for_write();
for (const int curve : all_positions.index_range()) {
positions.slice(points_by_curve[curve]).copy_from(all_positions[curve]);
cyclic[curve] = is_cyclic.contains(curve);
orders[curve] = order;
}
curves.tag_topology_changed();
return curves;
}
static bke::CurvesGeometry create_curves(const Vector<float3> positions,
const int order,
const Set<int> &is_cyclic)
{
return create_curves(Span<Vector<float3>>(&positions, 1), order, is_cyclic);
}
static void validate_positions(const Span<Vector<float3>> expected_positions,
const OffsetIndices<int> points_by_curve,
const Span<float3> positions)
{
for (const int curve : expected_positions.index_range()) {
const Span<float3> expected_curve_positions = expected_positions[curve];
const IndexRange points = points_by_curve[curve];
for (const int point : expected_curve_positions.index_range()) {
EXPECT_EQ(positions[points[point]], expected_curve_positions[point]);
}
}
}
class CurvesEditorsTest : public bke::BlenderGTestBase {};
TEST_F(CurvesEditorsTest, DuplicatePointsTwoSingle)
{
/* Two points from single curve. */
const Vector<float3> expected_positions = {{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}};
bke::CurvesGeometry curves = create_curves(expected_positions, 4, {});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{1, 2}.as_span(), memory);
duplicate_points(curves, mask);
EXPECT_TRUE(curves.curves_num() == 2);
const Span<float3> positions = curves.positions();
for (const int point : expected_positions.index_range()) {
EXPECT_TRUE(positions[point] == expected_positions[point]);
}
EXPECT_TRUE(positions[4] == expected_positions[1]);
EXPECT_TRUE(positions[5] == expected_positions[2]);
}
TEST_F(CurvesEditorsTest, DuplicatePointsFourThree)
{
/* Four points from three curves. One curve has one point. */
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(expected_positions, 4, {});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{0, 1, 4, 9}.as_span(), memory);
duplicate_points(curves, mask);
EXPECT_TRUE(curves.curves_num() == expected_positions.size() + 3);
const Span<float3> positions = curves.positions();
const OffsetIndices points_by_curve = curves.points_by_curve();
for (const int curve : expected_positions.index_range()) {
const Span<float3> expected_curve_positions = expected_positions[curve];
const IndexRange points = points_by_curve[curve];
for (const int point : expected_curve_positions.index_range()) {
EXPECT_TRUE(positions[points[point]] == expected_curve_positions[point]);
}
}
EXPECT_TRUE(positions[10] == expected_positions[0][0]);
EXPECT_TRUE(positions[11] == expected_positions[0][1]);
EXPECT_TRUE(positions[12] == expected_positions[1][0]);
EXPECT_TRUE(positions[13] == expected_positions[2][4]);
}
TEST_F(CurvesEditorsTest, DuplicatePointsTwoCyclic)
{
/* Two points from cyclic curve. Points are on cycle. */
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(expected_positions, 4, {2});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{5, 8}.as_span(), memory);
duplicate_points(curves, mask);
EXPECT_TRUE(curves.curves_num() == expected_positions.size() + 1);
const Span<float3> positions = curves.positions();
const OffsetIndices points_by_curve = curves.points_by_curve();
for (const int curve : expected_positions.index_range()) {
const Span<float3> expected_curve_positions = expected_positions[curve];
const IndexRange points = points_by_curve[curve];
for (const int point : expected_curve_positions.index_range()) {
EXPECT_TRUE(positions[points[point]] == expected_curve_positions[point]);
}
}
EXPECT_TRUE(positions[14] == expected_positions[2][3]);
EXPECT_TRUE(positions[15] == expected_positions[2][0]);
}
TEST_F(CurvesEditorsTest, SplitPointsTwoSingle)
{
/* Split two points from single curve. */
const Vector<float3> positions = {{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}};
bke::CurvesGeometry curves = create_curves(positions, 4, {});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({1, 2}, memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1, 1, 0}, {1, 1, 0}}, {{-1.5, 0, 0}, {-1, 1, 0}}, {{1, 1, 0}, {1.5, 0, 0}}};
GTEST_ASSERT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
}
TEST_F(CurvesEditorsTest, SplitPointsFourThree)
{
/* Four points from three curves. One curve has one point. */
const Vector<Vector<float3>> positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{0, 1, 4, 9}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}},
{{-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, -1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
GTEST_ASSERT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
}
TEST_F(CurvesEditorsTest, SplitPointsTwoCyclic)
{
/* Two points from cyclic curve. Points are on cycle. */
const Vector<Vector<float3>> positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {2});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{5, 8}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{-1, 1, 0}, {1, 1, 0}},
{{1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
GTEST_ASSERT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
Array<bool> expected_cyclic = {false, false, false, false, false};
VArray<bool> cyclic = new_curves.cyclic();
for (const int i : expected_cyclic.index_range()) {
EXPECT_EQ(expected_cyclic[i], cyclic[i]);
}
}
TEST_F(CurvesEditorsTest, SplitPointsTwoTouchCyclic)
{
/* Two points from cyclic curve. Points are touching cycle. */
const Vector<Vector<float3>> positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {2});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{5, 6}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, 1, 0}, {1, -1, 0}},
{{1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}, {1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
GTEST_ASSERT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
}
TEST_F(CurvesEditorsTest, SplitEverySecondCyclic)
{
/* Split every second point in cyclic curve. Expected result all selected points
* as separate curves and original curve. */
const Vector<Vector<float3>> positions = {{{0, -1, 0},
{-1, -1, 0},
{-1, 0, 0},
{-1, 1, 0},
{0, 1, 0},
{1, 1, 0},
{1, 0, 0},
{1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {0});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{0, 2, 4, 6}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {{{0, -1, 0}},
{{-1, 0, 0}},
{{0, 1, 0}},
{{1, 0, 0}},
{{0, -1, 0},
{-1, -1, 0},
{-1, 0, 0},
{-1, 1, 0},
{0, 1, 0},
{1, 1, 0},
{1, 0, 0},
{1, -1, 0}}};
GTEST_ASSERT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
}
TEST_F(CurvesEditorsTest, SplitAllSelectedButFirstCyclic)
{
/* Split all except first points in cyclic curve. Expected result two curves. One from selected
* points another from first, second and last. Both not cyclic. */
const Vector<Vector<float3>> positions = {{{0, -1, 0},
{-1, -1, 0},
{-1, 0, 0},
{-1, 1, 0},
{0, 1, 0},
{1, 1, 0},
{1, 0, 0},
{1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {0});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{1, 2, 3, 4, 5, 6, 7}.as_span(),
memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1, -1, 0}, {-1, 0, 0}, {-1, 1, 0}, {0, 1, 0}, {1, 1, 0}, {1, 0, 0}, {1, -1, 0}},
{{1, -1, 0}, {0, -1, 0}, {-1, -1, 0}},
};
GTEST_ASSERT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
EXPECT_EQ(new_curves.curves_num(), expected_positions.size());
EXPECT_FALSE(new_curves.cyclic()[0]);
EXPECT_FALSE(new_curves.cyclic()[1]);
}
TEST_F(CurvesEditorsTest, SplitTwoOnSeamAndExtraCyclic)
{
/* Split first, last and pair in the middle. Expected result four non cyclic curves. */
const Vector<Vector<float3>> positions = {{{0, -1, 0},
{-1, -1, 0},
{-1, 0, 0},
{-1, 1, 0},
{0, 1, 0},
{1, 1, 0},
{1, 0, 0},
{1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {0});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{0, 3, 4, 7}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1, 1, 0}, {0, 1, 0}},
{{1, -1, 0}, {0, -1, 0}},
{{0, -1, 0}, {-1, -1, 0}, {-1, 0, 0}, {-1, 1, 0}},
{{0, 1, 0}, {1, 1, 0}, {1, 0, 0}, {1, -1, 0}}};
GTEST_ASSERT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
EXPECT_FALSE(new_curves.cyclic()[0]);
EXPECT_FALSE(new_curves.cyclic()[1]);
EXPECT_FALSE(new_curves.cyclic()[2]);
EXPECT_FALSE(new_curves.cyclic()[3]);
}
} // namespace blender::ed::curves::tests