/* SPDX-FileCopyrightText: 2023 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "BLI_array.hh" #include "BLI_array_utils.hh" #include "BLI_index_mask.hh" #include "BLI_math_geom.h" #include "BLI_task.hh" #include "BKE_brush.hh" #include "BKE_colortools.h" #include "BKE_context.hh" #include "BKE_crazyspace.hh" #include "BKE_curves.hh" #include "BKE_curves_utils.hh" #include "BKE_grease_pencil.h" #include "BKE_grease_pencil.hh" #include "BKE_scene.h" #include "DEG_depsgraph_query.hh" #include "DNA_brush_enums.h" #include "ED_grease_pencil.hh" #include "ED_view3d.hh" #include "WM_api.hh" #include "WM_types.hh" #include "grease_pencil_intern.hh" namespace blender::ed::sculpt_paint::greasepencil { class EraseOperation : public GreasePencilStrokeOperation { public: ~EraseOperation() override {} void on_stroke_begin(const bContext &C, const InputSample &start_sample) override; void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override; void on_stroke_done(const bContext &C) override; bool keep_caps = false; float radius = 50.0f; eGP_BrushEraserMode eraser_mode = GP_BRUSH_ERASER_HARD; bool active_layer_only = false; }; /** * Utility class that actually executes the update when the stroke is updated. That's useful * because it avoids passing a very large number of parameters between functions. */ struct EraseOperationExecutor { float2 mouse_position{}; float eraser_radius{}; int2 mouse_position_pixels{}; int64_t eraser_squared_radius_pixels{}; bke::greasepencil::DrawingTransforms transforms_; EraseOperationExecutor(const bContext &C) { Object *object = CTX_data_active_object(&C); transforms_ = bke::greasepencil::DrawingTransforms(*object); } /** * Computes the intersections between a 2D line segment and a circle with integer values. * * \param s0, s1: endpoints of the segment. * \param center: center of the circle, * \param radius_2: squared radius of the circle. * * \param r_mu0: (output) signed distance from \a s0 to the first intersection, if it exists. * \param r_mu1: (output) signed distance from \a s0 to the second intersection, if it exists. * * All intersections with the infinite line of the segment are considered. * * \returns the number of intersection found. */ static int8_t intersections_segment_circle_integers(const int2 &s0, const int2 &s1, const int2 ¢er, const int64_t radius_2, int64_t &r_mu0, int64_t &r_mu1) { const int64_t d_s0_center = math::distance_squared(s0, center); const int64_t a = math::distance_squared(s0, s1); const int64_t b = 2 * math::dot(s0 - center, s1 - s0); const int64_t c = d_s0_center - radius_2; const int64_t i = b * b - 4 * a * c; if (i < 0) { /* No intersections. */ return 0; } const int64_t segment_length = math::distance(s0, s1); if (i == 0) { /* One intersection. */ const float mu0_f = -b / (2.0f * a); r_mu0 = round_fl_to_int(mu0_f * segment_length); return 1; } /* Two intersections. */ const float i_sqrt = sqrtf(float(i)); const float mu0_f = (-b + i_sqrt) / (2.0f * a); const float mu1_f = (-b - i_sqrt) / (2.0f * a); r_mu0 = round_fl_to_int(mu0_f * segment_length); r_mu1 = round_fl_to_int(mu1_f * segment_length); return 2; } struct SegmentCircleIntersection { /* Position of the intersection in the segment. */ float factor = -1.0f; /* True if the intersection corresponds to an inside/outside transition with respect to the * circle, false if it corresponds to an outside/inside transition. */ bool inside_outside_intersection = false; /* An intersection is considered valid if it lies inside of the segment, i.e. * if its factor is in (0,1). */ bool is_valid() const { return IN_RANGE(factor, 0.0f, 1.0f); } }; enum class PointCircleSide { Outside, OutsideInsideBoundary, InsideOutsideBoundary, Inside }; /** * Computes the intersection between the eraser tool and a 2D segment, using integer values. * Also computes if the endpoints of the segment lie inside/outside, or in the boundary of the * eraser. * * \param point, point_after: coordinates of the first (resp. second) endpoint in the segment. * * \param squared_radius: squared radius of the brush in pixels. * * \param r_mu0, r_mu0: (output) factor of the two intersections if they exists, otherwise (-1). * * \param point_side, point_after_side: (output) enum describing where the first (resp. second) * endpoint lies relatively to the eraser: inside, outside or at the boundary of the eraser. * * \returns total number of intersections lying inside the segment (ie whose factor is in ]0,1[). * * Note that the eraser is represented as a circle, and thus there can be only 0, 1 or 2 * intersections with a segment. */ 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 { /* Compute the integer values of the intersection. */ const int64_t segment_length = math::distance(point, point_after); int64_t mu0 = -1; int64_t mu1 = -1; const int8_t nb_intersections = intersections_segment_circle_integers( point, point_after, this->mouse_position_pixels, squared_radius, mu0, mu1); if (nb_intersections != 2) { /* No intersection with the infinite line : none of the points are inside the circle. * If only one intersection was found, then the eraser is tangential to the line, we don't * account for intersections in this case. */ r_mu0 = r_mu1 = -1.0f; r_point_side = PointCircleSide::Outside; r_point_after_side = PointCircleSide::Outside; return 0; } if (mu0 > mu1) { std::swap(mu0, mu1); } /* Compute on which side of the segment each intersection lies. * -1 : before or at the first endpoint, * 0 : in-between the endpoints, * 1 : after or at the last endpoint. */ const int8_t side_mu0 = (mu0 <= 0) ? (-1) : ((mu0 >= segment_length) ? 1 : 0); const int8_t side_mu1 = (mu1 <= 0) ? (-1) : ((mu1 >= segment_length) ? 1 : 0); /* The endpoints are on the circle's boundary if one of the intersection falls exactly on them. */ r_point_side = (mu0 == 0) ? PointCircleSide::OutsideInsideBoundary : ((mu1 == 0) ? PointCircleSide::InsideOutsideBoundary : PointCircleSide::Inside); r_point_after_side = (mu0 == segment_length) ? PointCircleSide::OutsideInsideBoundary : ((mu1 == segment_length) ? PointCircleSide::InsideOutsideBoundary : PointCircleSide::Inside); /* Compute the normalized position of the intersection in the curve. */ r_mu0 = mu0 / float(segment_length); r_mu1 = mu1 / float(segment_length); const bool is_mu0_inside = (side_mu0 == 0); const bool is_mu1_inside = (side_mu1 == 0); if (!is_mu0_inside && !is_mu1_inside) { /* None of the intersection lie within the segment the infinite line. */ if (side_mu0 == side_mu1) { /* If they are on the same side of the line, then none of the point are inside the circle. */ r_point_side = PointCircleSide::Outside; r_point_after_side = PointCircleSide::Outside; return 0; } /* If they are on different sides of the line, then both points are inside the circle, or in * the boundary. */ return 0; } if (is_mu0_inside && is_mu1_inside) { /* Both intersections lie within the segment, none of the points are inside the circle. */ r_point_side = PointCircleSide::Outside; r_point_after_side = PointCircleSide::Outside; return 2; } /* Only one intersection lies within the segment. Only one point should be erased, depending on * the side of the other intersection. */ const int8_t side_outside_intersection = is_mu0_inside ? side_mu1 : side_mu0; /* If the other intersection lies before the first endpoint, the first endpoint is inside. */ r_point_side = (side_outside_intersection == -1) ? r_point_side : PointCircleSide::Outside; r_point_after_side = (side_outside_intersection == 1) ? r_point_after_side : PointCircleSide::Outside; if (is_mu1_inside) { std::swap(r_mu0, r_mu1); } return 1; } /** * Compute intersections between the eraser and the input \a src Curves Geometry. * Also computes if the points of the geometry lie inside/outside, or in the boundary of the * eraser. * * \param screen_space_positions: 2D positions of the geometry in screen space. * * \param intersections_max_per_segment: maximum number of intersections per-segment. * * \param r_point_side: (output) for each point in the source, enum describing where the point * lies relatively to the eraser: inside, outside or at the boundary of the eraser. * * \param r_intersections: (output) array containing all the intersections found in the curves * geometry. The size of the array should be `src.points_num*intersections_max_per_segment`. * Initially all intersections are set as invalid, and the function fills valid intersections at * an offset of `src_point*intersections_max_per_segment`. * * \returns total number of intersections found. */ int curves_intersections_and_points_sides( const bke::CurvesGeometry &src, const Span screen_space_positions, const int intersections_max_per_segment, MutableSpan r_point_side, MutableSpan r_intersections) const { const OffsetIndices src_points_by_curve = src.points_by_curve(); const VArray src_cyclic = src.cyclic(); Array screen_space_positions_pixel(src.points_num()); threading::parallel_for(src.points_range(), 1024, [&](const IndexRange src_points) { for (const int src_point : src_points) { const float2 pos = screen_space_positions[src_point]; screen_space_positions_pixel[src_point] = int2(round_fl_to_int(pos[0]), round_fl_to_int(pos[1])); } }); threading::parallel_for(src.curves_range(), 512, [&](const IndexRange src_curves) { for (const int src_curve : src_curves) { const IndexRange src_curve_points = src_points_by_curve[src_curve]; if (src_curve_points.size() == 1) { /* One-point stroke : just check if the point is inside the eraser. */ const int src_point = src_curve_points.first(); const int64_t squared_distance = math::distance_squared( this->mouse_position_pixels, screen_space_positions_pixel[src_point]); /* Note: We don't account for boundaries here, since we are not going to split any * curve. */ r_point_side[src_point] = (squared_distance <= this->eraser_squared_radius_pixels) ? PointCircleSide::Inside : PointCircleSide::Outside; continue; } for (const int src_point : src_curve_points.drop_back(1)) { SegmentCircleIntersection inter0; SegmentCircleIntersection inter1; const int8_t nb_inter = segment_intersections_and_points_sides( screen_space_positions_pixel[src_point], screen_space_positions_pixel[src_point + 1], this->eraser_squared_radius_pixels, inter0.factor, inter1.factor, r_point_side[src_point], r_point_side[src_point + 1]); if (nb_inter > 0) { const int intersection_offset = src_point * intersections_max_per_segment; inter0.inside_outside_intersection = (inter0.factor > inter1.factor); r_intersections[intersection_offset + 0] = inter0; if (nb_inter > 1) { inter1.inside_outside_intersection = true; r_intersections[intersection_offset + 1] = inter1; } } } if (src_cyclic[src_curve]) { /* If the curve is cyclic, we need to check for the closing segment. */ const int src_last_point = src_curve_points.last(); const int src_first_point = src_curve_points.first(); SegmentCircleIntersection inter0; SegmentCircleIntersection inter1; const int8_t nb_inter = segment_intersections_and_points_sides( screen_space_positions_pixel[src_last_point], screen_space_positions_pixel[src_first_point], this->eraser_squared_radius_pixels, inter0.factor, inter1.factor, r_point_side[src_last_point], r_point_side[src_first_point]); if (nb_inter > 0) { const int intersection_offset = src_last_point * intersections_max_per_segment; inter0.inside_outside_intersection = (inter0.factor > inter1.factor); r_intersections[intersection_offset + 0] = inter0; if (nb_inter > 1) { inter1.inside_outside_intersection = true; r_intersections[intersection_offset + 1] = inter1; } } } } }); /* Compute total number of intersections. */ int total_intersections = 0; for (const SegmentCircleIntersection &intersection : r_intersections) { if (intersection.is_valid()) { total_intersections++; } } return total_intersections; } /** * Structure describing a point in the destination relatively to the source. * If a point in the destination \a is_src_point, then it corresponds * exactly to the point at \a src_point index in the source geometry. * Otherwise, it is a linear combination of points at \a src_point and \a src_next_point in the * source geometry, with the given \a factor. * A point in the destination is a \a cut if it splits the source curves geometry, meaning it is * the first point of a new curve in the destination. */ struct PointTransferData { int src_point; int src_next_point; float factor; bool is_src_point; bool is_cut; }; /** * Computes a \a dst curves geometry by applying a change of topology from a \a src curves * geometry. * The change of topology is described by \a src_to_dst_points, which size should be * equal to the number of points in the source. * For each point in the source, the corresponding vector in \a src_to_dst_points contains a set * of destination points (PointTransferData), which can correspond to points of the source, or * linear combination of them. Note that this vector can be empty, if we want to remove points * for example. Curves can also be split if a destination point is marked as a cut. * * \returns an array containing the same elements as \a src_to_dst_points, but in the destination * points domain. */ static Array compute_topology_change( const bke::CurvesGeometry &src, bke::CurvesGeometry &dst, const Span> src_to_dst_points, const bool keep_caps) { const int src_curves_num = src.curves_num(); const OffsetIndices src_points_by_curve = src.points_by_curve(); const VArray src_cyclic = src.cyclic(); int dst_points_num = 0; for (const Vector &src_transfer_data : src_to_dst_points) { dst_points_num += src_transfer_data.size(); } if (dst_points_num == 0) { dst.resize(0, 0); return Array(0); } /* Set the intersection parameters in the destination domain : a pair of int and float * numbers for which the integer is the index of the corresponding segment in the * source curves, and the float part is the (0,1) factor representing its position in * the segment. */ Array dst_transfer_data(dst_points_num); Array src_pivot_point(src_curves_num, -1); Array dst_interm_curves_offsets(src_curves_num + 1, 0); int dst_point = -1; for (const int src_curve : src.curves_range()) { const IndexRange src_points = src_points_by_curve[src_curve]; for (const int src_point : src_points) { for (const PointTransferData &dst_point_transfer : src_to_dst_points[src_point]) { if (dst_point_transfer.is_src_point) { dst_transfer_data[++dst_point] = dst_point_transfer; continue; } /* Add an intersection with the eraser and mark it as a cut. */ dst_transfer_data[++dst_point] = dst_point_transfer; /* For cyclic curves, mark the pivot point as the last intersection with the eraser * that starts a new segment in the destination. */ if (src_cyclic[src_curve] && dst_point_transfer.is_cut) { src_pivot_point[src_curve] = dst_point; } } } /* We store intermediate curve offsets represent an intermediate state of the * destination curves before cutting the curves at eraser's intersection. Thus, it * contains the same number of curves than in the source, but the offsets are * different, because points may have been added or removed. */ dst_interm_curves_offsets[src_curve + 1] = dst_point + 1; } /* Cyclic curves. */ Array src_now_cyclic(src_curves_num); threading::parallel_for(src.curves_range(), 4096, [&](const IndexRange src_curves) { for (const int src_curve : src_curves) { const int pivot_point = src_pivot_point[src_curve]; if (pivot_point == -1) { /* Either the curve was not cyclic or it wasn't cut : no need to change it. */ src_now_cyclic[src_curve] = src_cyclic[src_curve]; continue; } /* A cyclic curve was cut : * - this curve is not cyclic anymore, * - and we have to shift points to keep the closing segment. */ src_now_cyclic[src_curve] = false; const int dst_interm_first = dst_interm_curves_offsets[src_curve]; const int dst_interm_last = dst_interm_curves_offsets[src_curve + 1]; std::rotate(dst_transfer_data.begin() + dst_interm_first, dst_transfer_data.begin() + pivot_point, dst_transfer_data.begin() + dst_interm_last); } }); /* Compute the destination curve offsets. */ Vector dst_curves_offset; Vector dst_to_src_curve; dst_curves_offset.append(0); for (int src_curve : src.curves_range()) { const IndexRange dst_points(dst_interm_curves_offsets[src_curve], dst_interm_curves_offsets[src_curve + 1] - dst_interm_curves_offsets[src_curve]); int length_of_current = 0; for (int dst_point : dst_points) { if ((length_of_current > 0) && dst_transfer_data[dst_point].is_cut) { /* This is the new first point of a curve. */ dst_curves_offset.append(dst_point); dst_to_src_curve.append(src_curve); length_of_current = 0; } ++length_of_current; } if (length_of_current != 0) { /* End of a source curve. */ dst_curves_offset.append(dst_points.one_after_last()); dst_to_src_curve.append(src_curve); } } const int dst_curves_num = dst_curves_offset.size() - 1; if (dst_curves_num == 0) { dst.resize(0, 0); return dst_transfer_data; } /* Build destination curves geometry. */ dst.resize(dst_points_num, dst_curves_num); array_utils::copy(dst_curves_offset.as_span(), dst.offsets_for_write()); const OffsetIndices dst_points_by_curve = dst.points_by_curve(); /* Attributes. */ const bke::AttributeAccessor src_attributes = src.attributes(); bke::MutableAttributeAccessor dst_attributes = dst.attributes_for_write(); const bke::AnonymousAttributePropagationInfo propagation_info{}; /* Copy curves attributes. */ bke::gather_attributes(src_attributes, ATTR_DOMAIN_CURVE, propagation_info, {"cyclic"}, dst_to_src_curve, dst_attributes); if (src_cyclic.get_if_single().value_or(true)) { array_utils::gather( src_now_cyclic.as_span(), dst_to_src_curve.as_span(), dst.cyclic_for_write()); } dst.update_curve_types(); /* Display intersections with flat caps. */ if (!keep_caps) { bke::SpanAttributeWriter dst_start_caps = dst_attributes.lookup_or_add_for_write_span("start_cap", ATTR_DOMAIN_CURVE); bke::SpanAttributeWriter dst_end_caps = dst_attributes.lookup_or_add_for_write_span("end_cap", ATTR_DOMAIN_CURVE); threading::parallel_for(dst.curves_range(), 4096, [&](const IndexRange dst_curves) { for (const int dst_curve : dst_curves) { const IndexRange dst_curve_points = dst_points_by_curve[dst_curve]; if (dst_transfer_data[dst_curve_points.first()].is_cut) { dst_start_caps.span[dst_curve] = GP_STROKE_CAP_TYPE_FLAT; } if (dst_curve == dst_curves.last()) { continue; } const PointTransferData &next_point_transfer = dst_transfer_data[dst_points_by_curve[dst_curve + 1].first()]; if (next_point_transfer.is_cut) { dst_end_caps.span[dst_curve] = GP_STROKE_CAP_TYPE_FLAT; } } }); dst_start_caps.finish(); dst_end_caps.finish(); } /* Copy/Interpolate point attributes. */ for (bke::AttributeTransferData &attribute : bke::retrieve_attributes_for_transfer( src_attributes, dst_attributes, ATTR_DOMAIN_MASK_POINT, propagation_info)) { bke::attribute_math::convert_to_static_type(attribute.dst.span.type(), [&](auto dummy) { using T = decltype(dummy); auto src_attr = attribute.src.typed(); auto dst_attr = attribute.dst.span.typed(); threading::parallel_for(dst.points_range(), 4096, [&](const IndexRange dst_points) { for (const int dst_point : dst_points) { const PointTransferData &point_transfer = dst_transfer_data[dst_point]; if (point_transfer.is_src_point) { dst_attr[dst_point] = src_attr[point_transfer.src_point]; } else { dst_attr[dst_point] = bke::attribute_math::mix2( point_transfer.factor, src_attr[point_transfer.src_point], src_attr[point_transfer.src_next_point]); } } }); attribute.dst.finish(); }); } return dst_transfer_data; } /* The hard eraser cuts out the curves at their intersection with the eraser, and removes * everything that lies in-between two consecutive intersections. Note that intersections are * computed using integers (pixel-space) to avoid floating-point approximation errors. */ bool hard_eraser(const bke::CurvesGeometry &src, const Span screen_space_positions, bke::CurvesGeometry &dst, const bool keep_caps) const { const VArray src_cyclic = src.cyclic(); const int src_points_num = src.points_num(); /* For the hard erase, we compute with a circle, so there can only be a maximum of two * intersection per segment. */ const int intersections_max_per_segment = 2; /* Compute intersections between the eraser and the curves in the source domain. */ Array src_point_side(src_points_num, PointCircleSide::Outside); Array src_intersections(src_points_num * intersections_max_per_segment); curves_intersections_and_points_sides(src, screen_space_positions, intersections_max_per_segment, src_point_side, src_intersections); Array> src_to_dst_points(src_points_num); const OffsetIndices src_points_by_curve = src.points_by_curve(); for (const int src_curve : src.curves_range()) { const IndexRange src_points = src_points_by_curve[src_curve]; for (const int src_point : src_points) { Vector &dst_points = src_to_dst_points[src_point]; const int src_next_point = (src_point == src_points.last()) ? src_points.first() : (src_point + 1); const PointCircleSide point_side = src_point_side[src_point]; /* Add the source point only if it does not lie inside of the eraser. */ if (point_side != PointCircleSide::Inside) { dst_points.append({src_point, src_next_point, 0.0f, true, (point_side == PointCircleSide::InsideOutsideBoundary)}); } /* Add all intersections with the eraser. */ const IndexRange src_point_intersections(src_point * intersections_max_per_segment, intersections_max_per_segment); for (const SegmentCircleIntersection &intersection : src_intersections.as_span().slice(src_point_intersections)) { if (!intersection.is_valid()) { /* Stop at the first non valid intersection. */ break; } dst_points.append({src_point, src_next_point, intersection.factor, false, intersection.inside_outside_intersection}); } } } compute_topology_change(src, dst, src_to_dst_points, keep_caps); return true; } bool stroke_eraser(const bke::CurvesGeometry &src, const Span screen_space_positions, bke::CurvesGeometry &dst) const { const OffsetIndices src_points_by_curve = src.points_by_curve(); const VArray src_cyclic = src.cyclic(); IndexMaskMemory memory; const IndexMask strokes_to_keep = IndexMask::from_predicate( src.curves_range(), GrainSize(256), memory, [&](const int src_curve) { const IndexRange src_curve_points = src_points_by_curve[src_curve]; /* One-point stroke : remove the stroke if the point lies inside of the eraser. */ if (src_curve_points.size() == 1) { const float2 &point_pos = screen_space_positions[src_curve_points.first()]; const float dist_to_eraser = math::distance(point_pos, this->mouse_position); return !(dist_to_eraser < eraser_radius); } /* If any segment of the stroke is closer to the eraser than its radius, then remove * the stroke. */ for (const int src_point : src_curve_points.drop_back(1)) { const float dist_to_eraser = dist_to_line_segment_v2( this->mouse_position, screen_space_positions[src_point], screen_space_positions[src_point + 1]); if (dist_to_eraser < this->eraser_radius) { return false; } } if (src_cyclic[src_curve]) { const float dist_to_eraser = dist_to_line_segment_v2( this->mouse_position, screen_space_positions[src_curve_points.first()], screen_space_positions[src_curve_points.last()]); if (dist_to_eraser < this->eraser_radius) { return false; } } return true; }); if (strokes_to_keep.size() == src.curves_num()) { return false; } dst = bke::curves_copy_curve_selection(src, strokes_to_keep, {}); return true; } void execute(EraseOperation &self, const bContext &C, const InputSample &extension_sample) { using namespace blender::bke::greasepencil; Scene *scene = CTX_data_scene(&C); Depsgraph *depsgraph = CTX_data_depsgraph_pointer(&C); ARegion *region = CTX_wm_region(&C); Object *obact = CTX_data_active_object(&C); Object *ob_eval = DEG_get_evaluated_object(depsgraph, obact); Paint *paint = &scene->toolsettings->gp_paint->paint; Brush *brush = BKE_paint_brush(paint); /* Get the tool's data. */ this->mouse_position = extension_sample.mouse_position; this->eraser_radius = self.radius; if (BKE_brush_use_size_pressure(brush)) { this->eraser_radius *= BKE_curvemapping_evaluateF( brush->gpencil_settings->curve_strength, 0, extension_sample.pressure); } this->mouse_position_pixels = int2(round_fl_to_int(mouse_position[0]), round_fl_to_int(mouse_position[1])); const int64_t eraser_radius_pixels = round_fl_to_int(eraser_radius); this->eraser_squared_radius_pixels = eraser_radius_pixels * eraser_radius_pixels; /* Get the grease pencil drawing. */ GreasePencil &grease_pencil = *static_cast(obact->data); bool changed = false; const auto execute_eraser_on_drawing = [&](const int layer_index, const int frame_number, Drawing &drawing) { const bke::CurvesGeometry &src = drawing.strokes(); /* Evaluated geometry. */ bke::crazyspace::GeometryDeformation deformation = bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation( ob_eval, *obact, layer_index, frame_number); /* Compute screen space positions. */ Array screen_space_positions(src.points_num()); threading::parallel_for(src.points_range(), 4096, [&](const IndexRange src_points) { for (const int src_point : src_points) { ED_view3d_project_float_global( region, math::transform_point(transforms_.layer_space_to_world_space, deformation.positions[src_point]), screen_space_positions[src_point], V3D_PROJ_TEST_NOP); } }); /* Erasing operator. */ bke::CurvesGeometry dst; bool erased = false; switch (self.eraser_mode) { case GP_BRUSH_ERASER_STROKE: erased = stroke_eraser(src, screen_space_positions, dst); break; case GP_BRUSH_ERASER_HARD: erased = hard_eraser(src, screen_space_positions, dst, self.keep_caps); break; case GP_BRUSH_ERASER_SOFT: // To be implemented return; } if (erased) { /* Set the new geometry. */ drawing.geometry.wrap() = std::move(dst); drawing.tag_topology_changed(); changed = true; } }; if (self.active_layer_only) { /* Erase only on the drawing at the current frame of the active layer. */ const Layer *active_layer = grease_pencil.get_active_layer(); Drawing *drawing = grease_pencil.get_editable_drawing_at(active_layer, scene->r.cfra); if (drawing == nullptr) { return; } execute_eraser_on_drawing( active_layer->drawing_index_at(scene->r.cfra), scene->r.cfra, *drawing); } else { /* Erase on all editable drawings. */ const Array drawings = ed::greasepencil::retrieve_editable_drawings(*scene, grease_pencil); threading::parallel_for_each( drawings, [&](const ed::greasepencil::MutableDrawingInfo &info) { execute_eraser_on_drawing(info.layer_index, info.frame_number, info.drawing); }); } if (changed) { DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY); WM_event_add_notifier(&C, NC_GEOM | ND_DATA, &grease_pencil); } } }; void EraseOperation::on_stroke_begin(const bContext &C, const InputSample & /*start_sample*/) { Scene *scene = CTX_data_scene(&C); Paint *paint = BKE_paint_get_active_from_context(&C); Brush *brush = BKE_paint_brush(paint); BLI_assert(brush->gpencil_settings != nullptr); BKE_curvemapping_init(brush->gpencil_settings->curve_strength); this->radius = BKE_brush_size_get(scene, brush); this->eraser_mode = eGP_BrushEraserMode(brush->gpencil_settings->eraser_mode); this->keep_caps = ((brush->gpencil_settings->flag & GP_BRUSH_ERASER_KEEP_CAPS) != 0); this->active_layer_only = ((brush->gpencil_settings->flag & GP_BRUSH_ACTIVE_LAYER_ONLY) != 0); } void EraseOperation::on_stroke_extended(const bContext &C, const InputSample &extension_sample) { EraseOperationExecutor executor{C}; executor.execute(*this, C, extension_sample); } void EraseOperation::on_stroke_done(const bContext & /*C*/) {} std::unique_ptr new_erase_operation() { return std::make_unique(); } } // namespace blender::ed::sculpt_paint::greasepencil