Curves: Add extrude operator #116354

Merged
Hans Goudey merged 26 commits from laurynas/blender:curves-extrude-op into main 2024-01-10 17:27:04 +01:00
6 changed files with 382 additions and 0 deletions

View File

@ -6077,6 +6077,7 @@ def km_edit_curves(params):
("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}),
("curves.duplicate_move", {"type": 'D', "value": 'PRESS', "shift": True}, None),
*_template_items_select_actions(params, "curves.select_all"),
("curves.extrude_move", {"type": 'E', "value": 'PRESS'}, None),
("curves.select_linked", {"type": 'L', "value": 'PRESS', "ctrl": True}, None),
("curves.delete", {"type": 'X', "value": 'PRESS'}, None),
("curves.delete", {"type": 'DEL', "value": 'PRESS'}, None),

View File

@ -26,6 +26,7 @@ set(SRC
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_selection.cc

View File

@ -0,0 +1,357 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute.hh"
#include "BKE_context.hh"
#include "BKE_curves_utils.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "ED_curves.hh"
#include "DEG_depsgraph.hh"
laurynas marked this conversation as resolved Outdated

I'd suggest doing a "array of structs" to "struct of arrays" transformation and split this struct into separate arrays for each member. That would have a few benefits here:

  • Possibly easier use of existing abstractions like selection.foreach_range, OffsetIndices, array_utils::gather, etc.
  • Better performance since hot loops only read the data they'll actually use
  • Easier separation of concerns in code, i.e. only calculate one thing at a time
I'd suggest doing a "array of structs" to "struct of arrays" transformation and split this struct into separate arrays for each member. That would have a few benefits here: - Possibly easier use of existing abstractions like `selection.foreach_range`, `OffsetIndices`, `array_utils::gather`, etc. - Better performance since hot loops only read the data they'll actually use - Easier separation of concerns in code, i.e. only calculate one thing at a time

Well CurveCopy is refactored, but I'll have to think deeper regarding abstractions. Somehow I didn't notice selection.foreach_range exists, only it might change a lot.
By the way I was thinking about using OffsetIndices, but comment says "References an array of ascending indices."
Here I have "non decreasing indices". Will it work?

Well `CurveCopy` is refactored, but I'll have to think deeper regarding abstractions. Somehow I didn't notice `selection.foreach_range` exists, only it might change a lot. By the way I was thinking about using `OffsetIndices`, but comment says "References an array of ascending indices." Here I have "non decreasing indices". Will it work?

Not totally sure what the requirements are here, didn't look that in depth yet. But an array like 5, 5, 6, 8, 21, 21 will work; repeating offsets are okay, it just means that group is empty.

Not totally sure what the requirements are here, didn't look that in depth yet. But an array like `5, 5, 6, 8, 21, 21` will work; repeating offsets are okay, it just means that group is empty.
  1. I need to express ranges like [0, 2][2, 2][2, 4]. Ranges always share end points with a neighbor.
    Copied to destination indexes would result in [0, 1, 2, 2, 2, 3, 4].
    This would be extrusion of point 2 in 5 point curve [0, .., 4].
    Because of sharing neighbor I currently express this as [0, 2, 2, 4] and iterate with (i, i + 1).
    So for destination I see use of OffsetIndices, but for source indexes not sure.

  2. Ideally if to handle multiple curves in one data structure. Extrusion of points (2) and (7, 8) in curves [0, .., 4][5, .., 9] could look like:
    a. [0, 2][2, 2][2, 4][5, 7][7, 8][8, 9]. Here sharing endpoint rule is broken.
    b. [0, 2][2, 2][2, 7][7, 8][8, 9]. This would reduce copying count, but because of special cases is very hard to implement. Before I had a working version, I gave up on this after few days of trying.
    Any Thoughts?

1. I need to express ranges like `[0, 2][2, 2][2, 4]`. Ranges always share end points with a neighbor. Copied to destination indexes would result in `[0, 1, 2, 2, 2, 3, 4]`. This would be extrusion of point 2 in 5 point curve `[0, .., 4]`. Because of sharing neighbor I currently express this as `[0, 2, 2, 4]` and iterate with `(i, i + 1)`. So for destination I see use of `OffsetIndices`, but for source indexes not sure. 2. Ideally if to handle multiple curves in one data structure. Extrusion of points (2) and (7, 8) in curves `[0, .., 4][5, .., 9]` could look like: a. `[0, 2][2, 2][2, 4][5, 7][7, 8][8, 9]`. Here sharing endpoint rule is broken. b. `[0, 2][2, 2][2, 7][7, 8][8, 9]`. This would reduce copying count, but because of special cases is very hard to implement. Before I had a working version, I gave up on this after few days of trying. Any Thoughts?

Just checked code of OffsetIndices . It uses second index as exclusive, in this situation both inclusive needed.
OffsetIndices calculates IndexRange size by size = end - begin;, I need size = last - first + 1;.
Can't think of any smart way to restructure data to make use of OffsetIndices ...

Just checked code of `OffsetIndices `. It uses second index as exclusive, in this situation both inclusive needed. `OffsetIndices ` calculates `IndexRange` size by `size = end - begin;`, I need `size = last - first + 1;`. Can't think of any smart way to restructure data to make use of `OffsetIndices `...

That's fine, no need to force it! Thanks for taking a look though.

That's fine, no need to force it! Thanks for taking a look though.
namespace blender::ed::curves {
laurynas marked this conversation as resolved Outdated

This is getting more subjective now, and I think the PR is getting close. But my one larger remaining comment is that bundling all this data in a single struct (CurvesCopy) doesn't really help the situation. It may be clearer to declare each array as its needed in the caller. That gives a nice order of execution to the reader, and clarifies which parts of the algorithm need which data.

Sometimes that will make it easier to separate different parts of the algorithm into separate loops too. For example, maybe some data can be calculated in parallel while other data can't. Not sure if that applies here.

This is getting more subjective now, and I think the PR is getting close. But my one larger remaining comment is that bundling all this data in a single struct (`CurvesCopy`) doesn't really help the situation. It may be clearer to declare each array as its needed in the caller. That gives a nice order of execution to the reader, and clarifies which parts of the algorithm need which data. Sometimes that will make it easier to separate different parts of the algorithm into separate loops too. For example, maybe some data can be calculated in parallel while other data can't. Not sure if that applies here.
/**
* Merges copy intervals at curve endings to minimize number of copy operations.
* For example above intervals [0, 3, 4, 4, 4] became [0, 4, 4].
* Leading to only two copy operations.
*/
static Span<int> compress_intervals(const Span<IndexRange> curve_interval_ranges,
MutableSpan<int> intervals)
{
const int *src = intervals.data();
/* Skip the first curve, as all the data stays in the same place. */
int *dst = intervals.data() + curve_interval_ranges[0].size();
for (const int curve : IndexRange(1, curve_interval_ranges.size() - 1)) {
const IndexRange range = curve_interval_ranges[curve];
const int width = range.size() - 1;
std::copy_n(src + range.first() + 1, width, dst);
dst += width;
}
(*dst) = src[curve_interval_ranges[curve_interval_ranges.size() - 1].last() + 1];
return {intervals.data(), dst - intervals.data() + 1};
}
/**
* Creates copy intervals for selection #range in the context of #curve_index.
* If part of the #range is outside given curve, slices it and returns false indicating remaining
* still needs to be handled. If whole #range was handled returns true.
*/
static bool handle_range(const int curve_index,
const int interval_offset,
const Span<int> offsets,
int &current_interval,
IndexRange &range,
MutableSpan<int> curve_intervals,
MutableSpan<bool> is_first_selected)
{
const int first_elem = offsets[curve_index];
const int last_elem = offsets[curve_index + 1] - 1;
if (current_interval == 0) {
is_first_selected[curve_index] = range.first() == first_elem && range.size() == 1;
if (!is_first_selected[curve_index]) {
current_interval++;
}
}
curve_intervals[interval_offset + current_interval] = range.first();
current_interval++;
bool inside_curve = last_elem >= range.last();
if (inside_curve) {
curve_intervals[interval_offset + current_interval] = range.last();
}
else {
curve_intervals[interval_offset + current_interval] = last_elem;
range = IndexRange(last_elem + 1, range.last() - last_elem);
}
current_interval++;
laurynas marked this conversation as resolved Outdated

Pass Span by value, otherwise it's like a pointer to a pointer. And IndexMask is passed by const reference :P

Pass `Span` by value, otherwise it's like a pointer to a pointer. And `IndexMask` is passed by const reference :P

I don't have a good explanation for how it happened :)

I don't have a good explanation for how it happened :)
return inside_curve;
}
/**
* Calculates number of points in resulting curve denoted by #curve_index and sets it's
* #curve_offsets value.
*/
static void calc_curve_offset(const int curve_index,
int &interval_offset,
const Span<int> offsets,
MutableSpan<int> new_offsets,
MutableSpan<IndexRange> curve_interval_ranges)
{
const int points_in_curve = (offsets[curve_index + 1] - offsets[curve_index] +
curve_interval_ranges[curve_index].size() - 1);
new_offsets[curve_index + 1] = new_offsets[curve_index] + points_in_curve;
interval_offset += curve_interval_ranges[curve_index].size() + 1;
}
static void finish_curve(int &curve_index,
int &interval_offset,
int last_interval,
int last_elem,
const Span<int> offsets,
MutableSpan<int> new_offsets,
MutableSpan<int> curve_intervals,
MutableSpan<IndexRange> curve_interval_ranges,
MutableSpan<bool> is_first_selected)
{
if (curve_intervals[interval_offset + last_interval] != last_elem ||
curve_intervals[interval_offset + last_interval - 1] !=
curve_intervals[interval_offset + last_interval])
{
/* Append last element of the current curve if it is not extruded or extruded together with
* preceding points. */
last_interval++;
curve_intervals[interval_offset + last_interval] = last_elem;
}
else if (is_first_selected[curve_index] && last_interval == 1) {
/* Extrusion from one point. */
curve_intervals[interval_offset + last_interval + 1] =
curve_intervals[interval_offset + last_interval];
is_first_selected[curve_index] = false;
last_interval++;
}
curve_interval_ranges[curve_index] = IndexRange(interval_offset, last_interval);
calc_curve_offset(curve_index, interval_offset, offsets, new_offsets, curve_interval_ranges);
curve_index++;
}
static void finish_curve_or_full_copy(int &curve_index,
laurynas marked this conversation as resolved Outdated

Could you expand ins to a name that helps explain the code to the reader? Generally this sort of contraction makes this sort of thing more complex

Could you expand `ins` to a name that helps explain the code to the reader? Generally this sort of contraction makes this sort of thing more complex
int &interval_offset,
int current_interval,
const std::optional<IndexRange> prev_range,
const Span<int> offsets,
MutableSpan<int> new_offsets,
MutableSpan<int> curve_intervals,
MutableSpan<IndexRange> curve_interval_ranges,
MutableSpan<bool> is_first_selected)
{
const int last = offsets[curve_index + 1] - 1;
if (prev_range.has_value() && prev_range.value().last() >= offsets[curve_index]) {
finish_curve(curve_index,
interval_offset,
current_interval - 1,
last,
offsets,
new_offsets,
curve_intervals,
curve_interval_ranges,
is_first_selected);
}
else {
laurynas marked this conversation as resolved Outdated

Flip the condition and un-indent the rest of the function, same below with the empty selection check

Flip the condition and un-indent the rest of the function, same below with the empty selection check

Add static to functions that aren't exposed to the header

Add `static` to functions that aren't exposed to the header
/* Copy full curve if previous selected point vas not on this curve. */
const int first = offsets[curve_index];
laurynas marked this conversation as resolved Outdated

It's not totally clear what "shallow copy" means here. Usually I'd imagine that copying some data structure but not any data it owns itself.

It's not totally clear what "shallow copy" means here. Usually I'd imagine that copying some data structure but not any data it owns itself.

I meant here: copy curve as is or copy curve without modifications. Maybe finish_curve_or_full_copy ?

I meant here: copy curve as is or copy curve without modifications. Maybe finish_curve_or_full_copy ?

Full copy sounds good!

Full copy sounds good!
curve_interval_ranges[curve_index] = IndexRange(interval_offset, 1);
is_first_selected[curve_index] = false;
curve_intervals[interval_offset] = first;
curve_intervals[interval_offset + 1] = last;
calc_curve_offset(curve_index, interval_offset, offsets, new_offsets, curve_interval_ranges);
curve_index++;
laurynas marked this conversation as resolved Outdated

Use Array rather than manual allocations

Use `Array` rather than manual allocations
}
}
static void calc_curves_extrusion(const IndexMask &selection,
laurynas marked this conversation as resolved Outdated

Comment style

Comment style
const Span<int> offsets,
MutableSpan<int> new_offsets,
MutableSpan<int> curve_intervals,
MutableSpan<IndexRange> curve_interval_ranges,
MutableSpan<bool> is_first_selected)
{
std::optional<IndexRange> prev_range;
int current_interval = 0;
laurynas marked this conversation as resolved Outdated

Unused types variables here

Unused types variables here
int curve_index = 0;
int interval_offset = 0;
laurynas marked this conversation as resolved Outdated

Let's use the attribute API fully, without CustomData. That might require a bit more boilerplate, but that's easy to clean up in the future, and we have a high level goal of replacing CustomData storage internally

Let's use the attribute API fully, without `CustomData`. That might require a bit more boilerplate, but that's easy to clean up in the future, and we have a high level goal of replacing `CustomData` storage internally
curve_intervals[interval_offset] = offsets[0];
new_offsets[0] = offsets[0];
selection.foreach_range([&](const IndexRange range) {
/* Beginning of the range outside current curve. */
if (range.first() > offsets[curve_index + 1] - 1) {
do {
finish_curve_or_full_copy(curve_index,
interval_offset,
current_interval,
prev_range,
offsets,
laurynas marked this conversation as resolved Outdated

I guess old_attributes could be a bke::AttributeAccessor since those values don't have to be changed?

I guess `old_attributes` could be a `bke::AttributeAccessor` since those values don't have to be changed?

Couldn't find how to do attribute value copying from bke::AttributeAccessor to MutableAttributeAccessor in chunks.
Like with type.copy_assign_n(src.data(), dst.data(), dst.size());

Couldn't find how to do attribute value copying from `bke::AttributeAccessor` to `MutableAttributeAccessor` in chunks. Like with `type.copy_assign_n(src.data(), dst.data(), dst.size());`

You can use const GVArraySpan src_data = *src.lookup(name); to make sure you have a span of source data

You can use `const GVArraySpan src_data = *src.lookup(name);` to make sure you have a span of source data
new_offsets,
curve_intervals,
curve_interval_ranges,
is_first_selected);
} while (range.first() > offsets[curve_index + 1] - 1);
current_interval = 0;
curve_intervals[interval_offset] = offsets[curve_index];
}
IndexRange range_to_handle = range;
while (!handle_range(curve_index,
interval_offset,
offsets,
current_interval,
laurynas marked this conversation as resolved Outdated

Usually these algorithms are structured to only iterate over all the attributes once. So the first part will build mappings from input data to result data. In this case, that might be a combination of instructions for copying contiguous sections of data, and some data for which indices the copied points are from.

That structure can significantly improve performance, since it works much better with the cache of CPUs, since we access less data at once. It might also help separate concerns too.

Usually these algorithms are structured to only iterate over all the attributes once. So the first part will build mappings from input data to result data. In this case, that might be a combination of instructions for copying contiguous sections of data, and some data for which indices the copied points are from. That structure can significantly improve performance, since it works much better with the cache of CPUs, since we access less data at once. It might also help separate concerns too.
range_to_handle,
curve_intervals,
is_first_selected))
{
finish_curve(curve_index,
interval_offset,
current_interval - 1,
offsets[curve_index + 1] - 1,
offsets,
new_offsets,
curve_intervals,
curve_interval_ranges,
is_first_selected);
current_interval = 0;
curve_intervals[interval_offset] = offsets[curve_index];
laurynas marked this conversation as resolved Outdated

The bke::curves::copy_only_curve_domain utility in BKE_curves_utils.hh takes care of creating new curves and copying over the curve domain attributes for you.

The `bke::curves::copy_only_curve_domain` utility in `BKE_curves_utils.hh` takes care of creating new curves and copying over the curve domain attributes for you.
}
prev_range = range;
});
do {
finish_curve_or_full_copy(curve_index,
laurynas marked this conversation as resolved Outdated

Using a Span still makes sense here probably, rather than a raw pointer

Using a Span still makes sense here probably, rather than a raw pointer
interval_offset,
current_interval,
laurynas marked this conversation as resolved Outdated

Just realized this should handle multi-object edit mode too. There's an example of that in CURVES_OT_delete, or other operators around there.

Just realized this should handle multi-object edit mode too. There's an example of that in `CURVES_OT_delete`, or other operators around there.
prev_range,
laurynas marked this conversation as resolved Outdated

I think leaving ot->description blank will give some warning somewhere in the future

I think leaving `ot->description` blank will give some warning somewhere in the future
offsets,
new_offsets,
curve_intervals,
curve_interval_ranges,
is_first_selected);
prev_range.reset();
} while (curve_index < offsets.size() - 1);
}
static void extrude_curves(Curves &curves_id)
laurynas marked this conversation as resolved Outdated

Looks like this can be a const pointer, since the new curves are created separately

Looks like this can be a const pointer, since the new curves are created separately
{
const bke::AttrDomain selection_domain = bke::AttrDomain(curves_id.selection_domain);
if (selection_domain != bke::AttrDomain::Point) {
return;
}
IndexMaskMemory memory;
const IndexMask extruded_points = retrieve_selected_points(curves_id, memory);
if (extruded_points.is_empty()) {
return;
}
laurynas marked this conversation as resolved Outdated

If you create the new curves before calc_curves_extrusion, you can copy the new offsets directly into the new curves' offsets array. If you don't know the number of points at that point, you can resize just the point domain after.

If you create the new curves before `calc_curves_extrusion`, you can copy the new offsets directly into the new curves' offsets array. If you don't know the number of points at that point, you can resize just the point domain after.
const bke::CurvesGeometry &curves = curves_id.geometry.wrap();
const Span<int> old_offsets = curves.offsets();
bke::CurvesGeometry new_curves = bke::curves::copy_only_curve_domain(curves);
const int curves_num = curves.curves_num();
const int curve_intervals_size = extruded_points.size() * 2 + curves_num * 2;
laurynas marked this conversation as resolved Outdated

We know the domain is bke::AttrDomain::Point at this point.

I noticed that this won't copy the float vs. bool status of the input selection though. Since these curves are completely new, it will always create a boolean selection.

It would be pretty fancy to be able to copy the float values from the input curves as well. But if you don't feel inspired to do that, it's not a big deal either :)

We know the domain is `bke::AttrDomain::Point` at this point. I noticed that this won't copy the float vs. bool status of the input selection though. Since these curves are completely new, it will always create a boolean selection. It would be pretty fancy to be able to copy the float values from the input curves as well. But if you don't feel inspired to do that, it's not a big deal either :)

Don't know why selection is made with two possible types, but extrude shouldn't change the type.

Don't know why selection is made with two possible types, but extrude shouldn't change the type.

In sculpt mode there's "soft" selection which uses floats. Any value greater than zero is considered selected in edit mode.

In sculpt mode there's "soft" selection which uses floats. Any value greater than zero is considered selected in edit mode.

I was going to ask if other values besides 1.0f are possible, but decided that this can not be :)
Good that you told this.

I was going to ask if other values besides 1.0f are possible, but decided that this can not be :) Good that you told this.
laurynas marked this conversation as resolved Outdated

This variable could be declared here, and could have a better name besides d that reflected what it was doing

This variable could be declared here, and could have a better name besides `d` that reflected what it was doing
new_curves.resize(0, curves_num);
MutableSpan<int> new_offsets = new_curves.offsets_for_write();
laurynas marked this conversation as resolved Outdated

c -> curve

`c` -> `curve`
/* 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> curve_intervals(curve_intervals_size);
/* Points to intervals for each curve in the curve_intervals array.
* For example above value would be [{0, 1}, {2, 2}] */
Array<IndexRange> curve_interval_ranges(curves_num);
/* Per curve boolean indicating if first interval in a curve is selected.
* Other can be calculated as in a curve two adjacent intervals can have same selection state. */
Array<bool> is_first_selected(curves_num);
calc_curves_extrusion(extruded_points,
old_offsets,
new_offsets,
curve_intervals,
curve_interval_ranges,
is_first_selected);
new_curves.resize(new_offsets.last(), new_curves.curves_num());
const bke::AttributeAccessor src_attributes = curves.attributes();
const GVArraySpan src_selection = *src_attributes.lookup(".selection", bke::AttrDomain::Point);
const CPPType &src_selection_type = src_selection.type();
bke::GSpanAttributeWriter dst_selection = ensure_selection_attribute(
new_curves,
bke::AttrDomain::Point,
src_selection_type.is<bool>() ? CD_PROP_BOOL : CD_PROP_FLOAT);
threading::parallel_for(curves.curves_range(), 256, [&](IndexRange curves_range) {
for (const int curve : curves_range) {
const int first_index = curve_interval_ranges[curve].start();
const int first_value = curve_intervals[first_index];
bool is_selected = is_first_selected[curve];
laurynas marked this conversation as resolved Outdated

This should be unnecessary, since copy_only_curve_domain copies this info

This should be unnecessary, since `copy_only_curve_domain` copies this info
for (const int i : curve_interval_ranges[curve]) {
const int dest_index = new_offsets[curve] + curve_intervals[i] - first_value + i -
laurynas marked this conversation as resolved Outdated

std::move(new_curves) to avoid a copy

`std::move(new_curves)` to avoid a copy
first_index;
const int size = curve_intervals[i + 1] - curve_intervals[i] + 1;
GMutableSpan dst_span = dst_selection.span.slice(IndexRange(dest_index, size));
if (is_selected) {
src_selection_type.copy_assign_n(
src_selection.slice(IndexRange(curve_intervals[i], size)).data(),
dst_span.data(),
size);
}
else {
fill_selection(dst_span, false);
}
is_selected = !is_selected;
}
}
});
dst_selection.finish();
const Span<int> intervals = compress_intervals(curve_interval_ranges, curve_intervals);
bke::MutableAttributeAccessor dst_attributes = new_curves.attributes_for_write();
for (auto &attribute : bke::retrieve_attributes_for_transfer(
src_attributes, dst_attributes, ATTR_DOMAIN_MASK_POINT, {}, {".selection"}))
{
const CPPType &type = attribute.src.type();
threading::parallel_for(IndexRange(intervals.size() - 1), 512, [&](IndexRange range) {
for (const int i : range) {
const int first = intervals[i];
const int size = intervals[i + 1] - first + 1;
const int dest_index = intervals[i] + i;
type.copy_assign_n(attribute.src.slice(IndexRange(first, size)).data(),
attribute.dst.span.slice(IndexRange(dest_index, size)).data(),
size);
}
});
attribute.dst.finish();
}
curves_id.geometry.wrap() = std::move(new_curves);
DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY);
}
static int curves_extrude_exec(bContext *C, wmOperator * /*op*/)
{
for (Curves *curves_id : get_unique_editable_curves(*C)) {
extrude_curves(*curves_id);
}
return OPERATOR_FINISHED;
}
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

