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,
493 Array<SegmentCircleIntersection> src_intersections(src_points_num *
494 intersections_max_per_segment);
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,
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;
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. */
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,
677 Array<SegmentCircleIntersection> src_intersections(src_points_num *
678 intersections_max_per_segment);
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 &&
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);
857 ARegion *region = CTX_wm_region(&C);
859 Object *ob_eval = DEG_get_evaluated_object(depsgraph, obact);
860
862 Brush *brush = BKE_paint_brush(paint);
863
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. */
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. */
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
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
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);
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
in reality light always falls off quadratically Particle Retrieve the data of the particle that spawned the object for example to give variation to multiple instances of an object Point Retrieve information about points in a point cloud Retrieve the edges of an object as it appears to Cycles topology will always appear triangulated Convert a blackbody temperature to an RGB value Normal Generate a perturbed normal from an RGB normal map image Typically used for faking highly detailed surfaces Generate an OSL shader from a file or text data block Image Sample an image file as a texture Gabor Generate Gabor noise Gradient Generate interpolated color and intensity values based on the input vector Magic Generate a psychedelic color texture Voronoi Generate Worley noise based on the distance to random points Typically used to generate textures such as or biological cells Brick Generate a procedural texture producing bricks Texture Retrieve multiple types of texture coordinates nTypically used as inputs for texture nodes Vector Convert a point
#define C
Definition RandGen.cpp:29
#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
BPy_StructRNA * depsgraph
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:232
const T * begin() const
Definition BLI_array.hh:310
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: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
draw_view in_light_buf[] float
GeometryDeformation get_evaluated_grease_pencil_drawing_deformation(const Object *ob_eval, const Object &ob_orig, int layer_index, int frame)
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)
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_each(Range &&range, const Function &function)
Definition BLI_task.hh:58
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
VecBase< int32_t, 2 > int2
VecBase< float, 2 > float2
VecBase< float, 3 > float3
float distance(float a, float b)
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 ToolSettings * toolsettings
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)