Blender V4.3
grease_pencil_erase.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#include <algorithm>
6
7#include "BLI_array.hh"
8#include "BLI_array_utils.hh"
9#include "BLI_index_mask.hh"
10#include "BLI_math_base.hh"
11#include "BLI_math_geom.h"
12#include "BLI_task.hh"
13
14#include "BKE_attribute.hh"
15#include "BKE_brush.hh"
16#include "BKE_colortools.hh"
17#include "BKE_context.hh"
18#include "BKE_crazyspace.hh"
19#include "BKE_curves.hh"
20#include "BKE_grease_pencil.hh"
21#include "BKE_paint.hh"
22
24#include "DNA_brush_enums.h"
25
26#include "ED_grease_pencil.hh"
27#include "ED_view3d.hh"
28
29#include "WM_api.hh"
30#include "WM_types.hh"
31
33
35
37
38 public:
39 EraseOperation(bool temp_use_eraser) : temp_eraser_(temp_use_eraser) {}
40 ~EraseOperation() override {}
41
42 void on_stroke_begin(const bContext &C, const InputSample &start_sample) override;
43 void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override;
44 void on_stroke_done(const bContext &C) override;
45
47
48 private:
49 /* Eraser is used by the draw tool temporarily. */
50 bool temp_eraser_ = false;
51
52 bool keep_caps_ = false;
53 float radius_ = 50.0f;
54 float strength_ = 0.1f;
56 bool active_layer_only_ = false;
57
58 Set<GreasePencilDrawing *> affected_drawings_;
59};
60
62 /* Position of the intersection in the segment.
63 * Note: we use a value > 1.0f as initial value so that sorting intersection by increasing
64 * factor can directly put the invalid ones at the end. */
65 float factor = 2.0f;
66
67 /* True if the intersection corresponds to an inside/outside transition with respect to the
68 * circle, false if it corresponds to an outside/inside transition. */
70
71 int ring_index = -1;
72
73 /* An intersection is considered valid if it lies inside of the segment, i.e.
74 * if its factor is in (0,1). */
75 bool is_valid() const
76 {
77 return IN_RANGE(factor, 0.0f, 1.0f);
78 }
79};
81
87
92
95
96 /* Threshold below which points are considered as transparent and thus shall be removed. */
97 static constexpr float opacity_threshold = 0.05f;
98
100
101 struct EraserRing {
102 float radius;
104 float opacity;
105 bool hard_erase{false};
106 };
107
123 const int2 &s1,
124 const int2 &center,
125 const int64_t radius_2,
126 int64_t &r_mu0,
127 int64_t &r_mu1)
128 {
129 const int64_t d_s0_center = math::distance_squared(s0, center);
130 const int64_t a = math::distance_squared(s0, s1);
131 const int64_t b = 2 * math::dot(s0 - center, s1 - s0);
132 const int64_t c = d_s0_center - radius_2;
133
134 /* If points are close together there is no direction vector.
135 * Since the solution multiplies by this factor for integer math,
136 * the valid case of degenerate segments inside the circle needs special handling. */
137 if (a == 0) {
138 const int64_t i = -4 * c;
139 if (i < 0) {
140 /* No intersections. */
141 return 0;
142 }
143 if (i == 0) {
144 /* One intersection. */
145 r_mu0 = 0.0f;
146 return 1;
147 }
148 /* Two intersections. */
149 const float i_sqrt = math::sqrt(float(i));
150 r_mu0 = math::round(i_sqrt / 2.0f);
151 r_mu1 = math::round(-i_sqrt / 2.0f);
152 return 2;
153 }
154
155 const int64_t i = b * b - 4 * a * c;
156
157 if (i < 0) {
158 /* No intersections. */
159 return 0;
160 }
161
162 const int64_t segment_length = math::distance(s0, s1);
163
164 if (i == 0) {
165 /* One intersection. */
166 const float mu0_f = -b / (2.0f * a);
167 r_mu0 = math::round(mu0_f * segment_length);
168 return 1;
169 }
170
171 /* Two intersections. */
172 const float i_sqrt = math::sqrt(float(i));
173 const float mu0_f = (-b + i_sqrt) / (2.0f * a);
174 const float mu1_f = (-b - i_sqrt) / (2.0f * a);
175
176 r_mu0 = math::round(mu0_f * segment_length);
177 r_mu1 = math::round(mu1_f * segment_length);
178
179 return 2;
180 }
181
202 const int2 &point_after,
203 const int64_t squared_radius,
204 float &r_mu0,
205 float &r_mu1,
206 PointCircleSide &r_point_side,
207 PointCircleSide &r_point_after_side) const
208 {
209
210 /* Compute the integer values of the intersection. */
211 const int64_t segment_length = math::distance(point, point_after);
212 int64_t mu0 = -1;
213 int64_t mu1 = -1;
214 const int8_t nb_intersections = intersections_segment_circle_integers(
215 point, point_after, this->mouse_position_pixels, squared_radius, mu0, mu1);
216
217 if (nb_intersections != 2) {
218 /* No intersection with the infinite line : none of the points are inside the circle.
219 * If only one intersection was found, then the eraser is tangential to the line, we don't
220 * account for intersections in this case.
221 */
222 r_mu0 = r_mu1 = -1.0f;
223 r_point_side = PointCircleSide::Outside;
224 r_point_after_side = PointCircleSide::Outside;
225 return 0;
226 }
227
228 if (mu0 > mu1) {
229 std::swap(mu0, mu1);
230 }
231
232 /* Compute on which side of the segment each intersection lies.
233 * -1 : before or at the first endpoint,
234 * 0 : in-between the endpoints,
235 * 1 : after or at the last endpoint.
236 */
237 const int8_t side_mu0 = (mu0 <= 0) ? (-1) : ((mu0 >= segment_length) ? 1 : 0);
238 const int8_t side_mu1 = (mu1 <= 0) ? (-1) : ((mu1 >= segment_length) ? 1 : 0);
239
240 /* The endpoints are on the circle's boundary if one of the intersection falls exactly on them.
241 */
242 r_point_side = (mu0 == 0) ? PointCircleSide::OutsideInsideBoundary :
245 r_point_after_side = (mu0 == segment_length) ?
247 ((mu1 == segment_length) ? PointCircleSide::InsideOutsideBoundary :
249
250 /* Compute the normalized position of the intersection in the curve. */
251 r_mu0 = mu0 / float(segment_length);
252 r_mu1 = mu1 / float(segment_length);
253
254 const bool is_mu0_inside = (side_mu0 == 0);
255 const bool is_mu1_inside = (side_mu1 == 0);
256 if (!is_mu0_inside && !is_mu1_inside) {
257 /* None of the intersection lie within the segment the infinite line. */
258
259 if (side_mu0 == side_mu1) {
260 /* If they are on the same side of the line, then none of the point are inside the circle.
261 */
262 r_point_side = PointCircleSide::Outside;
263 r_point_after_side = PointCircleSide::Outside;
264 return 0;
265 }
266
267 /* If they are on different sides of the line, then both points are inside the circle, or in
268 * the boundary. */
269 return 0;
270 }
271
272 if (is_mu0_inside && is_mu1_inside) {
273 /* Both intersections lie within the segment, none of the points are inside the circle. */
274 r_point_side = PointCircleSide::Outside;
275 r_point_after_side = PointCircleSide::Outside;
276 return 2;
277 }
278
279 /* Only one intersection lies within the segment. Only one point should be erased, depending on
280 * the side of the other intersection. */
281 const int8_t side_outside_intersection = is_mu0_inside ? side_mu1 : side_mu0;
282
283 /* If the other intersection lies before the first endpoint, the first endpoint is inside. */
284 r_point_side = (side_outside_intersection == -1) ? r_point_side : PointCircleSide::Outside;
285 r_point_after_side = (side_outside_intersection == 1) ? r_point_after_side :
287
288 if (is_mu1_inside) {
289 std::swap(r_mu0, r_mu1);
290 }
291 return 1;
292 }
293
314 const bke::CurvesGeometry &src,
315 const Span<float2> screen_space_positions,
316 const Span<EraserRing> rings,
317 MutableSpan<std::pair<int, PointCircleSide>> r_point_ring,
318 MutableSpan<SegmentCircleIntersection> r_intersections) const
319 {
320 /* Each ring can generate zero, one or two intersections per segment. */
321 const int intersections_max_per_segment = rings.size() * 2;
322 const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
323 const VArray<bool> src_cyclic = src.cyclic();
324
325 Array<int2> screen_space_positions_pixel(src.points_num());
326 threading::parallel_for(src.points_range(), 1024, [&](const IndexRange src_points) {
327 for (const int src_point : src_points) {
328 const float2 pos = screen_space_positions[src_point];
329 screen_space_positions_pixel[src_point] = int2(round_fl_to_int(pos[0]),
330 round_fl_to_int(pos[1]));
331 }
332 });
333
334 threading::parallel_for(src.curves_range(), 512, [&](const IndexRange src_curves) {
335 for (const int src_curve : src_curves) {
336 const IndexRange src_curve_points = src_points_by_curve[src_curve];
337
338 if (src_curve_points.size() == 1) {
339 /* One-point stroke : just check if the point is inside the eraser. */
340 int ring_index = 0;
341 for (const EraserRing &eraser_point : rings) {
342 const int src_point = src_curve_points.first();
343 const int64_t squared_distance = math::distance_squared(
344 this->mouse_position_pixels, screen_space_positions_pixel[src_point]);
345
346 /* NOTE: We don't account for boundaries here, since we are not going to split any
347 * curve. */
348 if ((r_point_ring[src_point].first == -1) &&
349 (squared_distance <= eraser_point.squared_radius))
350 {
351 r_point_ring[src_point] = {ring_index, PointCircleSide::Inside};
352 }
353 ++ring_index;
354 }
355 continue;
356 }
357
358 for (const int src_point : src_curve_points.drop_back(1)) {
359 int ring_index = 0;
360 int intersection_offset = src_point * intersections_max_per_segment - 1;
361
362 for (const EraserRing &eraser_point : rings) {
363 SegmentCircleIntersection inter0;
364 SegmentCircleIntersection inter1;
365
366 inter0.ring_index = ring_index;
367 inter1.ring_index = ring_index;
368
369 PointCircleSide point_side;
370 PointCircleSide point_after_side;
371
372 const int8_t nb_inter = segment_intersections_and_points_sides(
373 screen_space_positions_pixel[src_point],
374 screen_space_positions_pixel[src_point + 1],
375 eraser_point.squared_radius,
376 inter0.factor,
377 inter1.factor,
378 point_side,
379 point_after_side);
380
381 /* The point belongs in the ring of the smallest radius circle it is in.
382 * Since our rings are stored in increasing radius order, it corresponds to the first
383 * ring that contains the point. We only include the InsideOutside boundary of the
384 * ring, that is why we do not check for OutsideInsideBoundary.
385 */
386 if ((r_point_ring[src_point].first == -1) &&
387 ELEM(point_side, PointCircleSide::Inside, PointCircleSide::InsideOutsideBoundary))
388 {
389 r_point_ring[src_point] = {ring_index, point_side};
390 }
391
392 if (src_point + 1 == src_curve_points.last()) {
393 if ((r_point_ring[src_point + 1].first == -1) &&
394 ELEM(point_after_side,
395 PointCircleSide::Inside,
396 PointCircleSide::InsideOutsideBoundary))
397 {
398 r_point_ring[src_point + 1] = {ring_index, point_after_side};
399 }
400 }
401
402 if (nb_inter > 0) {
403 inter0.inside_outside_intersection = (inter0.factor > inter1.factor);
404 r_intersections[++intersection_offset] = inter0;
405
406 if (nb_inter > 1) {
407 inter1.inside_outside_intersection = true;
408 r_intersections[++intersection_offset] = inter1;
409 }
410 }
411
412 ++ring_index;
413 }
414 }
415
416 if (src_cyclic[src_curve]) {
417 /* If the curve is cyclic, we need to check for the closing segment. */
418 const int src_last_point = src_curve_points.last();
419 const int src_first_point = src_curve_points.first();
420 int ring_index = 0;
421 int intersection_offset = src_last_point * intersections_max_per_segment - 1;
422
423 for (const EraserRing &eraser_point : rings) {
424 SegmentCircleIntersection inter0;
425 SegmentCircleIntersection inter1;
426
427 inter0.ring_index = ring_index;
428 inter1.ring_index = ring_index;
429
430 PointCircleSide point_side;
431 PointCircleSide point_after_side;
432
433 const int8_t nb_inter = segment_intersections_and_points_sides(
434 screen_space_positions_pixel[src_last_point],
435 screen_space_positions_pixel[src_first_point],
436 eraser_point.squared_radius,
437 inter0.factor,
438 inter1.factor,
439 point_side,
440 point_after_side);
441
442 /* Note : we don't need to set the point side here, since it was already set by the
443 * former loop. */
444
445 if (nb_inter > 0) {
446 inter0.inside_outside_intersection = (inter0.factor > inter1.factor);
447 r_intersections[++intersection_offset] = inter0;
448
449 if (nb_inter > 1) {
450 inter1.inside_outside_intersection = true;
451 r_intersections[++intersection_offset] = inter1;
452 }
453 }
454
455 ++ring_index;
456 }
457 }
458 }
459 });
460
461 /* Compute total number of intersections. */
462 int total_intersections = 0;
463 for (const SegmentCircleIntersection &intersection : r_intersections) {
464 if (intersection.is_valid()) {
465 total_intersections++;
466 }
467 }
468
469 return total_intersections;
470 }
471
472 /* The hard eraser cuts out the curves at their intersection with the eraser, and removes
473 * everything that lies in-between two consecutive intersections. Note that intersections are
474 * computed using integers (pixel-space) to avoid floating-point approximation errors. */
475
477 const Span<float2> screen_space_positions,
479 const bool keep_caps) const
480 {
481 const VArray<bool> src_cyclic = src.cyclic();
482 const int src_points_num = src.points_num();
483
484 /* For the hard erase, we compute with a circle, so there can only be a maximum of two
485 * intersection per segment. */
486 const Vector<EraserRing> eraser_rings(
487 1, {this->eraser_radius, this->eraser_squared_radius_pixels, 0.0f, true});
488 const int intersections_max_per_segment = eraser_rings.size() * 2;
489
490 /* Compute intersections between the eraser and the curves in the source domain. */
491 Array<std::pair<int, PointCircleSide>> src_point_ring(src_points_num,
492 {-1, PointCircleSide::Outside});
493 Array<SegmentCircleIntersection> src_intersections(src_points_num *
494 intersections_max_per_segment);
495 curves_intersections_and_points_sides(
496 src, screen_space_positions, eraser_rings, src_point_ring, src_intersections);
497
498 Array<Vector<ed::greasepencil::PointTransferData>> src_to_dst_points(src_points_num);
499 const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
500 for (const int src_curve : src.curves_range()) {
501 const IndexRange src_points = src_points_by_curve[src_curve];
502
503 for (const int src_point : src_points) {
504 Vector<ed::greasepencil::PointTransferData> &dst_points = src_to_dst_points[src_point];
505 const int src_next_point = (src_point == src_points.last()) ? src_points.first() :
506 (src_point + 1);
507 const PointCircleSide point_side = src_point_ring[src_point].second;
508
509 /* Add the source point only if it does not lie inside of the eraser. */
510 if (point_side != PointCircleSide::Inside) {
511 dst_points.append({src_point,
512 src_next_point,
513 0.0f,
514 true,
515 (point_side == PointCircleSide::InsideOutsideBoundary)});
516 }
517
518 /* Add all intersections with the eraser. */
519 const IndexRange src_point_intersections(src_point * intersections_max_per_segment,
520 intersections_max_per_segment);
521 for (const SegmentCircleIntersection &intersection :
522 src_intersections.as_span().slice(src_point_intersections))
523 {
524 if (!intersection.is_valid()) {
525 /* Stop at the first non valid intersection. */
526 break;
527 }
528 dst_points.append({src_point,
529 src_next_point,
530 intersection.factor,
531 false,
532 intersection.inside_outside_intersection});
533 }
534 }
535 }
536
537 ed::greasepencil::compute_topology_change(src, dst, src_to_dst_points, keep_caps);
538
539 return true;
540 }
541
543 {
544 /* The changes in opacity implied by the soft eraser are described by a falloff curve
545 * mapping. Abscissa of the curve is the normalized distance to the brush, and ordinate of
546 * the curve is the strength of the eraser.
547 *
548 * To apply this falloff as precisely as possible, we compute a set of "rings" to the brush,
549 * meaning a set of samples in the curve mapping in between which the strength of the eraser
550 * is applied linearly.
551 *
552 * In other words, we compute a minimal set of samples that describe the falloff curve as a
553 * polyline. */
554
555 /* First, distance-based sampling with a small pixel distance.
556 * The samples are stored in increasing radius order. */
557 const int step_pixels = 2;
558 int nb_samples = math::round(this->eraser_radius / step_pixels);
559 Vector<EraserRing> eraser_rings(nb_samples);
560 for (const int sample_index : eraser_rings.index_range()) {
561 const int64_t sampled_distance = (sample_index + 1) * step_pixels;
562
563 EraserRing &ring = eraser_rings[sample_index];
564 ring.radius = sampled_distance;
565 ring.squared_radius = sampled_distance * sampled_distance;
566 ring.opacity = 1.0 - this->eraser_strength *
568 this->brush_, float(sampled_distance), this->eraser_radius);
569 }
570
571 /* Then, prune samples that are under the opacity threshold. */
572 Array<bool> prune_sample(nb_samples, false);
573 for (const int sample_index : eraser_rings.index_range()) {
574 EraserRing &sample = eraser_rings[sample_index];
575
576 if (sample_index == nb_samples - 1) {
577 /* If this is the last samples, we need to keep it at the same position (it corresponds
578 * to the brush overall radius). It is a cut if the opacity is under the threshold.*/
579 sample.hard_erase = (sample.opacity < opacity_threshold);
580 continue;
581 }
582
583 EraserRing next_sample = eraser_rings[sample_index + 1];
584
585 /* If both samples are under the threshold, prune it !
586 * If none of them are under the threshold, leave them as they are.
587 */
588 if ((sample.opacity < opacity_threshold) == (next_sample.opacity < opacity_threshold)) {
589 prune_sample[sample_index] = (sample.opacity < opacity_threshold);
590 continue;
591 }
592
593 /* Otherwise, shift the sample to the spot where the opacity is exactly at the threshold.
594 * This way we don't remove larger opacity values in-between the samples. */
595 const EraserRing &sample_after = eraser_rings[sample_index + 1];
596
597 const float t = (opacity_threshold - sample.opacity) /
598 (sample_after.opacity - sample.opacity);
599
600 const int64_t radius = math::round(
601 math::interpolate(float(sample.radius), float(sample_after.radius), t));
602
603 sample.radius = radius;
604 sample.squared_radius = radius * radius;
605 sample.opacity = opacity_threshold;
606 sample.hard_erase = !(next_sample.opacity < opacity_threshold);
607 }
608
609 for (const int rev_sample_index : eraser_rings.index_range()) {
610 const int sample_index = nb_samples - rev_sample_index - 1;
611 if (prune_sample[sample_index]) {
612 eraser_rings.remove(sample_index);
613 }
614 }
615
616 /* Finally, simplify the array to have a minimal set of samples. */
617 nb_samples = eraser_rings.size();
618
619 const auto opacity_distance = [&](int64_t first_index, int64_t last_index, int64_t index) {
620 /* Distance function for the simplification algorithm.
621 * It is computed as the difference in opacity that may result from removing the
622 * samples inside the range. */
623 const EraserRing &sample_first = eraser_rings[first_index];
624 const EraserRing &sample_last = eraser_rings[last_index];
625 const EraserRing &sample = eraser_rings[index];
626
627 /* If we were to remove the samples between sample_first and sample_last, then the opacity
628 * at sample.radius would be a linear interpolation between the opacities in the endpoints
629 * of the range, with a parameter depending on the distance between radii. That is what we
630 * are computing here. */
631 const float t = (sample.radius - sample_first.radius) /
632 (sample_last.radius - sample_first.radius);
633 const float linear_opacity = math::interpolate(sample_first.opacity, sample_last.opacity, t);
634
635 return math::abs(sample.opacity - linear_opacity);
636 };
637 Array<bool> simplify_sample(nb_samples, false);
638 const float distance_threshold = 0.1f;
639 ed::greasepencil::ramer_douglas_peucker_simplify(
640 eraser_rings.index_range(), distance_threshold, opacity_distance, simplify_sample);
641
642 for (const int rev_sample_index : eraser_rings.index_range()) {
643 const int sample_index = nb_samples - rev_sample_index - 1;
644 if (simplify_sample[sample_index]) {
645 eraser_rings.remove(sample_index);
646 }
647 }
648
649 return eraser_rings;
650 }
651
660 const Span<float2> screen_space_positions,
662 const bool keep_caps)
663 {
664 using namespace blender::bke;
665 const std::string opacity_attr = "opacity";
666
667 /* The soft eraser changes the opacity of the strokes underneath it using a curve falloff. We
668 * sample this curve to get a set of rings in the brush. */
669 const Vector<EraserRing> eraser_rings = compute_piecewise_linear_falloff();
670 const int intersections_max_per_segment = eraser_rings.size() * 2;
671
672 /* Compute intersections between the source curves geometry and all the rings of the eraser.
673 */
674 const int src_points_num = src.points_num();
675 Array<std::pair<int, PointCircleSide>> src_point_ring(src_points_num,
676 {-1, PointCircleSide::Outside});
677 Array<SegmentCircleIntersection> src_intersections(src_points_num *
678 intersections_max_per_segment);
679 curves_intersections_and_points_sides(
680 src, screen_space_positions, eraser_rings, src_point_ring, src_intersections);
681
682 /* Function to get the resulting opacity at a specific point in the source. */
683 const VArray<float> &src_opacity = *src.attributes().lookup_or_default<float>(
684 opacity_attr, bke::AttrDomain::Point, 1.0f);
685 const auto compute_opacity = [&](const int src_point) {
686 const float distance = math::distance(screen_space_positions[src_point],
687 this->mouse_position);
688 const float brush_strength = this->eraser_strength *
690 this->brush_, distance, this->eraser_radius);
691 return math::clamp(src_opacity[src_point] - brush_strength, 0.0f, 1.0f);
692 };
693
694 /* Compute the map of points in the destination.
695 * For each point in the source, we create a vector of destination points. Destination points
696 * can either be directly a point of the source, or a point inside a segment of the source. A
697 * destination point can also carry the role of a "cut", meaning it is going to be the first
698 * point of a new curve split into the destination. */
699 Array<Vector<ed::greasepencil::PointTransferData>> src_to_dst_points(src_points_num);
700 const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
701 for (const int src_curve : src.curves_range()) {
702 const IndexRange src_points = src_points_by_curve[src_curve];
703
704 for (const int src_point : src_points) {
705 Vector<ed::greasepencil::PointTransferData> &dst_points = src_to_dst_points[src_point];
706 const int src_next_point = (src_point == src_points.last()) ? src_points.first() :
707 (src_point + 1);
708
709 /* Get the ring into which the source point lies.
710 * If the point is completely outside of the eraser, then the index is (-1). */
711 const int point_ring = src_point_ring[src_point].first;
712 const bool ring_is_cut = (point_ring != -1) && eraser_rings[point_ring].hard_erase;
713 const PointCircleSide point_side = src_point_ring[src_point].second;
714
715 const bool point_is_cut = ring_is_cut &&
716 (point_side == PointCircleSide::InsideOutsideBoundary);
717 const bool remove_point = ring_is_cut && (point_side == PointCircleSide::Inside);
718 if (!remove_point) {
719 dst_points.append(
720 {src_point, src_next_point, 0.0f, true, point_is_cut, compute_opacity(src_point)});
721 }
722
723 const IndexRange src_point_intersections(src_point * intersections_max_per_segment,
724 intersections_max_per_segment);
725
726 std::sort(src_intersections.begin() + src_point_intersections.first(),
727 src_intersections.begin() + src_point_intersections.last() + 1,
729 return a.factor < b.factor;
730 });
731
732 /* Add all intersections with the rings. */
733 for (const SegmentCircleIntersection &intersection :
734 src_intersections.as_span().slice(src_point_intersections))
735 {
736 if (!intersection.is_valid()) {
737 /* Stop at the first non valid intersection. */
738 break;
739 }
740
741 const EraserRing &ring = eraser_rings[intersection.ring_index];
742 const bool is_cut = intersection.inside_outside_intersection && ring.hard_erase;
743 const float initial_opacity = math::interpolate(
744 src_opacity[src_point], src_opacity[src_next_point], intersection.factor);
745
746 const float opacity = math::max(0.0f, math::min(initial_opacity, ring.opacity));
747
748 /* Avoid the accumulation of multiple cuts. */
749 if (is_cut && !dst_points.is_empty() && dst_points.last().is_cut) {
750 dst_points.remove_last();
751 }
752
753 dst_points.append(
754 {src_point, src_next_point, intersection.factor, false, is_cut, opacity});
755 }
756 }
757 }
758
759 const Array<ed::greasepencil::PointTransferData> dst_points = compute_topology_change(
760 src, dst, src_to_dst_points, keep_caps);
761
762 /* Set opacity. */
764
766 dst_attributes.lookup_or_add_for_write_span<float>(opacity_attr, bke::AttrDomain::Point);
767 threading::parallel_for(dst.points_range(), 4096, [&](const IndexRange dst_points_range) {
768 for (const int dst_point_index : dst_points_range) {
769 const ed::greasepencil::PointTransferData &dst_point = dst_points[dst_point_index];
770 dst_opacity.span[dst_point_index] = dst_point.opacity;
771 }
772 });
773 dst_opacity.finish();
774
775 SpanAttributeWriter<bool> dst_inserted = dst_attributes.lookup_or_add_for_write_span<bool>(
776 "_eraser_inserted", bke::AttrDomain::Point);
777 const OffsetIndices<int> &dst_points_by_curve = dst.points_by_curve();
778 threading::parallel_for(dst.curves_range(), 4096, [&](const IndexRange dst_curves_range) {
779 for (const int dst_curve : dst_curves_range) {
780 IndexRange dst_points_range = dst_points_by_curve[dst_curve];
781
782 dst_inserted.span[dst_points_range.first()] = false;
783 dst_inserted.span[dst_points_range.last()] = false;
784
785 if (dst_points_range.size() < 3) {
786 continue;
787 }
788
789 for (const int dst_point_index : dst_points_range.drop_back(1).drop_front(1)) {
790 const ed::greasepencil::PointTransferData &dst_point = dst_points[dst_point_index];
791 dst_inserted.span[dst_point_index] |= !dst_point.is_src_point;
792 }
793 }
794 });
795 dst_inserted.finish();
796
797 return true;
798 }
799
801 const Span<float2> screen_space_positions,
802 bke::CurvesGeometry &dst) const
803 {
804 const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
805 const VArray<bool> src_cyclic = src.cyclic();
806
807 IndexMaskMemory memory;
808 const IndexMask strokes_to_keep = IndexMask::from_predicate(
809 src.curves_range(), GrainSize(256), memory, [&](const int src_curve) {
810 const IndexRange src_curve_points = src_points_by_curve[src_curve];
811
812 /* One-point stroke : remove the stroke if the point lies inside of the eraser. */
813 if (src_curve_points.size() == 1) {
814 const float2 &point_pos = screen_space_positions[src_curve_points.first()];
815 const float dist_to_eraser = math::distance(point_pos, this->mouse_position);
816 return !(dist_to_eraser < this->eraser_radius);
817 }
818
819 /* If any segment of the stroke is closer to the eraser than its radius, then remove
820 * the stroke. */
821 for (const int src_point : src_curve_points.drop_back(1)) {
822 const float dist_to_eraser = dist_to_line_segment_v2(
823 this->mouse_position,
824 screen_space_positions[src_point],
825 screen_space_positions[src_point + 1]);
826 if (dist_to_eraser < this->eraser_radius) {
827 return false;
828 }
829 }
830
831 if (src_cyclic[src_curve]) {
832 const float dist_to_eraser = dist_to_line_segment_v2(
833 this->mouse_position,
834 screen_space_positions[src_curve_points.first()],
835 screen_space_positions[src_curve_points.last()]);
836 if (dist_to_eraser < this->eraser_radius) {
837 return false;
838 }
839 }
840
841 return true;
842 });
843
844 if (strokes_to_keep.size() == src.curves_num()) {
845 return false;
846 }
847
848 dst = bke::curves_copy_curve_selection(src, strokes_to_keep, {});
849 return true;
850 }
851
852 void execute(EraseOperation &self, const bContext &C, const InputSample &extension_sample)
853 {
854 using namespace blender::bke::greasepencil;
855 Scene *scene = CTX_data_scene(&C);
856 Depsgraph *depsgraph = CTX_data_depsgraph_pointer(&C);
857 ARegion *region = CTX_wm_region(&C);
858 Object *obact = CTX_data_active_object(&C);
859 Object *ob_eval = DEG_get_evaluated_object(depsgraph, obact);
860
861 Paint *paint = &scene->toolsettings->gp_paint->paint;
862 Brush *brush = BKE_paint_brush(paint);
863
865 brush = BKE_paint_eraser_brush(paint);
866 }
867
868 /* Get the brush data. */
869 this->mouse_position = extension_sample.mouse_position;
870 this->eraser_radius = self.radius_;
871 this->eraser_strength = self.strength_;
872
873 if (BKE_brush_use_size_pressure(brush)) {
874 this->eraser_radius *= BKE_curvemapping_evaluateF(
875 brush->gpencil_settings->curve_strength, 0, extension_sample.pressure);
876 }
877 if (BKE_brush_use_alpha_pressure(brush)) {
878 this->eraser_strength *= BKE_curvemapping_evaluateF(
879 brush->gpencil_settings->curve_strength, 0, extension_sample.pressure);
880 }
881 this->brush_ = brush;
882
883 this->mouse_position_pixels = int2(round_fl_to_int(this->mouse_position.x),
884 round_fl_to_int(this->mouse_position.y));
885 const int64_t eraser_radius_pixels = round_fl_to_int(this->eraser_radius);
886 this->eraser_squared_radius_pixels = eraser_radius_pixels * eraser_radius_pixels;
887
888 /* Get the grease pencil drawing. */
889 GreasePencil &grease_pencil = *static_cast<GreasePencil *>(obact->data);
890
891 bool changed = false;
892 const auto execute_eraser_on_drawing =
893 [&](const int layer_index, const int frame_number, Drawing &drawing) {
894 const Layer &layer = grease_pencil.layer(layer_index);
895 const bke::CurvesGeometry &src = drawing.strokes();
896
897 /* Evaluated geometry. */
899 bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation(
900 ob_eval, *obact, layer_index, frame_number);
901
902 /* Compute screen space positions. */
903 Array<float2> screen_space_positions(src.points_num());
904 threading::parallel_for(src.points_range(), 4096, [&](const IndexRange src_points) {
905 for (const int src_point : src_points) {
906 const int result = ED_view3d_project_float_global(
907 region,
908 math::transform_point(layer.to_world_space(*ob_eval),
909 deformation.positions[src_point]),
910 screen_space_positions[src_point],
911 V3D_PROJ_TEST_CLIP_NEAR | V3D_PROJ_TEST_CLIP_FAR);
912 if (result != V3D_PROJ_RET_OK) {
913 /* Set the screen space position to a impossibly far coordinate for all the points
914 * that are outside near/far clipping planes, this is to prevent accidental
915 * intersections with strokes not visibly present in the camera. */
916 screen_space_positions[src_point] = float2(1e20);
917 }
918 }
919 });
920
921 /* Erasing operator. */
923 bool erased = false;
924 switch (self.eraser_mode_) {
926 erased = stroke_eraser(src, screen_space_positions, dst);
927 break;
929 erased = hard_eraser(src, screen_space_positions, dst, self.keep_caps_);
930 break;
932 erased = soft_eraser(src, screen_space_positions, dst, self.keep_caps_);
933 break;
934 }
935
936 if (erased) {
937 /* Set the new geometry. */
938 drawing.geometry.wrap() = std::move(dst);
939 drawing.tag_topology_changed();
940 changed = true;
941 self.affected_drawings_.add(&drawing);
942 }
943 };
944
945 if (self.active_layer_only_) {
946 /* Erase only on the drawing at the current frame of the active layer. */
947 if (!grease_pencil.has_active_layer()) {
948 return;
949 }
950 const Layer &active_layer = *grease_pencil.get_active_layer();
951 Drawing *drawing = grease_pencil.get_editable_drawing_at(active_layer, scene->r.cfra);
952
953 if (drawing == nullptr) {
954 return;
955 }
956
957 execute_eraser_on_drawing(
958 *grease_pencil.get_layer_index(active_layer), scene->r.cfra, *drawing);
959 }
960 else {
961 /* Erase on all editable drawings. */
963 ed::greasepencil::retrieve_editable_drawings(*scene, grease_pencil);
964 threading::parallel_for_each(
965 drawings, [&](const ed::greasepencil::MutableDrawingInfo &info) {
966 execute_eraser_on_drawing(info.layer_index, info.frame_number, info.drawing);
967 });
968 }
969
970 if (changed) {
971 DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY);
972 WM_event_add_notifier(&C, NC_GEOM | ND_DATA, &grease_pencil);
973 }
974 }
975};
976
977void EraseOperation::on_stroke_begin(const bContext &C, const InputSample & /*start_sample*/)
978{
980 Brush *brush = BKE_paint_brush(paint);
981
982 /* If we're using the draw tool to erase (e.g. while holding ctrl), then we should use the
983 * eraser brush instead. */
984 if (temp_eraser_) {
985 Object *object = CTX_data_active_object(&C);
986 GreasePencil *grease_pencil = static_cast<GreasePencil *>(object->data);
987
988 radius_ = paint->eraser_brush->size;
989 grease_pencil->runtime->temp_eraser_size = radius_;
990 grease_pencil->runtime->temp_use_eraser = true;
991
992 brush = BKE_paint_eraser_brush(paint);
993 }
994 else {
995 radius_ = brush->size;
996 }
997
998 if (brush->gpencil_settings == nullptr) {
1000 }
1001 BLI_assert(brush->gpencil_settings != nullptr);
1002
1005
1006 eraser_mode_ = eGP_BrushEraserMode(brush->gpencil_settings->eraser_mode);
1007 keep_caps_ = ((brush->gpencil_settings->flag & GP_BRUSH_ERASER_KEEP_CAPS) != 0);
1008 active_layer_only_ = ((brush->gpencil_settings->flag & GP_BRUSH_ACTIVE_LAYER_ONLY) != 0);
1009 strength_ = brush->alpha;
1010}
1011
1012void EraseOperation::on_stroke_extended(const bContext &C, const InputSample &extension_sample)
1013{
1014 EraseOperationExecutor executor{C};
1015 executor.execute(*this, C, extension_sample);
1016}
1017
1018void EraseOperation::on_stroke_done(const bContext &C)
1019{
1020 Object *object = CTX_data_active_object(&C);
1021 GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object->data);
1022 if (temp_eraser_) {
1023 /* If we're using the draw tool to temporarily erase, then we need to reset the
1024 * `temp_use_eraser` flag here. */
1025 grease_pencil.runtime->temp_use_eraser = false;
1026 grease_pencil.runtime->temp_eraser_size = 0.0f;
1027 }
1028
1029 /* Epsilon used for simplify. */
1030 const float epsilon = 0.01f;
1031 for (GreasePencilDrawing *drawing_ : affected_drawings_) {
1032 blender::bke::CurvesGeometry &curves = drawing_->geometry.wrap();
1033
1034 /* Simplify in between the ranges of inserted points. */
1035 const VArray<bool> &point_was_inserted = *curves.attributes().lookup<bool>(
1036 "_eraser_inserted", bke::AttrDomain::Point);
1037 if (point_was_inserted.is_empty()) {
1038 continue;
1039 }
1040 IndexMaskMemory mem_inserted;
1041 IndexMask inserted_points = IndexMask::from_bools(point_was_inserted, mem_inserted);
1042
1043 /* Distance function for the simplification algorithm.
1044 * It is computed as the difference in opacity that may result from removing the
1045 * samples inside the range. */
1046 VArray<float> opacities = drawing_->wrap().opacities();
1047 Span<float3> positions = curves.positions();
1048 const auto opacity_distance = [&](int64_t first_index, int64_t last_index, int64_t index) {
1049 const float3 &s0 = positions[first_index];
1050 const float3 &s1 = positions[last_index];
1051 const float segment_length = math::distance(s0, s1);
1052 if (segment_length < 1e-6) {
1053 return 0.0f;
1054 }
1055 const float t = math::distance(s0, positions[index]) / segment_length;
1056 const float linear_opacity = math::interpolate(
1057 opacities[first_index], opacities[last_index], t);
1058 return math::abs(opacities[index] - linear_opacity);
1059 };
1060
1061 Array<bool> remove_points(curves.points_num(), false);
1062 inserted_points.foreach_range([&](const IndexRange &range) {
1063 IndexRange range_to_simplify(range.one_before_start(), range.size() + 2);
1064 ed::greasepencil::ramer_douglas_peucker_simplify(
1065 range_to_simplify, epsilon, opacity_distance, remove_points);
1066 });
1067
1068 /* Remove the points. */
1069 IndexMaskMemory mem_remove;
1070 IndexMask points_to_remove = IndexMask::from_bools(remove_points, mem_remove);
1071
1072 curves.remove_points(points_to_remove, {});
1073 drawing_->wrap().tag_topology_changed();
1074
1075 curves.attributes_for_write().remove("_eraser_inserted");
1076 }
1077
1078 affected_drawings_.clear();
1079}
1080
1081std::unique_ptr<GreasePencilStrokeOperation> new_erase_operation(const bool temp_eraser)
1082{
1083 return std::make_unique<EraseOperation>(temp_eraser);
1084}
1085
1086} // namespace blender::ed::sculpt_paint::greasepencil
bool BKE_brush_use_alpha_pressure(const Brush *brush)
Definition brush.cc:1096
float BKE_brush_curve_strength(eBrushCurvePreset preset, const CurveMapping *cumap, float distance, float brush_radius)
Definition brush.cc:1388
bool BKE_brush_use_size_pressure(const Brush *brush)
Definition brush.cc:1091
void BKE_brush_init_gpencil_settings(Brush *brush)
Definition brush.cc:563
float BKE_curvemapping_evaluateF(const CurveMapping *cumap, int cur, float value)
void BKE_curvemapping_init(CurveMapping *cumap)
Depsgraph * CTX_data_depsgraph_pointer(const bContext *C)
Object * CTX_data_active_object(const bContext *C)
Scene * CTX_data_scene(const bContext *C)
ARegion * CTX_wm_region(const bContext *C)
Low-level operations for curves.
Low-level operations for grease pencil.
Brush * BKE_paint_eraser_brush(Paint *paint)
Definition paint.cc:1155
Paint * BKE_paint_get_active_from_context(const bContext *C)
Definition paint.cc:477
Brush * BKE_paint_brush(Paint *paint)
Definition paint.cc:649
#define BLI_assert(a)
Definition BLI_assert.h:50
MINLINE int round_fl_to_int(float a)
float dist_to_line_segment_v2(const float p[2], const float l1[2], const float l2[2])
Definition math_geom.cc:298
#define IN_RANGE(a, b, c)
void DEG_id_tag_update(ID *id, unsigned int flags)
Object * DEG_get_evaluated_object(const Depsgraph *depsgraph, Object *object)
@ ID_RECALC_GEOMETRY
Definition DNA_ID.h:1041
@ GPAINT_BRUSH_TYPE_DRAW
@ GP_BRUSH_ERASER_KEEP_CAPS
@ GP_BRUSH_ACTIVE_LAYER_ONLY
eGP_BrushEraserMode
@ GP_BRUSH_ERASER_SOFT
@ GP_BRUSH_ERASER_STROKE
@ GP_BRUSH_ERASER_HARD
#define NC_GEOM
Definition WM_types.hh:360
#define ND_DATA
Definition WM_types.hh:475
ATTR_WARN_UNUSED_RESULT const BMVert const BMEdge * e
PyObject * self
Span< T > as_span() const
Definition BLI_array.hh:232
const T * begin() const
Definition BLI_array.hh:310
constexpr int64_t first() const
constexpr IndexRange drop_back(int64_t n) const
constexpr int64_t last(const int64_t n=0) const
constexpr int64_t size() const
constexpr int64_t size() const
Definition BLI_span.hh:253
int64_t size() const
void append(const T &value)
const T & last(const int64_t n=0) const
void remove(const int64_t index)
bool is_empty() const
IndexRange index_range() const
GAttributeReader lookup_or_default(StringRef attribute_id, AttrDomain domain, eCustomDataType data_type, const void *default_value=nullptr) const
OffsetIndices< int > points_by_curve() const
IndexRange curves_range() const
MutableAttributeAccessor attributes_for_write()
IndexRange points_range() const
VArray< bool > cyclic() const
GSpanAttributeWriter lookup_or_add_for_write_span(StringRef attribute_id, AttrDomain domain, eCustomDataType data_type, const AttributeInit &initializer=AttributeInitDefaultValue())
void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override
void on_stroke_begin(const bContext &C, const InputSample &start_sample) override
void foreach_range(Fn &&fn) const
local_group_size(16, 16) .push_constant(Type b
const Depsgraph * depsgraph
draw_view in_light_buf[] float
std::unique_ptr< GreasePencilStrokeOperation > new_erase_operation(const bool temp_eraser)
T clamp(const T &a, const T &min, const T &max)
T sqrt(const T &a)
T distance(const T &a, const T &b)
T dot(const QuaternionBase< T > &a, const QuaternionBase< T > &b)
T min(const T &a, const T &b)
T interpolate(const T &a, const T &b, const FactorT &t)
T distance_squared(const VecBase< T, Size > &a, const VecBase< T, Size > &b)
T max(const T &a, const T &b)
T abs(const T &a)
T round(const T &a)
void parallel_for(const IndexRange range, const int64_t grain_size, const Function &function, const TaskSizeHints &size_hints=detail::TaskSizeHints_Static(1))
Definition BLI_task.hh:95
static float brush_strength(const Sculpt &sd, const blender::ed::sculpt_paint::StrokeCache &cache, const float feather, const UnifiedPaintSettings &ups, const PaintModeSettings &)
Definition sculpt.cc:2067
__int64 int64_t
Definition stdint.h:89
signed char int8_t
Definition stdint.h:75
struct CurveMapping * curve_strength
float alpha
struct CurveMapping * curve
struct BrushGpencilSettings * gpencil_settings
char gpencil_brush_type
GreasePencilRuntimeHandle * runtime
struct Brush * eraser_brush
bool stroke_eraser(const bke::CurvesGeometry &src, const Span< float2 > screen_space_positions, bke::CurvesGeometry &dst) const
static int8_t intersections_segment_circle_integers(const int2 &s0, const int2 &s1, const int2 &center, const int64_t radius_2, int64_t &r_mu0, int64_t &r_mu1)
bool hard_eraser(const bke::CurvesGeometry &src, const Span< float2 > screen_space_positions, bke::CurvesGeometry &dst, const bool keep_caps) const
void execute(EraseOperation &self, const bContext &C, const InputSample &extension_sample)
int8_t segment_intersections_and_points_sides(const int2 &point, const int2 &point_after, const int64_t squared_radius, float &r_mu0, float &r_mu1, PointCircleSide &r_point_side, PointCircleSide &r_point_after_side) const
int curves_intersections_and_points_sides(const bke::CurvesGeometry &src, const Span< float2 > screen_space_positions, const Span< EraserRing > rings, MutableSpan< std::pair< int, PointCircleSide > > r_point_ring, MutableSpan< SegmentCircleIntersection > r_intersections) const
bool soft_eraser(const blender::bke::CurvesGeometry &src, const Span< float2 > screen_space_positions, blender::bke::CurvesGeometry &dst, const bool keep_caps)
void WM_event_add_notifier(const bContext *C, uint type, void *reference)