Refactor: GPv3: Move core cutter function

This moves the core of the cutter tool to
`ed::greasepencil::cutter::trim_curve_segments`.
This is in preperation for the draw tool which
also needs to be able to trim the stroke.

No functional changes expected.
This commit is contained in:
Falk David 2024-07-04 18:08:24 +02:00
parent a2482ab450
commit 969efcad7b
3 changed files with 567 additions and 560 deletions

@ -32,359 +32,56 @@
namespace blender::ed::greasepencil {
enum Side : uint8_t { Start = 0, End = 1 };
enum Distance : uint8_t { Min = 0, Max = 1 };
/**
* Structure describing a curve segment (a point range in a curve) that needs to be removed from
* the curve.
*/
struct CutterSegment {
/* Curve index. */
int curve;
/* Point range of the segment: starting point and end point. Matches the point offsets
* in a CurvesGeometry. */
int point_range[2];
/* The normalized distance where the cutter segment is intersected by another curve.
* For the outer ends of the cutter segment the intersection distance is given between:
* - [start point - 1] and [start point]
* - [end point] and [end point + 1]
*/
float intersection_distance[2];
/* Intersection flag: true if the start/end point of the segment is the result of an
* intersection, false if the point is the outer end of a curve. */
bool is_intersected[2];
};
/**
* Structure describing:
* - A collection of cutter segments.
*/
struct CutterSegments {
/* Collection of cutter segments: parts of curves between other curves, to be removed from the
* geometry. */
Vector<CutterSegment> segments;
/* Create an initial cutter segment with a point range of one point. */
CutterSegment *create_segment(const int curve, const int point)
{
CutterSegment segment{};
segment.curve = curve;
segment.point_range[Side::Start] = point;
segment.point_range[Side::End] = point;
this->segments.append(std::move(segment));
return &this->segments.last();
}
/* Merge cutter segments that are next to each other. */
void merge_adjacent_segments()
{
Vector<CutterSegment> merged_segments;
/* Note on performance: we deal with small numbers here, so we can afford the double loop. */
while (!this->segments.is_empty()) {
CutterSegment a = this->segments.pop_last();
bool merged = false;
for (CutterSegment &b : merged_segments) {
if (a.curve != b.curve) {
continue;
}
/* The segments overlap when the points ranges have overlap or are exactly adjacent. */
if ((a.point_range[Side::Start] <= b.point_range[Side::End] &&
a.point_range[Side::End] >= b.point_range[Side::Start]) ||
(a.point_range[Side::End] == b.point_range[Side::Start] - 1) ||
(b.point_range[Side::End] == a.point_range[Side::Start] - 1))
{
/* Merge the point ranges and related intersection data. */
const bool take_start_a = a.point_range[Side::Start] < b.point_range[Side::Start];
const bool take_end_a = a.point_range[Side::End] > b.point_range[Side::End];
b.point_range[Side::Start] = take_start_a ? a.point_range[Side::Start] :
b.point_range[Side::Start];
b.point_range[Side::End] = take_end_a ? a.point_range[Side::End] :
b.point_range[Side::End];
b.is_intersected[Side::Start] = take_start_a ? a.is_intersected[Side::Start] :
b.is_intersected[Side::Start];
b.is_intersected[Side::End] = take_end_a ? a.is_intersected[Side::End] :
b.is_intersected[Side::End];
b.intersection_distance[Side::Start] = take_start_a ?
a.intersection_distance[Side::Start] :
b.intersection_distance[Side::Start];
b.intersection_distance[Side::End] = take_end_a ? a.intersection_distance[Side::End] :
b.intersection_distance[Side::End];
merged = true;
break;
}
}
if (!merged) {
merged_segments.append(std::move(a));
}
}
this->segments = merged_segments;
}
};
/* When looking for intersections, we need a little padding, otherwise we could miss curves
* that intersect for the eye, but not in hard numbers. */
static constexpr int BBOX_PADDING = 2;
/* When creating new intersection points, we don't want them too close to their neighbor,
* because that clutters the geometry. This threshold defines what 'too close' is. */
static constexpr float DISTANCE_FACTOR_THRESHOLD = 0.01f;
/**
* Get the intersection distance of two line segments a-b and c-d.
* The intersection distance is defined as the normalized distance (0..1)
* from point a to the intersection point of a-b and c-d.
* Apply the stroke cutter to a drawing.
*/
static float get_intersection_distance_of_segments(const float2 &co_a,
const float2 &co_b,
const float2 &co_c,
const float2 &co_d)
{
/* Get intersection point. */
const float a1 = co_b[1] - co_a[1];
const float b1 = co_a[0] - co_b[0];
const float c1 = a1 * co_a[0] + b1 * co_a[1];
const float a2 = co_d[1] - co_c[1];
const float b2 = co_c[0] - co_d[0];
const float c2 = a2 * co_c[0] + b2 * co_c[1];
const float det = float(a1 * b2 - a2 * b1);
if (det == 0.0f) {
return 0.0f;
}
float2 isect((b2 * c1 - b1 * c2) / det, (a1 * c2 - a2 * c1) / det);
/* Get normalized distance from point a to intersection point. */
const float length_ab = math::length(co_b - co_a);
float distance = (length_ab == 0.0f ?
0.0f :
math::clamp(math::length(isect - co_a) / length_ab, 0.0f, 1.0f));
return distance;
}
/**
* For a curve, find all intersections with other curves.
*/
static void get_intersections_of_curve_with_curves(const int src_curve,
const bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
const Span<rcti> screen_space_bbox,
MutableSpan<bool> r_is_intersected_after_point,
MutableSpan<float2> r_intersection_distance)
{
const OffsetIndices<int> points_by_curve = src.points_by_curve();
const VArray<bool> is_cyclic = src.cyclic();
/* Edge case: skip curve with only one point. */
if (points_by_curve[src_curve].size() < 2) {
return;
}
/* Loop all curve points and check for intersections between point a and point a + 1. */
const IndexRange src_curve_points = points_by_curve[src_curve].drop_back(
is_cyclic[src_curve] ? 0 : 1);
for (const int point_a : src_curve_points) {
const int point_b = (point_a == points_by_curve[src_curve].last()) ? src_curve_points.first() :
point_a + 1;
/* Get coordinates of segment a-b. */
const float2 co_a = screen_space_positions[point_a];
const float2 co_b = screen_space_positions[point_b];
rcti bbox_ab;
BLI_rcti_init_minmax(&bbox_ab);
BLI_rcti_do_minmax_v(&bbox_ab, int2(co_a));
BLI_rcti_do_minmax_v(&bbox_ab, int2(co_b));
BLI_rcti_pad(&bbox_ab, BBOX_PADDING, BBOX_PADDING);
float intersection_distance_min = FLT_MAX;
float intersection_distance_max = -FLT_MAX;
/* Loop all curves, looking for intersecting segments. */
for (const int curve : src.curves_range()) {
/* Only process curves with at least two points. */
if (points_by_curve[curve].size() < 2) {
continue;
}
/* Bounding box check: skip curves that don't overlap segment a-b. */
if (!BLI_rcti_isect(&bbox_ab, &screen_space_bbox[curve], nullptr)) {
continue;
}
/* Find intersecting curve segments. */
const IndexRange points = points_by_curve[curve].drop_back(is_cyclic[curve] ? 0 : 1);
for (const int point_c : points) {
const int point_d = (point_c == points_by_curve[curve].last()) ? points.first() :
(point_c + 1);
/* Don't self check. */
if (curve == src_curve &&
(point_a == point_c || point_a == point_d || point_b == point_c || point_b == point_d))
{
continue;
}
/* Skip when bounding boxes of a-b and c-d don't overlap. */
const float2 co_c = screen_space_positions[point_c];
const float2 co_d = screen_space_positions[point_d];
rcti bbox_cd;
BLI_rcti_init_minmax(&bbox_cd);
BLI_rcti_do_minmax_v(&bbox_cd, int2(co_c));
BLI_rcti_do_minmax_v(&bbox_cd, int2(co_d));
BLI_rcti_pad(&bbox_cd, BBOX_PADDING, BBOX_PADDING);
if (!BLI_rcti_isect(&bbox_ab, &bbox_cd, nullptr)) {
continue;
}
/* Add some padding to the line segment c-d, otherwise we could just miss an
* intersection. */
const float2 padding_cd = math::normalize(co_d - co_c);
const float2 padded_c = co_c - padding_cd;
const float2 padded_d = co_d + padding_cd;
/* Check for intersection. */
const auto isect = math::isect_seg_seg(co_a, co_b, padded_c, padded_d);
if (ELEM(isect.kind, isect.LINE_LINE_CROSS, isect.LINE_LINE_EXACT)) {
/* We found an intersection, set the intersection flag for segment a-b. */
r_is_intersected_after_point[point_a] = true;
/* Calculate the intersection factor. This is the normalized distance (0..1) of the
* intersection point on line segment a-b, measured from point a. */
const float normalized_distance = get_intersection_distance_of_segments(
co_a, co_b, co_c, co_d);
intersection_distance_min = math::min(normalized_distance, intersection_distance_min);
intersection_distance_max = math::max(normalized_distance, intersection_distance_max);
}
}
}
if (r_is_intersected_after_point[point_a]) {
r_intersection_distance[point_a][Distance::Min] = intersection_distance_min;
r_intersection_distance[point_a][Distance::Max] = intersection_distance_max;
}
}
}
/**
* Expand a cutter segment by walking along the curve in forward or backward direction.
* A cutter segments ends at an intersection with another curve, or at the outer end of the curve.
*/
static void expand_cutter_segment_direction(CutterSegment &segment,
const int direction,
const bke::CurvesGeometry &src,
const Span<bool> is_intersected_after_point,
const Span<float2> intersection_distance,
MutableSpan<bool> point_is_in_segment)
{
const OffsetIndices<int> points_by_curve = src.points_by_curve();
const int point_first = points_by_curve[segment.curve].first();
const int point_last = points_by_curve[segment.curve].last();
const Side segment_side = (direction == 1) ? Side::End : Side::Start;
int point_a = segment.point_range[segment_side];
bool intersected = false;
segment.is_intersected[segment_side] = false;
/* Walk along the curve points. */
while ((direction == 1 && point_a < point_last) || (direction == -1 && point_a > point_first)) {
const int point_b = point_a + direction;
const bool at_end_of_curve = (direction == -1 && point_b == point_first) ||
(direction == 1 && point_b == point_last);
/* Expand segment point range. */
segment.point_range[segment_side] = point_a;
point_is_in_segment[point_a] = true;
/* Check for intersections with other curves. The intersections were established in ascending
* point order, so in forward direction we look at line segment a-b, in backward direction we
* look at line segment b-a. */
const int intersection_point = direction == 1 ? point_a : point_b;
intersected = is_intersected_after_point[intersection_point];
/* Avoid orphaned points at the end of a curve. */
if (at_end_of_curve &&
((direction == -1 &&
intersection_distance[intersection_point][Distance::Max] < DISTANCE_FACTOR_THRESHOLD) ||
(direction == 1 && intersection_distance[intersection_point][Distance::Min] >
(1.0f - DISTANCE_FACTOR_THRESHOLD))))
{
intersected = false;
break;
}
/* When we hit an intersection, store the intersection distance. Potentially, line segment
* a-b can be intersected by multiple curves, so we want to fetch the first intersection
* point we bumped into. In forward direction this is the minimum distance, in backward
* direction the maximum. */
if (intersected) {
segment.is_intersected[segment_side] = true;
segment.intersection_distance[segment_side] =
(direction == 1) ? intersection_distance[intersection_point][Distance::Min] :
intersection_distance[intersection_point][Distance::Max];
break;
}
/* Keep walking along curve. */
point_a += direction;
}
/* Adjust point range at curve ends. */
if (!intersected) {
if (direction == -1) {
segment.point_range[Side::Start] = point_first;
point_is_in_segment[point_first] = true;
}
else {
segment.point_range[Side::End] = point_last;
point_is_in_segment[point_last] = true;
}
}
}
/**
* Expand a cutter segment of one point by walking along the curve in both directions.
*/
static void expand_cutter_segment(CutterSegment &segment,
const bke::CurvesGeometry &src,
const Span<bool> is_intersected_after_point,
const Span<float2> intersection_distance,
MutableSpan<bool> point_is_in_segment)
{
const int8_t directions[2] = {-1, 1};
for (const int8_t direction : directions) {
expand_cutter_segment_direction(segment,
direction,
src,
is_intersected_after_point,
intersection_distance,
point_is_in_segment);
}
}
/**
* Find curve points within the lasso area, expand them to segments between other curves and
* delete them from the geometry.
*/
static std::optional<bke::CurvesGeometry> stroke_cutter_find_and_remove_segments(
const bke::CurvesGeometry &src,
const Span<int2> mcoords,
const Span<float2> screen_space_positions,
const Span<rcti> screen_space_bbox,
const bool keep_caps)
static bool execute_cutter_on_drawing(const int layer_index,
const int frame_number,
const Object &ob_eval,
const Object &obact,
const ARegion &region,
const float4x4 &projection,
const Span<int2> mcoords,
const bool keep_caps,
bke::greasepencil::Drawing &drawing)
{
const bke::CurvesGeometry &src = drawing.strokes();
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
/* Get 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<float2> screen_space_positions(src.points_num());
threading::parallel_for(src.points_range(), 4096, [&](const IndexRange src_points) {
for (const int src_point : src_points) {
screen_space_positions[src_point] = ED_view3d_project_float_v2_m4(
&region, deformation.positions[src_point], projection);
}
});
/* Compute bounding boxes of curves in screen space. The bounding boxes are used to speed
* up the search for intersecting curves. */
Array<rcti> screen_space_bbox(src.curves_num());
threading::parallel_for(src.curves_range(), 512, [&](const IndexRange src_curves) {
for (const int src_curve : src_curves) {
rcti *bbox = &screen_space_bbox[src_curve];
BLI_rcti_init_minmax(bbox);
const IndexRange src_points = src_points_by_curve[src_curve];
for (const int src_point : src_points) {
BLI_rcti_do_minmax_v(bbox, int2(screen_space_positions[src_point]));
}
/* Add some padding, otherwise we could just miss intersections. */
BLI_rcti_pad(bbox, BBOX_PADDING, BBOX_PADDING);
}
});
rcti bbox_lasso;
BLI_lasso_boundbox(&bbox_lasso, mcoords);
@ -418,225 +115,27 @@ static std::optional<bke::CurvesGeometry> stroke_cutter_find_and_remove_segments
}
}
IndexMaskMemory memory;
const IndexMask curve_selection = IndexMask::from_indices(selected_curves.as_span(), memory);
/* Abort when the lasso area is empty. */
if (selected_curves.is_empty()) {
return std::nullopt;
if (curve_selection.is_empty()) {
return false;
}
/* For the selected curves, find all the intersections with other curves. */
const int src_points_num = src.points_num();
Array<bool> is_intersected_after_point(src_points_num, false);
Array<float2> intersection_distance(src_points_num);
threading::parallel_for(selected_curves.index_range(), 1, [&](const IndexRange curve_range) {
for (const int selected_curve : curve_range) {
const int src_curve = selected_curves[selected_curve];
get_intersections_of_curve_with_curves(src_curve,
src,
screen_space_positions,
screen_space_bbox,
is_intersected_after_point,
intersection_distance);
}
});
/* Expand the selected curve points to cutter segments (the part of the curve between two
* intersections). */
const VArray<bool> is_cyclic = src.cyclic();
Array<bool> point_is_in_segment(src_points_num, false);
threading::EnumerableThreadSpecific<CutterSegments> cutter_segments_by_thread;
threading::parallel_for(selected_curves.index_range(), 1, [&](const IndexRange curve_range) {
for (const int selected_curve : curve_range) {
CutterSegments &thread_segments = cutter_segments_by_thread.local();
const int src_curve = selected_curves[selected_curve];
for (const int selected_point : selected_points_in_curves[selected_curve]) {
/* Skip point when it is already part of a cutter segment. */
if (point_is_in_segment[selected_point]) {
continue;
}
/* Create new cutter segment. */
CutterSegment *segment = thread_segments.create_segment(src_curve, selected_point);
/* Expand the cutter segment in both directions until an intersection is found or the
* end of the curve is reached. */
expand_cutter_segment(
*segment, src, is_intersected_after_point, intersection_distance, point_is_in_segment);
/* When the end of a curve is reached and the curve is cyclic, we add an extra cutter
* segment for the cyclic second part. */
if (is_cyclic[src_curve] &&
(!segment->is_intersected[Side::Start] || !segment->is_intersected[Side::End]) &&
!(!segment->is_intersected[Side::Start] && !segment->is_intersected[Side::End]))
{
const int cyclic_outer_point = !segment->is_intersected[Side::Start] ?
src_points_by_curve[src_curve].last() :
src_points_by_curve[src_curve].first();
segment = thread_segments.create_segment(src_curve, cyclic_outer_point);
/* Expand this second segment. */
expand_cutter_segment(*segment,
src,
is_intersected_after_point,
intersection_distance,
point_is_in_segment);
}
}
}
});
CutterSegments cutter_segments;
for (CutterSegments &thread_segments : cutter_segments_by_thread) {
cutter_segments.segments.extend(thread_segments.segments);
}
/* Abort when no cutter segments are found in the lasso area. */
if (cutter_segments.segments.is_empty()) {
return std::nullopt;
}
/* Merge adjacent cutter segments. E.g. two point ranges of 0-10 and 11-20 will be merged
* to one range of 0-20. */
cutter_segments.merge_adjacent_segments();
/* Create the point transfer data, for converting the source geometry into the new geometry.
* First, add all curve points not affected by the cutter tool. */
Array<Vector<PointTransferData>> src_to_dst_points(src_points_num);
for (const int src_curve : src.curves_range()) {
const IndexRange src_points = src_points_by_curve[src_curve];
for (const int src_point : src_points) {
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
const int src_next_point = (src_point == src_points.last()) ? src_points.first() :
(src_point + 1);
/* Add the source point only if it does not lie inside a cutter segment. */
if (!point_is_in_segment[src_point]) {
dst_points.append({src_point, src_next_point, 0.0f, true, false});
}
}
}
/* Add new curve points at the intersection points of the cutter segments.
*
* a b
* source curve o--------o---*---o--------o----*---o--------o
* ^ ^
* cutter segment |-----------------|
*
* o = existing curve point
* * = newly created curve point
*
* The curve points between *a and *b will be deleted.
* The source curve will be cut in two:
* - the first curve ends at *a
* - the second curve starts at *b
*
* We avoid inserting a new point very close to the adjacent one, because that's just adding
* clutter to the geometry.
*/
for (const CutterSegment &cutter_segment : cutter_segments.segments) {
/* Intersection at cutter segment start. */
if (cutter_segment.is_intersected[Side::Start] &&
cutter_segment.intersection_distance[Side::Start] > DISTANCE_FACTOR_THRESHOLD)
{
const int src_point = cutter_segment.point_range[Side::Start] - 1;
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
dst_points.append({src_point,
src_point + 1,
cutter_segment.intersection_distance[Side::Start],
false,
false});
}
/* Intersection at cutter segment end. */
if (cutter_segment.is_intersected[Side::End]) {
const int src_point = cutter_segment.point_range[Side::End];
if (cutter_segment.intersection_distance[Side::End] < (1.0f - DISTANCE_FACTOR_THRESHOLD)) {
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
dst_points.append({src_point,
src_point + 1,
cutter_segment.intersection_distance[Side::End],
false,
true});
}
else {
/* Mark the 'is_cut' flag on the next point, because a new curve is starting here after
* the removed cutter segment. */
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point + 1];
for (PointTransferData &dst_point : dst_points) {
if (dst_point.is_src_point) {
dst_point.is_cut = true;
}
}
}
}
}
/* Create the new curves geometry. */
bke::CurvesGeometry dst;
compute_topology_change(src, dst, src_to_dst_points, keep_caps);
return dst;
}
/**
* Apply the stroke cutter to a drawing.
*/
static bool execute_cutter_on_drawing(const int layer_index,
const int frame_number,
const Object &ob_eval,
const Object &obact,
const ARegion &region,
const float4x4 &projection,
const Span<int2> mcoords,
const bool keep_caps,
bke::greasepencil::Drawing &drawing)
{
const bke::CurvesGeometry &src = drawing.strokes();
/* Get 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<float2> screen_space_positions(src.points_num());
threading::parallel_for(src.points_range(), 4096, [&](const IndexRange src_points) {
for (const int src_point : src_points) {
screen_space_positions[src_point] = ED_view3d_project_float_v2_m4(
&region, deformation.positions[src_point], projection);
}
});
/* Compute bounding boxes of curves in screen space. The bounding boxes are used to speed
* up the search for intersecting curves. */
Array<rcti> screen_space_bbox(src.curves_num());
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
threading::parallel_for(src.curves_range(), 512, [&](const IndexRange src_curves) {
for (const int src_curve : src_curves) {
rcti *bbox = &screen_space_bbox[src_curve];
BLI_rcti_init_minmax(bbox);
const IndexRange src_points = src_points_by_curve[src_curve];
for (const int src_point : src_points) {
BLI_rcti_do_minmax_v(bbox, int2(screen_space_positions[src_point]));
}
/* Add some padding, otherwise we could just miss intersections. */
BLI_rcti_pad(bbox, BBOX_PADDING, BBOX_PADDING);
}
});
/* Apply cutter. */
std::optional<bke::CurvesGeometry> cut_strokes = stroke_cutter_find_and_remove_segments(
src, mcoords, screen_space_positions, screen_space_bbox, keep_caps);
bke::CurvesGeometry cut_strokes = ed::greasepencil::cutter::trim_curve_segments(
src,
screen_space_positions,
screen_space_bbox,
curve_selection,
selected_points_in_curves,
keep_caps);
if (cut_strokes.has_value()) {
/* Set the new geometry. */
drawing.geometry.wrap() = std::move(cut_strokes.value());
drawing.tag_topology_changed();
}
/* Set the new geometry. */
drawing.strokes_for_write() = std::move(cut_strokes);
drawing.tag_topology_changed();
return cut_strokes.has_value();
return true;
}
/**

@ -11,6 +11,7 @@
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_kdtree.h"
#include "BLI_math_vector.hh"
#include "BLI_rect.h"
#include "BLI_stack.hh"
#include "BKE_curves_utils.hh"
@ -714,4 +715,502 @@ bke::CurvesGeometry create_curves_outline(const bke::greasepencil::Drawing &draw
return dst_curves;
}
namespace cutter {
enum Side : uint8_t { Start = 0, End = 1 };
enum Distance : uint8_t { Min = 0, Max = 1 };
/* When looking for intersections, we need a little padding, otherwise we could miss curves
* that intersect for the eye, but not in hard numbers. */
static constexpr int BBOX_PADDING = 2;
/* When creating new intersection points, we don't want them too close to their neighbor,
* because that clutters the geometry. This threshold defines what 'too close' is. */
static constexpr float DISTANCE_FACTOR_THRESHOLD = 0.01f;
/**
* Structure describing a curve segment (a point range in a curve) that needs to be removed from
* the curve.
*/
struct Segment {
/* Curve index. */
int curve;
/* Point range of the segment: starting point and end point. Matches the point offsets
* in a CurvesGeometry. */
int point_range[2];
/* The normalized distance where the cutter segment is intersected by another curve.
* For the outer ends of the cutter segment the intersection distance is given between:
* - [start point - 1] and [start point]
* - [end point] and [end point + 1]
*/
float intersection_distance[2];
/* Intersection flag: true if the start/end point of the segment is the result of an
* intersection, false if the point is the outer end of a curve. */
bool is_intersected[2];
};
/**
* Structure describing:
* - A collection of cutter segments.
*/
struct Segments {
/* Collection of cutter segments: parts of curves between other curves, to be removed from the
* geometry. */
Vector<Segment> segments;
/* Create an initial cutter segment with a point range of one point. */
Segment *create_segment(const int curve, const int point)
{
Segment segment{};
segment.curve = curve;
segment.point_range[Side::Start] = point;
segment.point_range[Side::End] = point;
this->segments.append(std::move(segment));
return &this->segments.last();
}
/* Merge cutter segments that are next to each other. */
void merge_adjacent_segments()
{
Vector<Segment> merged_segments;
/* Note on performance: we deal with small numbers here, so we can afford the double loop. */
while (!this->segments.is_empty()) {
Segment a = this->segments.pop_last();
bool merged = false;
for (Segment &b : merged_segments) {
if (a.curve != b.curve) {
continue;
}
/* The segments overlap when the points ranges have overlap or are exactly adjacent. */
if ((a.point_range[Side::Start] <= b.point_range[Side::End] &&
a.point_range[Side::End] >= b.point_range[Side::Start]) ||
(a.point_range[Side::End] == b.point_range[Side::Start] - 1) ||
(b.point_range[Side::End] == a.point_range[Side::Start] - 1))
{
/* Merge the point ranges and related intersection data. */
const bool take_start_a = a.point_range[Side::Start] < b.point_range[Side::Start];
const bool take_end_a = a.point_range[Side::End] > b.point_range[Side::End];
b.point_range[Side::Start] = take_start_a ? a.point_range[Side::Start] :
b.point_range[Side::Start];
b.point_range[Side::End] = take_end_a ? a.point_range[Side::End] :
b.point_range[Side::End];
b.is_intersected[Side::Start] = take_start_a ? a.is_intersected[Side::Start] :
b.is_intersected[Side::Start];
b.is_intersected[Side::End] = take_end_a ? a.is_intersected[Side::End] :
b.is_intersected[Side::End];
b.intersection_distance[Side::Start] = take_start_a ?
a.intersection_distance[Side::Start] :
b.intersection_distance[Side::Start];
b.intersection_distance[Side::End] = take_end_a ? a.intersection_distance[Side::End] :
b.intersection_distance[Side::End];
merged = true;
break;
}
}
if (!merged) {
merged_segments.append(std::move(a));
}
}
this->segments = merged_segments;
}
};
/**
* Get the intersection distance of two line segments a-b and c-d.
* The intersection distance is defined as the normalized distance (0..1)
* from point a to the intersection point of a-b and c-d.
*/
static float get_intersection_distance_of_segments(const float2 &co_a,
const float2 &co_b,
const float2 &co_c,
const float2 &co_d)
{
/* Get intersection point. */
const float a1 = co_b[1] - co_a[1];
const float b1 = co_a[0] - co_b[0];
const float c1 = a1 * co_a[0] + b1 * co_a[1];
const float a2 = co_d[1] - co_c[1];
const float b2 = co_c[0] - co_d[0];
const float c2 = a2 * co_c[0] + b2 * co_c[1];
const float det = float(a1 * b2 - a2 * b1);
if (det == 0.0f) {
return 0.0f;
}
float2 isect((b2 * c1 - b1 * c2) / det, (a1 * c2 - a2 * c1) / det);
/* Get normalized distance from point a to intersection point. */
const float length_ab = math::length(co_b - co_a);
float distance = (length_ab == 0.0f ?
0.0f :
math::clamp(math::length(isect - co_a) / length_ab, 0.0f, 1.0f));
return distance;
}
/**
* For a curve, find all intersections with other curves.
*/
static void get_intersections_of_curve_with_curves(const int src_curve,
const bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
const Span<rcti> screen_space_curve_bounds,
MutableSpan<bool> r_is_intersected_after_point,
MutableSpan<float2> r_intersection_distance)
{
const OffsetIndices<int> points_by_curve = src.points_by_curve();
const VArray<bool> is_cyclic = src.cyclic();
/* Edge case: skip curve with only one point. */
if (points_by_curve[src_curve].size() < 2) {
return;
}
/* Loop all curve points and check for intersections between point a and point a + 1. */
const IndexRange src_curve_points = points_by_curve[src_curve].drop_back(
is_cyclic[src_curve] ? 0 : 1);
for (const int point_a : src_curve_points) {
const int point_b = (point_a == points_by_curve[src_curve].last()) ? src_curve_points.first() :
point_a + 1;
/* Get coordinates of segment a-b. */
const float2 co_a = screen_space_positions[point_a];
const float2 co_b = screen_space_positions[point_b];
rcti bbox_ab;
BLI_rcti_init_minmax(&bbox_ab);
BLI_rcti_do_minmax_v(&bbox_ab, int2(co_a));
BLI_rcti_do_minmax_v(&bbox_ab, int2(co_b));
BLI_rcti_pad(&bbox_ab, BBOX_PADDING, BBOX_PADDING);
float intersection_distance_min = FLT_MAX;
float intersection_distance_max = -FLT_MAX;
/* Loop all curves, looking for intersecting segments. */
for (const int curve : src.curves_range()) {
/* Only process curves with at least two points. */
if (points_by_curve[curve].size() < 2) {
continue;
}
/* Bounding box check: skip curves that don't overlap segment a-b. */
if (!BLI_rcti_isect(&bbox_ab, &screen_space_curve_bounds[curve], nullptr)) {
continue;
}
/* Find intersecting curve segments. */
const IndexRange points = points_by_curve[curve].drop_back(is_cyclic[curve] ? 0 : 1);
for (const int point_c : points) {
const int point_d = (point_c == points_by_curve[curve].last()) ? points.first() :
(point_c + 1);
/* Don't self check. */
if (curve == src_curve &&
(point_a == point_c || point_a == point_d || point_b == point_c || point_b == point_d))
{
continue;
}
/* Skip when bounding boxes of a-b and c-d don't overlap. */
const float2 co_c = screen_space_positions[point_c];
const float2 co_d = screen_space_positions[point_d];
rcti bbox_cd;
BLI_rcti_init_minmax(&bbox_cd);
BLI_rcti_do_minmax_v(&bbox_cd, int2(co_c));
BLI_rcti_do_minmax_v(&bbox_cd, int2(co_d));
BLI_rcti_pad(&bbox_cd, BBOX_PADDING, BBOX_PADDING);
if (!BLI_rcti_isect(&bbox_ab, &bbox_cd, nullptr)) {
continue;
}
/* Add some padding to the line segment c-d, otherwise we could just miss an
* intersection. */
const float2 padding_cd = math::normalize(co_d - co_c);
const float2 padded_c = co_c - padding_cd;
const float2 padded_d = co_d + padding_cd;
/* Check for intersection. */
const auto isect = math::isect_seg_seg(co_a, co_b, padded_c, padded_d);
if (ELEM(isect.kind, isect.LINE_LINE_CROSS, isect.LINE_LINE_EXACT)) {
/* We found an intersection, set the intersection flag for segment a-b. */
r_is_intersected_after_point[point_a] = true;
/* Calculate the intersection factor. This is the normalized distance (0..1) of the
* intersection point on line segment a-b, measured from point a. */
const float normalized_distance = get_intersection_distance_of_segments(
co_a, co_b, co_c, co_d);
intersection_distance_min = math::min(normalized_distance, intersection_distance_min);
intersection_distance_max = math::max(normalized_distance, intersection_distance_max);
}
}
}
if (r_is_intersected_after_point[point_a]) {
r_intersection_distance[point_a][Distance::Min] = intersection_distance_min;
r_intersection_distance[point_a][Distance::Max] = intersection_distance_max;
}
}
}
/**
* Expand a cutter segment by walking along the curve in forward or backward direction.
* A cutter segments ends at an intersection with another curve, or at the outer end of the curve.
*/
static void expand_cutter_segment_direction(Segment &segment,
const int direction,
const bke::CurvesGeometry &src,
const Span<bool> is_intersected_after_point,
const Span<float2> intersection_distance,
MutableSpan<bool> point_is_in_segment)
{
const OffsetIndices<int> points_by_curve = src.points_by_curve();
const int point_first = points_by_curve[segment.curve].first();
const int point_last = points_by_curve[segment.curve].last();
const Side segment_side = (direction == 1) ? Side::End : Side::Start;
int point_a = segment.point_range[segment_side];
bool intersected = false;
segment.is_intersected[segment_side] = false;
/* Walk along the curve points. */
while ((direction == 1 && point_a < point_last) || (direction == -1 && point_a > point_first)) {
const int point_b = point_a + direction;
const bool at_end_of_curve = (direction == -1 && point_b == point_first) ||
(direction == 1 && point_b == point_last);
/* Expand segment point range. */
segment.point_range[segment_side] = point_a;
point_is_in_segment[point_a] = true;
/* Check for intersections with other curves. The intersections were established in ascending
* point order, so in forward direction we look at line segment a-b, in backward direction we
* look at line segment b-a. */
const int intersection_point = direction == 1 ? point_a : point_b;
intersected = is_intersected_after_point[intersection_point];
/* Avoid orphaned points at the end of a curve. */
if (at_end_of_curve &&
((direction == -1 &&
intersection_distance[intersection_point][Distance::Max] < DISTANCE_FACTOR_THRESHOLD) ||
(direction == 1 && intersection_distance[intersection_point][Distance::Min] >
(1.0f - DISTANCE_FACTOR_THRESHOLD))))
{
intersected = false;
break;
}
/* When we hit an intersection, store the intersection distance. Potentially, line segment
* a-b can be intersected by multiple curves, so we want to fetch the first intersection
* point we bumped into. In forward direction this is the minimum distance, in backward
* direction the maximum. */
if (intersected) {
segment.is_intersected[segment_side] = true;
segment.intersection_distance[segment_side] =
(direction == 1) ? intersection_distance[intersection_point][Distance::Min] :
intersection_distance[intersection_point][Distance::Max];
break;
}
/* Keep walking along curve. */
point_a += direction;
}
/* Adjust point range at curve ends. */
if (!intersected) {
if (direction == -1) {
segment.point_range[Side::Start] = point_first;
point_is_in_segment[point_first] = true;
}
else {
segment.point_range[Side::End] = point_last;
point_is_in_segment[point_last] = true;
}
}
}
/**
* Expand a cutter segment of one point by walking along the curve in both directions.
*/
static void expand_cutter_segment(Segment &segment,
const bke::CurvesGeometry &src,
const Span<bool> is_intersected_after_point,
const Span<float2> intersection_distance,
MutableSpan<bool> point_is_in_segment)
{
const int8_t directions[2] = {-1, 1};
for (const int8_t direction : directions) {
expand_cutter_segment_direction(segment,
direction,
src,
is_intersected_after_point,
intersection_distance,
point_is_in_segment);
}
}
bke::CurvesGeometry trim_curve_segments(const bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
const Span<rcti> screen_space_curve_bounds,
const IndexMask &curve_selection,
const Vector<Vector<int>> &selected_points_in_curves,
const bool keep_caps)
{
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
/* For the selected curves, find all the intersections with other curves. */
const int src_points_num = src.points_num();
Array<bool> is_intersected_after_point(src_points_num, false);
Array<float2> intersection_distance(src_points_num);
curve_selection.foreach_index(GrainSize(32), [&](const int curve_i) {
get_intersections_of_curve_with_curves(curve_i,
src,
screen_space_positions,
screen_space_curve_bounds,
is_intersected_after_point,
intersection_distance);
});
/* Expand the selected curve points to cutter segments (the part of the curve between two
* intersections). */
const VArray<bool> is_cyclic = src.cyclic();
Array<bool> point_is_in_segment(src_points_num, false);
threading::EnumerableThreadSpecific<Segments> cutter_segments_by_thread;
curve_selection.foreach_index(GrainSize(32), [&](const int curve_i, const int pos) {
Segments &thread_segments = cutter_segments_by_thread.local();
for (const int selected_point : selected_points_in_curves[pos]) {
/* Skip point when it is already part of a cutter segment. */
if (point_is_in_segment[selected_point]) {
continue;
}
/* Create new cutter segment. */
Segment *segment = thread_segments.create_segment(curve_i, selected_point);
/* Expand the cutter segment in both directions until an intersection is found or the
* end of the curve is reached. */
expand_cutter_segment(
*segment, src, is_intersected_after_point, intersection_distance, point_is_in_segment);
/* When the end of a curve is reached and the curve is cyclic, we add an extra cutter
* segment for the cyclic second part. */
if (is_cyclic[curve_i] &&
(!segment->is_intersected[Side::Start] || !segment->is_intersected[Side::End]) &&
!(!segment->is_intersected[Side::Start] && !segment->is_intersected[Side::End]))
{
const int cyclic_outer_point = !segment->is_intersected[Side::Start] ?
src_points_by_curve[curve_i].last() :
src_points_by_curve[curve_i].first();
segment = thread_segments.create_segment(curve_i, cyclic_outer_point);
/* Expand this second segment. */
expand_cutter_segment(
*segment, src, is_intersected_after_point, intersection_distance, point_is_in_segment);
}
}
});
Segments cutter_segments;
for (Segments &thread_segments : cutter_segments_by_thread) {
cutter_segments.segments.extend(thread_segments.segments);
}
/* Abort when no cutter segments are found in the lasso area. */
bke::CurvesGeometry dst;
if (cutter_segments.segments.is_empty()) {
return dst;
}
/* Merge adjacent cutter segments. E.g. two point ranges of 0-10 and 11-20 will be merged
* to one range of 0-20. */
cutter_segments.merge_adjacent_segments();
/* Create the point transfer data, for converting the source geometry into the new geometry.
* First, add all curve points not affected by the cutter tool. */
Array<Vector<PointTransferData>> src_to_dst_points(src_points_num);
for (const int src_curve : src.curves_range()) {
const IndexRange src_points = src_points_by_curve[src_curve];
for (const int src_point : src_points) {
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
const int src_next_point = (src_point == src_points.last()) ? src_points.first() :
(src_point + 1);
/* Add the source point only if it does not lie inside a cutter segment. */
if (!point_is_in_segment[src_point]) {
dst_points.append({src_point, src_next_point, 0.0f, true, false});
}
}
}
/* Add new curve points at the intersection points of the cutter segments.
*
* a b
* source curve o--------o---*---o--------o----*---o--------o
* ^ ^
* cutter segment |-----------------|
*
* o = existing curve point
* * = newly created curve point
*
* The curve points between *a and *b will be deleted.
* The source curve will be cut in two:
* - the first curve ends at *a
* - the second curve starts at *b
*
* We avoid inserting a new point very close to the adjacent one, because that's just adding
* clutter to the geometry.
*/
for (const Segment &cutter_segment : cutter_segments.segments) {
/* Intersection at cutter segment start. */
if (cutter_segment.is_intersected[Side::Start] &&
cutter_segment.intersection_distance[Side::Start] > DISTANCE_FACTOR_THRESHOLD)
{
const int src_point = cutter_segment.point_range[Side::Start] - 1;
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
dst_points.append({src_point,
src_point + 1,
cutter_segment.intersection_distance[Side::Start],
false,
false});
}
/* Intersection at cutter segment end. */
if (cutter_segment.is_intersected[Side::End]) {
const int src_point = cutter_segment.point_range[Side::End];
if (cutter_segment.intersection_distance[Side::End] < (1.0f - DISTANCE_FACTOR_THRESHOLD)) {
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point];
dst_points.append({src_point,
src_point + 1,
cutter_segment.intersection_distance[Side::End],
false,
true});
}
else {
/* Mark the 'is_cut' flag on the next point, because a new curve is starting here after
* the removed cutter segment. */
Vector<PointTransferData> &dst_points = src_to_dst_points[src_point + 1];
for (PointTransferData &dst_point : dst_points) {
if (dst_point.is_src_point) {
dst_point.is_cut = true;
}
}
}
}
}
/* Create the new curves geometry. */
compute_topology_change(src, dst, src_to_dst_points, keep_caps);
return dst;
}
} // namespace cutter
} // namespace blender::ed::greasepencil

@ -591,4 +591,13 @@ bke::CurvesGeometry create_curves_outline(const bke::greasepencil::Drawing &draw
float outline_offset,
int material_index);
namespace cutter {
bke::CurvesGeometry trim_curve_segments(const bke::CurvesGeometry &src,
Span<float2> screen_space_positions,
Span<rcti> screen_space_curve_bounds,
const IndexMask &curve_selection,
const Vector<Vector<int>> &selected_points_in_curves,
bool keep_caps);
}; // namespace cutter
} // namespace blender::ed::greasepencil