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