GPv3: Soft mode for the Eraser tool #110310

Open
Amélie Fondevilla wants to merge 57 commits from amelief/blender:gpv3-erase-operator-soft-mode into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
3 changed files with 477 additions and 59 deletions

View File

@ -80,6 +80,8 @@ class UnifiedPaintPanel:
return tool_settings.gpencil_weight_paint
elif mode == 'VERTEX_GPENCIL':
return tool_settings.gpencil_vertex_paint
elif mode == 'PAINT_GREASE_PENCIL':
return tool_settings.gpencil_paint
elif mode == 'SCULPT_CURVES':
return tool_settings.curves_sculpt
return None

View File

@ -1750,8 +1750,25 @@ class _defs_paint_grease_pencil:
if not brush:
return
layout.prop(brush.gpencil_settings, "eraser_mode", expand=True)
if brush.gpencil_settings.eraser_mode == "HARD":
if brush.gpencil_settings.eraser_mode == "SOFT":
from bl_ui.properties_paint_common import UnifiedPaintPanel, FalloffPanel
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
pressure_name="use_pressure_strength",
unified_name="use_unified_strength",
slider=True,
header=True,
)
layout.popover("VIEW3D_PT_tools_brush_falloff")
if brush.gpencil_settings.eraser_mode in {"HARD", "SOFT"}:
layout.prop(brush.gpencil_settings, "use_keep_caps_eraser")
layout.prop(brush.gpencil_settings, "use_active_layer_only")
return dict(
idname="builtin_brush.Erase",

View File

@ -23,6 +23,7 @@
#include "DEG_depsgraph_query.h"
#include "DNA_brush_enums.h"
#include "ED_grease_pencil.hh"
#include "ED_view3d.hh"
#include "WM_api.hh"
@ -43,8 +44,11 @@ class EraseOperation : public GreasePencilStrokeOperation {
bool keep_caps = false;
float radius = 50.0f;
float strength = 0.1f;
eGP_BrushEraserMode eraser_mode = GP_BRUSH_ERASER_HARD;
bool active_layer_only = false;
Set<GreasePencilDrawing *> affected_drawings;
};
/**
@ -55,12 +59,24 @@ struct EraseOperationExecutor {
float2 mouse_position{};
float eraser_radius{};
float eraser_strength{};
Brush *brush_{};
int2 mouse_position_pixels{};
int64_t eraser_squared_radius_pixels{};
/* Threshold below which points are considered as transparent and thus shall be removed. */
static constexpr float opacity_threshold = 0.05f;
EraseOperationExecutor(const bContext & /*C*/) {}
struct EraserRing {
float radius;
int64_t squared_radius;
float opacity;
bool hard_erase{false};
};
/**
* Computes the intersections between a 2D line segment and a circle with integer values.
*
@ -114,13 +130,17 @@ struct EraseOperationExecutor {
}
struct SegmentCircleIntersection {
/* Position of the intersection in the segment.*/
float factor = -1.0f;
/* Position of the intersection in the segment.
* Note: we use a value > 1.0f as initial value so that sorting intersection by increasing
* factor can directly put the invalid ones at the end. */
float factor = 2.0f;
/* True if the intersection corresponds to an inside/outside transition with respect to the
* circle, false if it corresponds to an outside/inside transition . */
bool inside_outside_intersection = false;
int ring_index = -1;
/* An intersection is considered valid if it lies inside of the segment, i.e. if its factor is
* in (0,1)*/
bool is_valid() const
@ -264,11 +284,12 @@ struct EraseOperationExecutor {
int curves_intersections_and_points_sides(
const bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
const int intersections_max_per_segment,
MutableSpan<PointCircleSide> r_point_side,
const Span<EraserRing> rings,
MutableSpan<std::pair<int, PointCircleSide>> r_point_ring,
MutableSpan<SegmentCircleIntersection> r_intersections) const
{
/* Each ring can generate zero, one or two intersections per segment. */
const int intersections_max_per_segment = rings.size() * 2;
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
const VArray<bool> src_cyclic = src.cyclic();
@ -287,41 +308,78 @@ struct EraseOperationExecutor {
if (src_curve_points.size() == 1) {
/* One-point stroke : just check if the point is inside the eraser. */
const int src_point = src_curve_points.first();
const int64_t squared_distance = math::distance_squared(
this->mouse_position_pixels, screen_space_positions_pixel[src_point]);
int ring_index = 0;
for (const EraserRing &eraser_point : rings) {
const int src_point = src_curve_points.first();
const int64_t squared_distance = math::distance_squared(
this->mouse_position_pixels, screen_space_positions_pixel[src_point]);
/* Note: We don't account for boundaries here, since we are not going to split any
* curve. */
r_point_side[src_point] = (squared_distance <= this->eraser_squared_radius_pixels) ?
PointCircleSide::Inside :
PointCircleSide::Outside;
/* Note: We don't account for boundaries here, since we are not going to split any
* curve. */
if ((r_point_ring[src_point].first == -1) &&
(squared_distance <= eraser_point.squared_radius)) {
r_point_ring[src_point] = {ring_index, PointCircleSide::Inside};
}
++ring_index;
}
continue;
}
for (const int src_point : src_curve_points.drop_back(1)) {
SegmentCircleIntersection inter0;
SegmentCircleIntersection inter1;
int ring_index = 0;
int intersection_offset = src_point * intersections_max_per_segment - 1;
const int8_t nb_inter = segment_intersections_and_points_sides(
screen_space_positions_pixel[src_point],
screen_space_positions_pixel[src_point + 1],
this->eraser_squared_radius_pixels,
inter0.factor,
inter1.factor,
r_point_side[src_point],
r_point_side[src_point + 1]);
for (const EraserRing &eraser_point : rings) {
SegmentCircleIntersection inter0;
SegmentCircleIntersection inter1;
if (nb_inter > 0) {
const int intersection_offset = src_point * intersections_max_per_segment;
inter0.ring_index = ring_index;
inter1.ring_index = ring_index;
inter0.inside_outside_intersection = (inter0.factor > inter1.factor);
r_intersections[intersection_offset + 0] = inter0;
PointCircleSide point_side;
PointCircleSide point_after_side;
if (nb_inter > 1) {
inter1.inside_outside_intersection = true;
r_intersections[intersection_offset + 1] = inter1;
const int8_t nb_inter = segment_intersections_and_points_sides(
screen_space_positions_pixel[src_point],
screen_space_positions_pixel[src_point + 1],
eraser_point.squared_radius,
inter0.factor,
inter1.factor,
point_side,
point_after_side);
/* The point belongs in the ring of the smallest radius circle it is in.
* Since our rings are stored in increasing radius order, it corresponds to the first
* ring that contains the point. We only include the InsideOutside boundary of the
* ring, that is why we do not check for OutsideInsideBoundary.
*/
if ((r_point_ring[src_point].first == -1) &&
ELEM(point_side, PointCircleSide::Inside, PointCircleSide::InsideOutsideBoundary))
{
r_point_ring[src_point] = {ring_index, point_side};
}
if (src_point + 1 == src_curve_points.last()) {
if ((r_point_ring[src_point + 1].first == -1) &&
ELEM(point_after_side,
PointCircleSide::Inside,
PointCircleSide::InsideOutsideBoundary))
{
r_point_ring[src_point + 1] = {ring_index, point_after_side};
}
}
if (nb_inter > 0) {
inter0.inside_outside_intersection = (inter0.factor > inter1.factor);
r_intersections[++intersection_offset] = inter0;
if (nb_inter > 1) {
inter1.inside_outside_intersection = true;
r_intersections[++intersection_offset] = inter1;
}
}
++ring_index;
}
}
@ -329,29 +387,42 @@ struct EraseOperationExecutor {
/* If the curve is cyclic, we need to check for the closing segment. */
const int src_last_point = src_curve_points.last();
const int src_first_point = src_curve_points.first();
int ring_index = 0;
int intersection_offset = src_last_point * intersections_max_per_segment - 1;
SegmentCircleIntersection inter0;
SegmentCircleIntersection inter1;
for (const EraserRing &eraser_point : rings) {
SegmentCircleIntersection inter0;
SegmentCircleIntersection inter1;
const int8_t nb_inter = segment_intersections_and_points_sides(
screen_space_positions_pixel[src_last_point],
screen_space_positions_pixel[src_first_point],
this->eraser_squared_radius_pixels,
inter0.factor,
inter1.factor,
r_point_side[src_last_point],
r_point_side[src_first_point]);
inter0.ring_index = ring_index;
inter1.ring_index = ring_index;
if (nb_inter > 0) {
const int intersection_offset = src_last_point * intersections_max_per_segment;
PointCircleSide point_side;
PointCircleSide point_after_side;
inter0.inside_outside_intersection = (inter0.factor > inter1.factor);
r_intersections[intersection_offset + 0] = inter0;
const int8_t nb_inter = segment_intersections_and_points_sides(
screen_space_positions_pixel[src_last_point],
screen_space_positions_pixel[src_first_point],
eraser_point.squared_radius,
inter0.factor,
inter1.factor,
point_side,
point_after_side);
if (nb_inter > 1) {
inter1.inside_outside_intersection = true;
r_intersections[intersection_offset + 1] = inter1;
/* Note : we don't need to set the point side here, since it was already set by the
* former loop. */
if (nb_inter > 0) {
inter0.inside_outside_intersection = (inter0.factor > inter1.factor);
r_intersections[++intersection_offset] = inter0;
if (nb_inter > 1) {
inter1.inside_outside_intersection = true;
r_intersections[++intersection_offset] = inter1;
}
}
++ring_index;
}
}
}
@ -384,6 +455,12 @@ struct EraseOperationExecutor {
float factor;
bool is_src_point;
bool is_cut;
/* Additional attributes changes that can be stored to be used after a call to
* compute_topology_change.
* Note that they won't be automatically updated in the destination's attributes.
*/
float opacity;
};
/**
@ -611,17 +688,17 @@ struct EraseOperationExecutor {
/* For the hard erase, we compute with a circle, so there can only be a maximum of two
* intersection per segment. */
const int intersections_max_per_segment = 2;
const Vector<EraserRing> eraser_rings(
1, {this->eraser_radius, this->eraser_squared_radius_pixels, 0.0f, true});
const int intersections_max_per_segment = eraser_rings.size() * 2;
/* Compute intersections between the eraser and the curves in the source domain. */
Array<PointCircleSide> src_point_side(src_points_num, PointCircleSide::Outside);
Array<std::pair<int, PointCircleSide>> src_point_ring(src_points_num,
{-1, PointCircleSide::Outside});
Array<SegmentCircleIntersection> src_intersections(src_points_num *
intersections_max_per_segment);
curves_intersections_and_points_sides(src,
screen_space_positions,
intersections_max_per_segment,
src_point_side,
src_intersections);
curves_intersections_and_points_sides(
src, screen_space_positions, eraser_rings, src_point_ring, src_intersections);
Array<Vector<PointTransferData>> src_to_dst_points(src_points_num);
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
@ -632,7 +709,7 @@ struct EraseOperationExecutor {
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
const int src_next_point = (src_point == src_points.last()) ? src_points.first() :
(src_point + 1);
const PointCircleSide point_side = src_point_side[src_point];
const PointCircleSide point_side = src_point_ring[src_point].second;
/* Add the source point only if it does not lie inside of the eraser. */
if (point_side != PointCircleSide::Inside) {
@ -667,6 +744,265 @@ struct EraseOperationExecutor {
return true;
}
Vector<EraserRing> compute_piecewise_linear_falloff() const
{
/* The changes in opacity implied by the soft eraser are described by a falloff curve
* mapping. Abscissa of the curve is the normalized distance to the brush, and ordinate of
* the curve is the strength of the eraser.
*
* To apply this falloff as precisely as possible, we compute a set of "rings" to the brush,
* meaning a set of samples in the curve mapping in between which the strength of the eraser
* is applied linearly.
*
* In other words, we compute a minimal set of samples that describe the falloff curve as a
* polyline. */
/* First, distance-based sampling with a small pixel distance.
* The samples are stored in increasing radius order. */
const int step_pixels = 2;
int nb_samples = round_fl_to_int(this->eraser_radius / step_pixels);
Vector<EraserRing> eraser_rings(nb_samples);
for (const int sample_index : eraser_rings.index_range()) {
const int64_t sampled_distance = (sample_index + 1) * step_pixels;
EraserRing &ring = eraser_rings[sample_index];
ring.radius = sampled_distance;
ring.squared_radius = sampled_distance * sampled_distance;
ring.opacity = 1.0 - this->eraser_strength *
BKE_brush_curve_strength(
this->brush_, float(sampled_distance), this->eraser_radius);
}
/* Then, prune samples that are under the opacity threshold. */
Array<bool> prune_sample(nb_samples, false);
for (const int sample_index : eraser_rings.index_range()) {
EraserRing &sample = eraser_rings[sample_index];
if (sample_index == nb_samples - 1) {
/* If this is the last samples, we need to keep it at the same position (it corresponds
* to the brush overall radius). It is a cut if the opacity is under the threshold.*/
sample.hard_erase = (sample.opacity < opacity_threshold);
continue;
}
EraserRing next_sample = eraser_rings[sample_index + 1];
/* If both samples are under the threshold, prune it !
* If none of them are under the threshold, leave them as they are.
*/
if ((sample.opacity < opacity_threshold) == (next_sample.opacity < opacity_threshold)) {
prune_sample[sample_index] = (sample.opacity < opacity_threshold);
continue;
}
/* Otherwise, shift the sample to the spot where the opacity is exactly at the threshold.
* This way we don't remove larger opacity values in-between the samples. */
const EraserRing &sample_after = eraser_rings[sample_index + 1];
const float t = (opacity_threshold - sample.opacity) /
(sample_after.opacity - sample.opacity);
const int64_t radius = round_fl_to_int_clamp(
math::interpolate(float(sample.radius), float(sample_after.radius), t));
sample.radius = radius;
sample.squared_radius = radius * radius;
sample.opacity = opacity_threshold;
sample.hard_erase = !(next_sample.opacity < opacity_threshold);
}
for (const int rev_sample_index : eraser_rings.index_range()) {
const int sample_index = nb_samples - rev_sample_index - 1;
if (prune_sample[sample_index]) {
eraser_rings.remove(sample_index);
}
}
/* Finally, simplify the array to have a minimal set of samples. */
nb_samples = eraser_rings.size();
const auto opacity_distance = [&](const IndexRange &sub_range, const int index) {
/* Distance function for the simplification algorithm.
* It is computed as the difference in opacity that may result from removing the
* samples inside the range. */
const EraserRing &sample_first = eraser_rings[sub_range.first()];
const EraserRing &sample_last = eraser_rings[sub_range.last()];
const EraserRing &sample = eraser_rings[sub_range[index]];
/* If we were to remove the samples between sample_first and sample_last, then the opacity
* at sample.radius would be a linear interpolation between the opacities in the endpoints
* of the range, with a parameter depending on the distance between radii. That is what we
* are computing here. */
const float t = (sample.radius - sample_first.radius) /
(sample_last.radius - sample_first.radius);
const float linear_opacity = math::interpolate(sample_first.opacity, sample_last.opacity, t);
return math::abs(sample.opacity - linear_opacity);
};
Array<bool> simplify_sample(nb_samples, false);
const float distance_threshold = 0.1f;
ed::greasepencil::ramer_douglas_peucker_simplify(
eraser_rings.index_range(), distance_threshold, opacity_distance, simplify_sample);
for (const int rev_sample_index : eraser_rings.index_range()) {
const int sample_index = nb_samples - rev_sample_index - 1;
if (simplify_sample[sample_index]) {
eraser_rings.remove(sample_index);
}
}
return eraser_rings;
}
/**
* The soft eraser decreases the opacity of the points it hits.
* The new opacity is computed as a minimum between the current opacity and
* a falloff function of the distance of the point to the center of the eraser.
* If the opacity of a point falls below a threshold, then the point is removed from the
* curves.
*/
bool soft_eraser(const blender::bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
blender::bke::CurvesGeometry &dst,
const bool keep_caps)
{
using namespace blender::bke;
const std::string opacity_attr = "opacity";
/* The soft eraser changes the opacity of the strokes underneath it using a curve falloff. We
* sample this curve to get a set of rings in the brush. */
const Vector<EraserRing> eraser_rings = compute_piecewise_linear_falloff();
const int intersections_max_per_segment = eraser_rings.size() * 2;
/* Compute intersections between the source curves geometry and all the rings of the eraser.
*/
const int src_points_num = src.points_num();
Array<std::pair<int, PointCircleSide>> src_point_ring(src_points_num,
{-1, PointCircleSide::Outside});
Array<SegmentCircleIntersection> src_intersections(src_points_num *
intersections_max_per_segment);
curves_intersections_and_points_sides(
src, screen_space_positions, eraser_rings, src_point_ring, src_intersections);
/* Function to get the resulting opacity at a specific point in the source. */
const VArray<float> &src_opacity = *(
src.attributes().lookup_or_default<float>(opacity_attr, ATTR_DOMAIN_POINT, 1.0f));
const auto compute_opacity = [&](const int src_point) {
const float distance = math::distance(screen_space_positions[src_point],
this->mouse_position);
const float brush_opacity = 1.0f - this->eraser_strength *
BKE_brush_curve_strength(
this->brush_, distance, this->eraser_radius);
return math::min(src_opacity[src_point], brush_opacity);
};
/* Compute the map of points in the destination.
* For each point in the source, we create a vector of destination points. Destination points
* can either be directly a point of the source, or a point inside a segment of the source. A
* destination point can also carry the role of a "cut", meaning it is going to be the first
* point of a new splitted curve in the destination. */
Array<Vector<PointTransferData>> src_to_dst_points(src_points_num);
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
for (const int src_curve : src.curves_range()) {
const IndexRange src_points = src_points_by_curve[src_curve];
for (const int src_point : src_points) {
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
const int src_next_point = (src_point == src_points.last()) ? src_points.first() :
(src_point + 1);
/* Get the ring into which the source point lies.
* If the point is completely outside of the eraser, then the index is (-1). */
const int point_ring = src_point_ring[src_point].first;
const bool ring_is_cut = (point_ring != -1) && eraser_rings[point_ring].hard_erase;
const PointCircleSide point_side = src_point_ring[src_point].second;
const bool point_is_cut = ring_is_cut &&
(point_side == PointCircleSide::InsideOutsideBoundary);
const bool remove_point = ring_is_cut && (point_side == PointCircleSide::Inside);
if (!remove_point) {
dst_points.append(
{src_point, src_next_point, 0.0f, true, point_is_cut, compute_opacity(src_point)});
}
const IndexRange src_point_intersections(src_point * intersections_max_per_segment,
intersections_max_per_segment);
std::sort(src_intersections.begin() + src_point_intersections.first(),
src_intersections.begin() + src_point_intersections.last() + 1,
[](SegmentCircleIntersection a, SegmentCircleIntersection b) {
return a.factor < b.factor;
});
/* Add all intersections with the rings. */
for (const SegmentCircleIntersection &intersection :
src_intersections.as_span().slice(src_point_intersections))
{
if (!intersection.is_valid()) {
/* Stop at the first non valid intersection. */
break;
}
const EraserRing &ring = eraser_rings[intersection.ring_index];
const bool is_cut = intersection.inside_outside_intersection && ring.hard_erase;
const float initial_opacity = math::interpolate(
src_opacity[src_point], src_opacity[src_next_point], intersection.factor);
const float opacity = math::max(0.0f, math::min(initial_opacity, ring.opacity));
/* Avoid the accumulation of multiple cuts. */
if (is_cut && !dst_points.is_empty() && dst_points.last().is_cut) {
dst_points.remove_last();
}
dst_points.append(
{src_point, src_next_point, intersection.factor, false, is_cut, opacity});
}
}
}
const Array<PointTransferData> dst_points = compute_topology_change(
src, dst, src_to_dst_points, keep_caps);
/* Set opacity. */
bke::MutableAttributeAccessor dst_attributes = dst.attributes_for_write();
const bke::AnonymousAttributePropagationInfo propagation_info{};
bke::SpanAttributeWriter<float> dst_opacity =
dst_attributes.lookup_or_add_for_write_span<float>(opacity_attr, ATTR_DOMAIN_POINT);
threading::parallel_for(dst.points_range(), 4096, [&](const IndexRange dst_points_range) {
for (const int dst_point_index : dst_points_range) {
const PointTransferData &dst_point = dst_points[dst_point_index];
dst_opacity.span[dst_point_index] = dst_point.opacity;
}
});
dst_opacity.finish();
SpanAttributeWriter<bool> dst_inserted = dst_attributes.lookup_or_add_for_write_span<bool>(
"_eraser_inserted", ATTR_DOMAIN_POINT);
const OffsetIndices<int> &dst_points_by_curve = dst.points_by_curve();
threading::parallel_for(dst.curves_range(), 4096, [&](const IndexRange dst_curves_range) {
for (const int dst_curve : dst_curves_range) {
IndexRange dst_points_range = dst_points_by_curve[dst_curve];
dst_inserted.span[dst_points_range.first()] = false;
dst_inserted.span[dst_points_range.last()] = false;
if (dst_points_range.size() < 3) {
continue;
}
for (const int dst_point_index : dst_points_range.drop_back(1).drop_front(1)) {
const PointTransferData &dst_point = dst_points[dst_point_index];
dst_inserted.span[dst_point_index] |= !dst_point.is_src_point;
}
}
});
dst_inserted.finish();
return true;
}
bool stroke_eraser(const bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
bke::CurvesGeometry &dst) const
@ -734,10 +1070,17 @@ struct EraseOperationExecutor {
/* Get the tool's data. */
this->mouse_position = extension_sample.mouse_position;
this->eraser_radius = self.radius;
this->eraser_strength = self.strength;
if (BKE_brush_use_size_pressure(brush)) {
this->eraser_radius *= BKE_curvemapping_evaluateF(
brush->gpencil_settings->curve_strength, 0, extension_sample.pressure);
}
if (BKE_brush_use_alpha_pressure(brush)) {
this->eraser_strength *= BKE_curvemapping_evaluateF(
brush->gpencil_settings->curve_strength, 0, extension_sample.pressure);
}
this->brush_ = brush;
this->mouse_position_pixels = int2(round_fl_to_int(mouse_position[0]),
round_fl_to_int(mouse_position[1]));
@ -778,8 +1121,8 @@ struct EraseOperationExecutor {
erased = hard_eraser(src, screen_space_positions, dst, self.keep_caps);
break;
case GP_BRUSH_ERASER_SOFT:
// To be implemented
return;
erased = soft_eraser(src, screen_space_positions, dst, self.keep_caps);
break;
}
if (erased) {
@ -787,6 +1130,7 @@ struct EraseOperationExecutor {
drawing.geometry.wrap() = std::move(dst);
drawing.tag_topology_changed();
changed = true;
self.affected_drawings.add(&drawing);
}
};
@ -827,6 +1171,7 @@ void EraseOperation::on_stroke_begin(const bContext &C, const InputSample & /*st
this->eraser_mode = eGP_BrushEraserMode(brush->gpencil_settings->eraser_mode);
this->keep_caps = ((brush->gpencil_settings->flag & GP_BRUSH_ERASER_KEEP_CAPS) != 0);
this->active_layer_only = ((brush->gpencil_settings->flag & GP_BRUSH_ACTIVE_LAYER_ONLY) != 0);
this->strength = BKE_brush_alpha_get(scene, brush);
}
void EraseOperation::on_stroke_extended(const bContext &C, const InputSample &extension_sample)
@ -835,7 +1180,61 @@ void EraseOperation::on_stroke_extended(const bContext &C, const InputSample &ex
executor.execute(*this, C, extension_sample);
}
void EraseOperation::on_stroke_done(const bContext & /*C*/) {}
void EraseOperation::on_stroke_done(const bContext & /*C*/)
{
const float epsilon = 0.01f;
for (GreasePencilDrawing *drawing_ : this->affected_drawings) {
blender::bke::CurvesGeometry &curves = drawing_->geometry.wrap();
/* Simplify in between the ranges of inserted points. */
const VArray<bool> &point_was_inserted = *curves.attributes().lookup<bool>("_eraser_inserted",
ATTR_DOMAIN_POINT);
if (point_was_inserted.is_empty()) {
continue;
}
IndexMaskMemory mem_inserted;
IndexMask inserted_points = IndexMask::from_bools(point_was_inserted, mem_inserted);
/* Distance function for the simplification algorithm.
* It is computed as the difference in opacity that may result from removing the
* samples inside the range. */
VArray<float> opacities = drawing_->wrap().opacities();
Span<float3> positions = curves.positions();
const auto opacity_distance = [&](const IndexRange &sub_range, const int index) {
Span<float3> s_positions = positions.slice(sub_range);
const float3 &s0 = s_positions.first();
const float3 &s1 = s_positions.last();
const float segment_length = math::distance(s0, s1);
if (segment_length < 1e-6) {
return 0.0f;
}
const float t = math::distance(s0, s_positions[index]) / segment_length;
const float linear_opacity = math::interpolate(
opacities[sub_range.first()], opacities[sub_range.last()], t);
const int abs_index = index + sub_range.first();
return math::abs(opacities[abs_index] - linear_opacity);
};
Array<bool> remove_points(curves.points_num(), false);
inserted_points.foreach_range([&](const IndexRange &range) {
IndexRange range_to_simplify(range.one_before_start(), range.size() + 2);
ed::greasepencil::ramer_douglas_peucker_simplify(
range_to_simplify, epsilon, opacity_distance, remove_points);
});
/* Remove the points. */
IndexMaskMemory mem_remove;
IndexMask points_to_remove = IndexMask::from_bools(remove_points, mem_remove);
curves.remove_points(points_to_remove);
drawing_->wrap().tag_topology_changed();
curves.attributes_for_write().remove("_eraser_inserted");
}
this->affected_drawings.clear();
}
std::unique_ptr<GreasePencilStrokeOperation> new_erase_operation()
{