@ -1332,6 +1332,7 @@ void ED_operatortypes_curves()
WM_operatortype_append(CURVES_OT_convert_to_particle_system);
WM_operatortype_append(CURVES_OT_convert_from_particle_system);
WM_operatortype_append(CURVES_OT_draw);
WM_operatortype_append(CURVES_OT_extrude);
WM_operatortype_append(CURVES_OT_snap_curves_to_surface);
WM_operatortype_append(CURVES_OT_set_selection_domain);
WM_operatortype_append(CURVES_OT_select_all);
@ -1360,6 +1361,16 @@ void ED_operatormacros_curves()
otmacro = WM_operatortype_macro_define(ot, "TRANSFORM_OT_translate");
RNA_boolean_set(otmacro->ptr, "use_proportional_edit", false);
RNA_boolean_set(otmacro->ptr, "mirror", false);
/* Extrude + Move */
ot = WM_operatortype_append_macro("CURVES_OT_extrude_move",
"Extrude Curve and Move",
"Extrude curve and move result",
OPTYPE_UNDO | OPTYPE_REGISTER);
WM_operatortype_macro_define(ot, "CURVES_OT_extrude");
otmacro = WM_operatortype_macro_define(ot, "TRANSFORM_OT_translate");
RNA_boolean_set(otmacro->ptr, "use_proportional_edit", false);
RNA_boolean_set(otmacro->ptr, "mirror", false);
}
void ED_keymap_curves(wmKeyConfig *keyconf)

View File

@ -128,6 +128,16 @@ void fill_selection_true(GMutableSpan selection)
}
}
void fill_selection(GMutableSpan selection, bool value)
{
if (selection.type().is<bool>()) {
selection.typed<bool>().fill(value);
}
else if (selection.type().is<float>()) {
selection.typed<float>().fill(value ? 1.0f : 0.0f);
}
}
void fill_selection_false(GMutableSpan selection, const IndexMask &mask)
{
if (selection.type().is<bool>()) {

View File

@ -80,6 +80,7 @@ bool curves_poll(bContext *C);
void CURVES_OT_attribute_set(wmOperatorType *ot);
void CURVES_OT_draw(wmOperatorType *ot);
void CURVES_OT_extrude(wmOperatorType *ot);
/** \} */
@ -143,6 +144,7 @@ IndexMask random_mask(const bke::CurvesGeometry &curves,
void fill_selection_false(GMutableSpan span);
void fill_selection_true(GMutableSpan span);
void fill_selection(GMutableSpan selection, bool value);
void fill_selection_false(GMutableSpan selection, const IndexMask &mask);
void fill_selection_true(GMutableSpan selection, const IndexMask &mask);